# 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.orbiquant.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.orbiquant.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.orbiquant.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
