# RF-ML-004: Pipeline de Entrenamiento

**Versión:** 1.0.0
**Fecha:** 2025-12-05
**Épica:** OQI-006 - Señales ML y Predicciones
**Prioridad:** P1
**Story Points:** 8

---

## Descripción

El sistema debe proporcionar un pipeline completo de entrenamiento de modelos XGBoost, incluyendo descarga de datos históricos, feature engineering, entrenamiento, validación y persistencia de modelos. El pipeline debe ser ejecutable bajo demanda y soportar entrenamiento programado.

---

## Requisitos Funcionales

### RF-ML-004.1: Descarga de Datos Históricos

El sistema debe:
- Descargar datos históricos de Binance API (velas de 5 minutos)
- Soportar cantidad configurable de samples (default: 500, máximo: 5000)
- Almacenar datos en formato OHLCV (Open, High, Low, Close, Volume)
- Validar integridad de datos (sin gaps temporales)

**Configuración:**
```python
@dataclass
class TrainingConfig:
    symbol: str              # ej: "BTCUSDT"
    samples: int = 500       # Número de velas históricas
    interval: str = "5m"     # Siempre 5 minutos
    test_split: float = 0.2  # 20% para testing
    validation_split: float = 0.1  # 10% para validación
```

### RF-ML-004.2: Feature Engineering

El sistema debe:
- Calcular 30+ features técnicas (RF-ML-003)
- Eliminar filas con valores NaN/Inf
- Normalizar features si es necesario
- Crear features de rezagos (lags) si aplica

**Features calculadas:**
- Volatilidad: 8 features
- Momentum: 6 features
- Medias Móviles: 12 features
- Indicadores: 4 features (RSI, MACD, BB)
- Volumen: 1 feature
- High/Low: 6+ features

### RF-ML-004.3: Generación de Targets

El sistema debe generar targets para cada horizonte:

```python
# Para cada horizonte (scalping, intraday, swing, position)
horizons = {
    'scalping': 6,    # 30 min
    'intraday': 18,   # 90 min
    'swing': 36,      # 3 horas
    'position': 72    # 6 horas
}

for horizon_name, n_candles in horizons.items():
    # Calcular max/min futuro
    future_high = max(high[i:i+n_candles])
    future_low = min(low[i:i+n_candles])

    # Calcular ratios
    max_ratio = future_high / close[i] - 1
    min_ratio = 1 - future_low / close[i]

    # Asignar como target
    y_high[i] = max_ratio
    y_low[i] = min_ratio
```

### RF-ML-004.4: División de Datos

El sistema debe dividir los datos en:

| Set | Porcentaje | Uso |
|-----|------------|-----|
| **Training** | 70% | Entrenar el modelo |
| **Validation** | 10% | Ajustar hiperparámetros |
| **Test** | 20% | Evaluar performance final |

**Importante:** División temporal (no aleatoria) para evitar look-ahead bias.

```python
# División temporal
total_samples = len(X)
train_end = int(total_samples * 0.7)
val_end = int(total_samples * 0.8)

X_train, y_train = X[:train_end], y[:train_end]
X_val, y_val = X[train_end:val_end], y[train_end:val_end]
X_test, y_test = X[val_end:], y[val_end:]
```

### RF-ML-004.5: Entrenamiento XGBoost

El sistema debe entrenar dos modelos por horizonte:
- **xgb_high:** Predice `max_ratio`
- **xgb_low:** Predice `min_ratio`

**Configuración del modelo:**
```python
xgb_params = {
    'objective': 'reg:squarederror',
    'n_estimators': 100,
    'max_depth': 6,
    'learning_rate': 0.1,
    'subsample': 0.8,
    'colsample_bytree': 0.8,
    'min_child_weight': 1,
    'random_state': 42,
    'n_jobs': -1  # Usar todos los cores
}

# Entrenar
model_high = XGBRegressor(**xgb_params)
model_high.fit(X_train, y_high_train)

model_low = XGBRegressor(**xgb_params)
model_low.fit(X_train, y_low_train)
```

### RF-ML-004.6: Validación y Métricas

El sistema debe calcular métricas de performance:

**Métricas principales:**
- **MAE (Mean Absolute Error):** Error promedio absoluto
- **RMSE (Root Mean Squared Error):** Raíz del error cuadrático medio
- **R² Score:** Coeficiente de determinación

```python
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score

# Predecir en test set
y_pred_high = model_high.predict(X_test)
y_pred_low = model_low.predict(X_test)

# Calcular métricas
metrics = {
    'high_mae': mean_absolute_error(y_test_high, y_pred_high),
    'high_rmse': np.sqrt(mean_squared_error(y_test_high, y_pred_high)),
    'high_r2': r2_score(y_test_high, y_pred_high),
    'low_mae': mean_absolute_error(y_test_low, y_pred_low),
    'low_rmse': np.sqrt(mean_squared_error(y_test_low, y_pred_low)),
    'low_r2': r2_score(y_test_low, y_pred_low),
    'train_samples': len(X_train),
    'test_samples': len(X_test)
}
```

**Umbrales de aceptación:**
| Métrica | Umbral | Significado |
|---------|--------|-------------|
| high_mae | < 0.02 | Error < 2% |
| high_rmse | < 0.025 | RMSE < 2.5% |
| low_mae | < 0.02 | Error < 2% |
| low_rmse | < 0.03 | RMSE < 3% |

### RF-ML-004.7: Persistencia de Modelos

El sistema debe:
- Guardar modelos entrenados en formato JSON (XGBoost nativo)
- Incluir metadata (símbolo, fecha, métricas, versión)
- Versionado de modelos

**Estructura de archivos:**
```
apps/ml-services/trained_models/
├── BTCUSDT/
│   ├── scalping/
│   │   ├── xgb_high_v1.0.0.json
│   │   ├── xgb_low_v1.0.0.json
│   │   └── metadata.json
│   ├── intraday/
│   ├── swing/
│   └── position/
└── ETHUSDT/
    └── ...
```

**Metadata.json:**
```json
{
  "model_version": "1.0.0",
  "symbol": "BTCUSDT",
  "horizon": "scalping",
  "trained_at": "2025-12-05T18:45:00.000Z",
  "samples": 500,
  "train_samples": 350,
  "test_samples": 100,
  "metrics": {
    "high_mae": 0.00099,
    "high_rmse": 0.00141,
    "low_mae": 0.00173,
    "low_rmse": 0.00284
  },
  "features": ["volatility_5", "rsi_14", ...],
  "xgb_params": { ... }
}
```

### RF-ML-004.8: Entrenamiento Programado

El sistema debe soportar:
- Entrenamiento manual vía API: `POST /api/train/{symbol}`
- Entrenamiento programado (cron job): semanal, cada domingo 2:00 AM
- Re-entrenamiento automático si MAE > umbral (degradación del modelo)

---

## Datos de Entrada

| Campo | Tipo | Descripción | Requerido |
|-------|------|-------------|-----------|
| symbol | string | Par de trading | Sí |
| samples | number | Cantidad de velas históricas | No (default: 500) |
| horizon | enum | Horizonte específico o "all" | No (default: "all") |

---

## Datos de Salida

### Inicio de Entrenamiento

```typescript
interface TrainingStartResponse {
  status: 'training_started' | 'already_training';
  symbol: string;
  samples: number;
  horizon: string;
  message: string;
}
```

**Ejemplo:**
```json
{
  "status": "training_started",
  "symbol": "BTCUSDT",
  "samples": 500,
  "horizon": "all",
  "message": "Model training started in background. Check /api/training/status for progress."
}
```

### Estado de Entrenamiento

```typescript
interface TrainingStatus {
  training_in_progress: boolean;
  is_trained: boolean;
  current_symbol?: string;
  progress_pct?: number;
  last_training?: {
    symbol: string;
    timestamp: string;
    samples: number;
    metrics: {
      high_mae: number;
      high_rmse: number;
      low_mae: number;
      low_rmse: number;
      train_samples: number;
      test_samples: number;
    };
  };
}
```

**Ejemplo:**
```json
{
  "training_in_progress": false,
  "is_trained": true,
  "last_training": {
    "symbol": "BTCUSDT",
    "timestamp": "2025-12-05T18:45:23.123456Z",
    "samples": 500,
    "metrics": {
      "high_mae": 0.00099,
      "high_rmse": 0.00141,
      "low_mae": 0.00173,
      "low_rmse": 0.00284,
      "train_samples": 355,
      "test_samples": 89
    }
  }
}
```

---

## Reglas de Negocio

1. **Un Entrenamiento a la Vez:** Solo puede haber un proceso de entrenamiento activo
2. **Mínimo de Samples:** Al menos 100 samples para entrenar
3. **Máximo de Samples:** Máximo 5000 samples (limitación de API Binance)
4. **Sobrescritura:** Un nuevo entrenamiento sobrescribe el modelo anterior
5. **Validación Pre-entrenamiento:** Verificar disponibilidad de datos antes de iniciar
6. **Timeout:** El entrenamiento tiene timeout de 30 minutos

---

## Criterios de Aceptación

```gherkin
Escenario: Iniciar entrenamiento manual
  DADO que el usuario es administrador
  CUANDO hace POST /api/train/BTCUSDT?samples=500
  ENTONCES el entrenamiento inicia en background
  Y recibe status "training_started"
  Y puede consultar el progreso en /api/training/status

Escenario: Entrenamiento completo exitoso
  DADO que el entrenamiento ha finalizado
  CUANDO consulta /api/training/status
  ENTONCES training_in_progress = false
  Y is_trained = true
  Y last_training contiene métricas
  Y high_mae < 0.02 y low_mae < 0.02

Escenario: Entrenamiento con datos insuficientes
  DADO que solo existen 50 velas históricas
  CUANDO intenta entrenar
  ENTONCES recibe error 400 Bad Request
  Y el mensaje indica "Insufficient data"

Escenario: Modelo entrenado está disponible
  DADO que el entrenamiento finalizó exitosamente
  CUANDO hace GET /api/predict/BTCUSDT
  ENTONCES usa el modelo recién entrenado
  Y is_trained = true en la respuesta
```

---

## Dependencias

### Técnicas:
- **XGBoost 2.0+:** Motor de ML
- **scikit-learn:** Métricas y validación
- **Pandas/NumPy:** Procesamiento de datos
- **Binance API:** Datos históricos
- **Celery (opcional):** Background tasks

### Funcionales:
- **RF-ML-003:** Indicadores técnicos (features)
- Requiere infraestructura de almacenamiento para modelos

---

## Notas Técnicas

### Pipeline Completo

```python
# apps/ml-services/src/models/training_pipeline.py

class TrainingPipeline:
    """
    Pipeline completo de entrenamiento
    """

    async def train(self, symbol: str, samples: int = 500) -> dict:
        # 1. Descargar datos
        logger.info(f"Downloading {samples} candles for {symbol}")
        ohlcv = await market_data.fetch_ohlcv(symbol, limit=samples)

        # 2. Calcular features
        logger.info("Calculating technical indicators")
        features = TechnicalIndicators.calculate_all(ohlcv)

        # 3. Generar targets para cada horizonte
        logger.info("Generating targets")
        targets = self._generate_targets(ohlcv)

        # 4. Entrenar modelos para cada horizonte
        results = {}
        for horizon in ['scalping', 'intraday', 'swing', 'position']:
            logger.info(f"Training {horizon} models")

            # Dividir datos
            X_train, X_test, y_train, y_test = self._split_data(
                features, targets[horizon]
            )

            # Entrenar
            model_high = XGBRegressor(**xgb_params)
            model_low = XGBRegressor(**xgb_params)

            model_high.fit(X_train, y_train['high'])
            model_low.fit(X_train, y_train['low'])

            # Validar
            metrics = self._calculate_metrics(
                model_high, model_low, X_test, y_test
            )

            # Guardar
            self._save_models(symbol, horizon, model_high, model_low, metrics)

            results[horizon] = metrics

        return results
```

### Performance:
- Entrenamiento completo (4 horizontes): ~2-5 minutos con 500 samples
- Download de datos: ~10 segundos
- Feature engineering: ~5 segundos
- Entrenamiento XGBoost: ~30 segundos por horizonte

---

## Referencias

- [XGBoost Training Guide](https://xgboost.readthedocs.io/en/latest/tutorials/model.html)
- [Scikit-learn Cross Validation](https://scikit-learn.org/stable/modules/cross_validation.html)
- [Time Series Split](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html)

---

**Creado por:** Requirements-Analyst
**Fecha:** 2025-12-05
**Última actualización:** 2025-12-05
