ML Engine Updates: - Updated BTCUSD with Polygon API data (2024-2025): 215,699 new records - Re-trained all ML models: Attention (R²: 0.223), Base, Metamodel (87.3% confidence) - Backtest results: +176.71R profit with aggressive_filter strategy Documentation Consolidation: - Created docs/99-analisis/_MAP.md index with 13 new analysis documents - Consolidated inventories: removed duplicates from orchestration/inventarios/ - Updated ML_INVENTORY.yml with BTCUSD metrics and training results - Added execution reports: FASE11-BTCUSD, correction issues, alignment validation Architecture & Integration: - Updated all module documentation with NEXUS v3.4 frontmatter - Fixed _MAP.md indexes across all folders - Updated orchestration plans and traces Files: 229 changed, 5064 insertions(+), 1872 deletions(-) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
784 lines
16 KiB
Markdown
784 lines
16 KiB
Markdown
---
|
|
id: "ET-ML-004"
|
|
title: "FastAPI Endpoints"
|
|
type: "Technical Specification"
|
|
status: "Done"
|
|
priority: "Alta"
|
|
epic: "OQI-006"
|
|
project: "trading-platform"
|
|
version: "1.0.0"
|
|
created_date: "2025-12-05"
|
|
updated_date: "2026-01-04"
|
|
---
|
|
|
|
# ET-ML-004: FastAPI Endpoints
|
|
|
|
## Metadata
|
|
|
|
| Campo | Valor |
|
|
|-------|-------|
|
|
| **ID** | ET-ML-004 |
|
|
| **Épica** | OQI-006 - Señales ML |
|
|
| **Tipo** | Especificación Técnica |
|
|
| **Versión** | 1.0.0 |
|
|
| **Estado** | Aprobado |
|
|
| **Última actualización** | 2025-12-05 |
|
|
|
|
---
|
|
|
|
## Propósito
|
|
|
|
Especificar los endpoints de la API REST del ML Engine, incluyendo schemas de request/response, validación, autenticación y documentación OpenAPI.
|
|
|
|
---
|
|
|
|
## Base URL
|
|
|
|
```
|
|
Production: https://ml.trading.com/api/v1
|
|
Development: http://localhost:8000/api/v1
|
|
```
|
|
|
|
---
|
|
|
|
## Autenticación
|
|
|
|
Todos los endpoints requieren API Key en el header:
|
|
|
|
```http
|
|
X-API-Key: your-api-key-here
|
|
```
|
|
|
|
---
|
|
|
|
## Endpoints
|
|
|
|
### 1. Predictions
|
|
|
|
#### POST /predictions
|
|
|
|
Genera predicción de rango de precio.
|
|
|
|
**Request:**
|
|
```typescript
|
|
{
|
|
symbol: string; // "BTCUSDT" | "ETHUSDT"
|
|
horizon: number; // 6 | 18 | 36 | 72 (candles)
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
success: boolean;
|
|
data: {
|
|
symbol: string;
|
|
horizon: number;
|
|
horizon_label: string; // "scalping" | "intraday" | "swing" | "position"
|
|
timestamp: string; // ISO 8601
|
|
|
|
current_price: number;
|
|
predicted_high: number;
|
|
predicted_low: number;
|
|
|
|
delta_high_percent: number;
|
|
delta_low_percent: number;
|
|
range_percent: number;
|
|
|
|
confidence: {
|
|
mae: number; // Historical MAE for this horizon
|
|
model_version: string;
|
|
};
|
|
|
|
expires_at: string; // ISO 8601 - When this prediction expires
|
|
};
|
|
metadata: {
|
|
request_id: string;
|
|
latency_ms: number;
|
|
cached: boolean;
|
|
};
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
```bash
|
|
curl -X POST https://ml.trading.com/api/v1/predictions \
|
|
-H "X-API-Key: your-key" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"symbol": "BTCUSDT", "horizon": 18}'
|
|
```
|
|
|
|
**Response Example:**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"data": {
|
|
"symbol": "BTCUSDT",
|
|
"horizon": 18,
|
|
"horizon_label": "intraday",
|
|
"timestamp": "2025-12-05T10:30:00Z",
|
|
"current_price": 43250.50,
|
|
"predicted_high": 43520.75,
|
|
"predicted_low": 43012.30,
|
|
"delta_high_percent": 0.625,
|
|
"delta_low_percent": -0.551,
|
|
"range_percent": 1.176,
|
|
"confidence": {
|
|
"mae": 0.32,
|
|
"model_version": "v1.2.0"
|
|
},
|
|
"expires_at": "2025-12-05T12:00:00Z"
|
|
},
|
|
"metadata": {
|
|
"request_id": "req_abc123",
|
|
"latency_ms": 45,
|
|
"cached": false
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 2. Signals
|
|
|
|
#### POST /signals
|
|
|
|
Genera señal de trading.
|
|
|
|
**Request:**
|
|
```typescript
|
|
{
|
|
symbol: string;
|
|
horizon: number;
|
|
include_range?: boolean; // Include price range prediction
|
|
include_tpsl?: boolean; // Include TP/SL prediction
|
|
}
|
|
```
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
success: boolean;
|
|
data: {
|
|
symbol: string;
|
|
horizon: number;
|
|
timestamp: string;
|
|
|
|
signal: {
|
|
type: "buy" | "sell" | "hold";
|
|
confidence: number; // 0.0 - 1.0
|
|
strength: "weak" | "moderate" | "strong";
|
|
|
|
probabilities: {
|
|
hold: number;
|
|
buy: number;
|
|
sell: number;
|
|
};
|
|
};
|
|
|
|
price_range?: { // If include_range = true
|
|
current: number;
|
|
predicted_high: number;
|
|
predicted_low: number;
|
|
};
|
|
|
|
tpsl?: { // If include_tpsl = true
|
|
prediction: "take_profit" | "stop_loss";
|
|
probability_tp: number;
|
|
probability_sl: number;
|
|
suggested_tp_percent: number;
|
|
suggested_sl_percent: number;
|
|
};
|
|
|
|
recommendation: {
|
|
action: "BUY" | "SELL" | "HOLD";
|
|
entry_zone?: {
|
|
min: number;
|
|
max: number;
|
|
};
|
|
take_profit?: number;
|
|
stop_loss?: number;
|
|
risk_reward?: string; // "1:2.5"
|
|
quality: "low" | "medium" | "high";
|
|
reason: string;
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
```bash
|
|
curl -X POST https://ml.trading.com/api/v1/signals \
|
|
-H "X-API-Key: your-key" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"symbol": "BTCUSDT", "horizon": 18, "include_range": true, "include_tpsl": true}'
|
|
```
|
|
|
|
---
|
|
|
|
#### GET /signals/history
|
|
|
|
Obtiene historial de señales.
|
|
|
|
**Query Parameters:**
|
|
```
|
|
symbol: string (required)
|
|
horizon: number (optional)
|
|
type: string (optional) - "buy" | "sell" | "hold"
|
|
from: string (optional) - ISO 8601 date
|
|
to: string (optional) - ISO 8601 date
|
|
limit: number (optional) - default 50, max 100
|
|
offset: number (optional) - default 0
|
|
```
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
success: boolean;
|
|
data: {
|
|
signals: Array<{
|
|
id: string;
|
|
symbol: string;
|
|
horizon: number;
|
|
type: string;
|
|
confidence: number;
|
|
current_price: number;
|
|
created_at: string;
|
|
outcome?: {
|
|
result: "profit" | "loss" | "pending";
|
|
pnl_percent?: number;
|
|
closed_at?: string;
|
|
};
|
|
}>;
|
|
pagination: {
|
|
total: number;
|
|
limit: number;
|
|
offset: number;
|
|
has_more: boolean;
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 3. Indicators
|
|
|
|
#### GET /indicators
|
|
|
|
Obtiene indicadores técnicos actuales.
|
|
|
|
**Query Parameters:**
|
|
```
|
|
symbol: string (required)
|
|
indicators: string (optional) - comma-separated list
|
|
```
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
success: boolean;
|
|
data: {
|
|
symbol: string;
|
|
timestamp: string;
|
|
price: number;
|
|
|
|
indicators: {
|
|
rsi_14: number;
|
|
macd: {
|
|
line: number;
|
|
signal: number;
|
|
histogram: number;
|
|
};
|
|
bollinger: {
|
|
upper: number;
|
|
middle: number;
|
|
lower: number;
|
|
position: number; // 0-100
|
|
};
|
|
moving_averages: {
|
|
sma_20: number;
|
|
sma_50: number;
|
|
ema_12: number;
|
|
ema_26: number;
|
|
};
|
|
momentum: {
|
|
roc_10: number;
|
|
stochastic_k: number;
|
|
stochastic_d: number;
|
|
williams_r: number;
|
|
};
|
|
volume: {
|
|
current: number;
|
|
avg_20: number;
|
|
ratio: number;
|
|
mfi_14: number;
|
|
};
|
|
volatility: {
|
|
atr_14: number;
|
|
atr_percent: number;
|
|
std_20: number;
|
|
};
|
|
};
|
|
|
|
summary: {
|
|
trend: "bullish" | "bearish" | "neutral";
|
|
momentum: "overbought" | "oversold" | "neutral";
|
|
volatility: "high" | "normal" | "low";
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 4. Models
|
|
|
|
#### GET /models/status
|
|
|
|
Estado de los modelos cargados.
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
success: boolean;
|
|
data: {
|
|
models: Array<{
|
|
name: string;
|
|
type: "regressor" | "classifier";
|
|
version: string;
|
|
loaded: boolean;
|
|
last_trained: string;
|
|
|
|
metrics: {
|
|
accuracy?: number;
|
|
mae?: number;
|
|
f1_score?: number;
|
|
};
|
|
|
|
symbols: string[];
|
|
horizons: number[];
|
|
}>;
|
|
|
|
system: {
|
|
total_models: number;
|
|
loaded_models: number;
|
|
memory_usage_mb: number;
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
#### GET /models/{model_name}/metrics
|
|
|
|
Métricas detalladas de un modelo.
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
success: boolean;
|
|
data: {
|
|
model_name: string;
|
|
version: string;
|
|
|
|
training: {
|
|
samples: number;
|
|
features: number;
|
|
trained_at: string;
|
|
training_time_seconds: number;
|
|
};
|
|
|
|
performance: {
|
|
// For regressors
|
|
mae?: number;
|
|
mse?: number;
|
|
rmse?: number;
|
|
mape?: number;
|
|
|
|
// For classifiers
|
|
accuracy?: number;
|
|
precision?: number;
|
|
recall?: number;
|
|
f1_score?: number;
|
|
auc?: number;
|
|
};
|
|
|
|
feature_importance: Array<{
|
|
feature: string;
|
|
importance: number;
|
|
}>;
|
|
|
|
recent_predictions: {
|
|
total: number;
|
|
correct: number;
|
|
accuracy_24h: number;
|
|
};
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### 5. Health
|
|
|
|
#### GET /health
|
|
|
|
Health check básico.
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
status: "healthy" | "degraded" | "unhealthy";
|
|
timestamp: string;
|
|
}
|
|
```
|
|
|
|
#### GET /health/detailed
|
|
|
|
Health check detallado.
|
|
|
|
**Response:**
|
|
```typescript
|
|
{
|
|
status: "healthy" | "degraded" | "unhealthy";
|
|
timestamp: string;
|
|
|
|
components: {
|
|
api: {
|
|
status: "up" | "down";
|
|
response_time_ms: number;
|
|
};
|
|
models: {
|
|
status: "up" | "down";
|
|
loaded_count: number;
|
|
total_count: number;
|
|
};
|
|
redis: {
|
|
status: "up" | "down";
|
|
latency_ms: number;
|
|
};
|
|
binance: {
|
|
status: "up" | "down";
|
|
last_price_update: string;
|
|
};
|
|
database: {
|
|
status: "up" | "down";
|
|
connection_pool: {
|
|
active: number;
|
|
idle: number;
|
|
max: number;
|
|
};
|
|
};
|
|
};
|
|
|
|
metrics: {
|
|
uptime_seconds: number;
|
|
requests_per_minute: number;
|
|
avg_latency_ms: number;
|
|
error_rate_percent: number;
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Schemas (Pydantic)
|
|
|
|
```python
|
|
# app/schemas/prediction.py
|
|
from pydantic import BaseModel, Field, validator
|
|
from typing import Optional, Literal
|
|
from datetime import datetime
|
|
|
|
class PredictionRequest(BaseModel):
|
|
symbol: str = Field(..., description="Trading pair symbol")
|
|
horizon: int = Field(..., description="Prediction horizon in candles")
|
|
|
|
@validator('symbol')
|
|
def validate_symbol(cls, v):
|
|
valid_symbols = ['BTCUSDT', 'ETHUSDT']
|
|
if v not in valid_symbols:
|
|
raise ValueError(f'Symbol must be one of: {valid_symbols}')
|
|
return v
|
|
|
|
@validator('horizon')
|
|
def validate_horizon(cls, v):
|
|
valid_horizons = [6, 18, 36, 72]
|
|
if v not in valid_horizons:
|
|
raise ValueError(f'Horizon must be one of: {valid_horizons}')
|
|
return v
|
|
|
|
class Config:
|
|
schema_extra = {
|
|
"example": {
|
|
"symbol": "BTCUSDT",
|
|
"horizon": 18
|
|
}
|
|
}
|
|
|
|
class PredictionConfidence(BaseModel):
|
|
mae: float
|
|
model_version: str
|
|
|
|
class PredictionData(BaseModel):
|
|
symbol: str
|
|
horizon: int
|
|
horizon_label: str
|
|
timestamp: datetime
|
|
|
|
current_price: float
|
|
predicted_high: float
|
|
predicted_low: float
|
|
|
|
delta_high_percent: float
|
|
delta_low_percent: float
|
|
range_percent: float
|
|
|
|
confidence: PredictionConfidence
|
|
expires_at: datetime
|
|
|
|
class ResponseMetadata(BaseModel):
|
|
request_id: str
|
|
latency_ms: int
|
|
cached: bool
|
|
|
|
class PredictionResponse(BaseModel):
|
|
success: bool
|
|
data: PredictionData
|
|
metadata: ResponseMetadata
|
|
```
|
|
|
|
```python
|
|
# app/schemas/signal.py
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, Literal
|
|
from datetime import datetime
|
|
|
|
class SignalRequest(BaseModel):
|
|
symbol: str
|
|
horizon: int
|
|
include_range: bool = False
|
|
include_tpsl: bool = False
|
|
|
|
class SignalProbabilities(BaseModel):
|
|
hold: float
|
|
buy: float
|
|
sell: float
|
|
|
|
class Signal(BaseModel):
|
|
type: Literal["buy", "sell", "hold"]
|
|
confidence: float = Field(..., ge=0, le=1)
|
|
strength: Literal["weak", "moderate", "strong"]
|
|
probabilities: SignalProbabilities
|
|
|
|
class PriceRange(BaseModel):
|
|
current: float
|
|
predicted_high: float
|
|
predicted_low: float
|
|
|
|
class TPSL(BaseModel):
|
|
prediction: Literal["take_profit", "stop_loss"]
|
|
probability_tp: float
|
|
probability_sl: float
|
|
suggested_tp_percent: float
|
|
suggested_sl_percent: float
|
|
|
|
class EntryZone(BaseModel):
|
|
min: float
|
|
max: float
|
|
|
|
class Recommendation(BaseModel):
|
|
action: Literal["BUY", "SELL", "HOLD"]
|
|
entry_zone: Optional[EntryZone]
|
|
take_profit: Optional[float]
|
|
stop_loss: Optional[float]
|
|
risk_reward: Optional[str]
|
|
quality: Literal["low", "medium", "high"]
|
|
reason: str
|
|
|
|
class SignalData(BaseModel):
|
|
symbol: str
|
|
horizon: int
|
|
timestamp: datetime
|
|
signal: Signal
|
|
price_range: Optional[PriceRange]
|
|
tpsl: Optional[TPSL]
|
|
recommendation: Recommendation
|
|
|
|
class SignalResponse(BaseModel):
|
|
success: bool
|
|
data: SignalData
|
|
```
|
|
|
|
---
|
|
|
|
## Router Implementation
|
|
|
|
```python
|
|
# app/api/routers/predictions.py
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from app.schemas.prediction import PredictionRequest, PredictionResponse
|
|
from app.services.predictor import PredictorService
|
|
from app.core.security import validate_api_key
|
|
from app.core.rate_limit import limiter
|
|
import uuid
|
|
import time
|
|
|
|
router = APIRouter()
|
|
|
|
@router.post("", response_model=PredictionResponse)
|
|
@limiter.limit("100/minute")
|
|
async def create_prediction(
|
|
request: PredictionRequest,
|
|
api_key: str = Depends(validate_api_key),
|
|
predictor: PredictorService = Depends()
|
|
):
|
|
"""
|
|
Generate price range prediction for a trading pair.
|
|
|
|
- **symbol**: Trading pair (BTCUSDT, ETHUSDT)
|
|
- **horizon**: Prediction horizon in 5-minute candles (6, 18, 36, 72)
|
|
"""
|
|
start_time = time.time()
|
|
request_id = str(uuid.uuid4())[:8]
|
|
|
|
try:
|
|
prediction, cached = await predictor.predict(
|
|
symbol=request.symbol,
|
|
horizon=request.horizon
|
|
)
|
|
|
|
latency_ms = int((time.time() - start_time) * 1000)
|
|
|
|
return PredictionResponse(
|
|
success=True,
|
|
data=prediction,
|
|
metadata={
|
|
"request_id": f"req_{request_id}",
|
|
"latency_ms": latency_ms,
|
|
"cached": cached
|
|
}
|
|
)
|
|
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
```
|
|
|
|
```python
|
|
# app/api/routers/signals.py
|
|
from fastapi import APIRouter, Depends, Query
|
|
from typing import Optional, List
|
|
from app.schemas.signal import SignalRequest, SignalResponse, SignalHistoryResponse
|
|
from app.services.signal_generator import SignalGeneratorService
|
|
from app.core.security import validate_api_key
|
|
|
|
router = APIRouter()
|
|
|
|
@router.post("", response_model=SignalResponse)
|
|
async def generate_signal(
|
|
request: SignalRequest,
|
|
api_key: str = Depends(validate_api_key),
|
|
signal_gen: SignalGeneratorService = Depends()
|
|
):
|
|
"""Generate trading signal with optional range and TP/SL predictions."""
|
|
return await signal_gen.generate(
|
|
symbol=request.symbol,
|
|
horizon=request.horizon,
|
|
include_range=request.include_range,
|
|
include_tpsl=request.include_tpsl
|
|
)
|
|
|
|
@router.get("/history")
|
|
async def get_signal_history(
|
|
symbol: str,
|
|
horizon: Optional[int] = None,
|
|
type: Optional[str] = Query(None, regex="^(buy|sell|hold)$"),
|
|
limit: int = Query(50, le=100),
|
|
offset: int = 0,
|
|
api_key: str = Depends(validate_api_key),
|
|
signal_gen: SignalGeneratorService = Depends()
|
|
):
|
|
"""Get historical signals with optional filters."""
|
|
return await signal_gen.get_history(
|
|
symbol=symbol,
|
|
horizon=horizon,
|
|
signal_type=type,
|
|
limit=limit,
|
|
offset=offset
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Error Responses
|
|
|
|
```python
|
|
# app/core/exceptions.py
|
|
from fastapi import HTTPException
|
|
|
|
class APIError(HTTPException):
|
|
"""Base API error"""
|
|
pass
|
|
|
|
class ValidationError(APIError):
|
|
def __init__(self, detail: str):
|
|
super().__init__(status_code=400, detail=detail)
|
|
|
|
class AuthenticationError(APIError):
|
|
def __init__(self):
|
|
super().__init__(status_code=401, detail="Invalid API key")
|
|
|
|
class RateLimitError(APIError):
|
|
def __init__(self):
|
|
super().__init__(status_code=429, detail="Rate limit exceeded")
|
|
|
|
class ModelError(APIError):
|
|
def __init__(self, detail: str):
|
|
super().__init__(status_code=500, detail=f"Model error: {detail}")
|
|
```
|
|
|
|
**Error Response Format:**
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": {
|
|
"code": "VALIDATION_ERROR",
|
|
"message": "Symbol XYZUSDT is not supported",
|
|
"details": {
|
|
"field": "symbol",
|
|
"valid_values": ["BTCUSDT", "ETHUSDT"]
|
|
}
|
|
},
|
|
"metadata": {
|
|
"request_id": "req_abc123",
|
|
"timestamp": "2025-12-05T10:30:00Z"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Rate Limits
|
|
|
|
| Endpoint | Limit | Window |
|
|
|----------|-------|--------|
|
|
| POST /predictions | 100 | 1 minute |
|
|
| POST /signals | 100 | 1 minute |
|
|
| GET /signals/history | 60 | 1 minute |
|
|
| GET /indicators | 120 | 1 minute |
|
|
| GET /models/* | 30 | 1 minute |
|
|
| GET /health | Unlimited | - |
|
|
|
|
---
|
|
|
|
## Referencias
|
|
|
|
- [ET-ML-001: Arquitectura](./ET-ML-001-arquitectura.md)
|
|
- [FastAPI Documentation](https://fastapi.tiangolo.com/)
|
|
- [OpenAPI Specification](https://swagger.io/specification/)
|
|
|
|
---
|
|
|
|
**Autor:** Requirements-Analyst
|
|
**Fecha:** 2025-12-05
|