--- 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