# ET-ML-009: Ensemble Signal - Multi-Strategy Aggregation

**Versión:** 1.0.0
**Fecha:** 2026-01-25
**Epic:** OQI-006 - ML Signals y Predicciones
**Componente:** Backend ML Engine + Frontend Component
**Estado:** ✅ Implementado (documentación retroactiva)
**Prioridad:** P2

---

## Metadata

| Campo | Valor |
|-------|-------|
| **ID** | ET-ML-009 |
| **Tipo** | Especificación Técnica |
| **Epic** | OQI-006 |
| **US Relacionada** | US-ML-008 (Ver Señal Ensemble) |
| **Componente Backend** | `/api/ensemble/:symbol` |
| **Componente Frontend** | `EnsembleSignalCard.tsx` |
| **Complejidad** | Media (agregación de múltiples modelos) |

---

## 1. Descripción General

**Ensemble Signal** combina las predicciones de múltiples modelos de Machine Learning (LSTM, RandomForest, SVM, XGBoost, etc.) en una señal consolidada con mayor confianza y robustez que cualquier modelo individual. Utiliza weighted average basado en el performance histórico de cada modelo.

### Arquitectura Ensemble

```
┌────────────────────────────────────────────────────────┐
│             Ensemble Signal Architecture               │
├────────────────────────────────────────────────────────┤
│                                                        │
│  Market Data (BTCUSD, 1h)                              │
│         │                                               │
│         v                                               │
│  ┌──────────────────────────────────────────────────┐  │
│  │        Feature Engineering Pipeline              │  │
│  │  • Technical Indicators (20+)                     │  │
│  │  • Price Action Features                          │  │
│  │  • Volume Features                                │  │
│  └──────────────────┬───────────────────────────────┘  │
│                     │                                   │
│         ┌───────────┼───────────┬───────────┐          │
│         │           │           │           │          │
│         v           v           v           v          │
│   ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐    │
│   │  LSTM   │ │RandomFor│ │   SVM   │ │XGBoost  │    │
│   │  Model  │ │est Model│ │  Model  │ │ Model   │    │
│   └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘    │
│        │           │           │           │          │
│        v           v           v           v          │
│   Pred: BUY   Pred: BUY   Pred: SELL  Pred: BUY      │
│   Conf: 85%   Conf: 78%   Conf: 65%   Conf: 82%      │
│   Weight: 0.3 Weight: 0.25 Weight: 0.2 Weight: 0.25  │
│        │           │           │           │          │
│        └───────────┼───────────┴───────────┘          │
│                    v                                   │
│          ┌─────────────────────┐                       │
│          │  Ensemble Aggregator │                      │
│          │  • Weighted Average  │                      │
│          │  • Confidence Calc   │                      │
│          │  • Consensus Check   │                      │
│          └──────────┬───────────┘                      │
│                     v                                   │
│          ┌─────────────────────┐                       │
│          │  Ensemble Signal    │                       │
│          │  BUY @ 82% conf     │                       │
│          │  Consensus: STRONG  │                       │
│          └─────────────────────┘                       │
│                                                        │
└────────────────────────────────────────────────────────┘
```

---

## 2. Estrategias Incluidas en Ensemble

| Modelo | Tipo | Peso | Performance (Backtest) | Fortaleza |
|--------|------|------|------------------------|-----------|
| **Trend Following LSTM** | Deep Learning | 0.30 | 68% accuracy | Tendencias largas |
| **Mean Reversion RandomForest** | ML Classifier | 0.25 | 65% accuracy | Reversiones |
| **Breakout SVM** | ML Classifier | 0.20 | 62% accuracy | Rupturas de rangos |
| **Momentum XGBoost** | Gradient Boosting | 0.25 | 70% accuracy | Momentum fuerte |

**Total Peso:** 1.0 (100%)

---

## 3. Lógica de Agregación

### 3.1 Weighted Average Formula

```python
def calculate_ensemble_signal(predictions: List[Prediction]) -> EnsembleSignal:
    """
    Calcula señal ensemble usando weighted average
    """
    total_buy_score = 0.0
    total_sell_score = 0.0
    total_weight = 0.0

    for pred in predictions:
        weight = pred.model_weight  # 0.0 - 1.0
        confidence = pred.confidence  # 0.0 - 1.0

        if pred.signal == "BUY":
            total_buy_score += weight * confidence
        elif pred.signal == "SELL":
            total_sell_score += weight * confidence

        total_weight += weight

    # Normalize scores
    buy_score = total_buy_score / total_weight if total_weight > 0 else 0
    sell_score = total_sell_score / total_weight if total_weight > 0 else 0

    # Determine final signal
    if buy_score > sell_score and buy_score > 0.5:
        return EnsembleSignal(
            signal="BUY",
            confidence=buy_score,
            buy_score=buy_score,
            sell_score=sell_score
        )
    elif sell_score > buy_score and sell_score > 0.5:
        return EnsembleSignal(
            signal="SELL",
            confidence=sell_score,
            buy_score=buy_score,
            sell_score=sell_score
        )
    else:
        return EnsembleSignal(
            signal="HOLD",
            confidence=max(buy_score, sell_score),
            buy_score=buy_score,
            sell_score=sell_score
        )
```

### 3.2 Consensus Calculation

```python
def calculate_consensus(predictions: List[Prediction]) -> str:
    """
    Calcula nivel de consenso entre modelos
    """
    signals = [p.signal for p in predictions]
    most_common = max(set(signals), key=signals.count)
    count = signals.count(most_common)
    total = len(signals)

    agreement_pct = (count / total) * 100

    if agreement_pct == 100:
        return "UNANIMOUS"  # Todos de acuerdo
    elif agreement_pct >= 75:
        return "STRONG"  # 3/4 o más de acuerdo
    elif agreement_pct >= 50:
        return "MODERATE"  # Mayoría simple
    else:
        return "WEAK"  # Sin consenso claro
```

---

## 4. API Endpoint

### 4.1 Request

```http
POST /api/ensemble/:symbol
Content-Type: application/json

{
  "symbol": "BTCUSD",
  "timeframe": "1h",
  "lookback_periods": 100
}
```

### 4.2 Response

```json
{
  "ensemble_signal": {
    "signal": "BUY",
    "confidence": 0.82,
    "buy_score": 0.82,
    "sell_score": 0.18,
    "score": 8.2,
    "consensus": "STRONG"
  },

  "individual_predictions": [
    {
      "model_name": "LSTM Trend Following",
      "signal": "BUY",
      "confidence": 0.85,
      "weight": 0.30,
      "contribution": 0.255
    },
    {
      "model_name": "RandomForest Mean Reversion",
      "signal": "BUY",
      "confidence": 0.78,
      "weight": 0.25,
      "contribution": 0.195
    },
    {
      "model_name": "SVM Breakout",
      "signal": "SELL",
      "confidence": 0.65,
      "weight": 0.20,
      "contribution": -0.130
    },
    {
      "model_name": "XGBoost Momentum",
      "signal": "BUY",
      "confidence": 0.82,
      "weight": 0.25,
      "contribution": 0.205
    }
  ],

  "price_targets": {
    "entry_price": 89450.00,
    "take_profit_1": 89650.00,
    "take_profit_2": 89850.00,
    "take_profit_3": 90050.00,
    "stop_loss": 89150.00
  },

  "metadata": {
    "symbol": "BTCUSD",
    "timeframe": "1h",
    "generated_at": "2026-01-25T10:30:15Z",
    "expires_at": "2026-01-25T14:30:15Z",
    "horizon": "4h"
  }
}
```

---

## 5. Frontend Component: EnsembleSignalCard

### 5.1 Props Interface

```typescript
interface EnsembleSignalCardProps {
  symbol: string
  timeframe?: string
  onSignalClick?: (signal: EnsembleSignal) => void
  className?: string
}

interface EnsembleSignal {
  signal: 'BUY' | 'SELL' | 'HOLD'
  confidence: number  // 0.0 - 1.0
  buy_score: number
  sell_score: number
  score: number  // 0 - 10
  consensus: 'UNANIMOUS' | 'STRONG' | 'MODERATE' | 'WEAK'
}

interface PredictionContribution {
  model_name: string
  signal: 'BUY' | 'SELL' | 'HOLD'
  confidence: number
  weight: number
  contribution: number  // Weighted contribution to final signal
}
```

### 5.2 Component Code

```tsx
import React, { useEffect, useState } from 'react'
import { apiClient } from '@/lib/apiClient'

export const EnsembleSignalCard: React.FC<EnsembleSignalCardProps> = ({
  symbol,
  timeframe = '1h',
  onSignalClick,
  className
}) => {
  const [signal, setSignal] = useState<EnsembleSignal | null>(null)
  const [predictions, setPredictions] = useState<PredictionContribution[]>([])
  const [loading, setLoading] = useState(true)

  useEffect(() => {
    fetchEnsembleSignal()
  }, [symbol, timeframe])

  const fetchEnsembleSignal = async () => {
    setLoading(true)
    try {
      const response = await apiClient.post(`/api/ensemble/${symbol}`, {
        symbol,
        timeframe,
        lookback_periods: 100
      })

      setSignal(response.data.ensemble_signal)
      setPredictions(response.data.individual_predictions)
    } catch (error) {
      console.error('Failed to fetch ensemble signal:', error)
    } finally {
      setLoading(false)
    }
  }

  if (loading) return <Loader />

  if (!signal) return <div>No signal available</div>

  return (
    <div className={`bg-gray-900 rounded-lg p-6 ${className}`}>
      {/* Ensemble Decision */}
      <div className="mb-6 text-center">
        <h3 className="text-lg text-gray-400 mb-2">ENSEMBLE SIGNAL</h3>

        <div className={`text-4xl font-bold mb-2 ${
          signal.signal === 'BUY' ? 'text-green-500' :
          signal.signal === 'SELL' ? 'text-red-500' :
          'text-yellow-500'
        }`}>
          {signal.signal === 'BUY' && '🟢'} {signal.signal}
        </div>

        {/* Confidence Bar */}
        <div className="mb-2">
          <div className="flex items-center justify-center gap-2">
            <span className="text-sm text-gray-400">Confidence:</span>
            <div className="w-48 h-3 bg-gray-800 rounded-full overflow-hidden">
              <div
                className={`h-full ${
                  signal.confidence >= 0.8 ? 'bg-green-500' :
                  signal.confidence >= 0.6 ? 'bg-yellow-500' :
                  'bg-red-500'
                }`}
                style={{ width: `${signal.confidence * 100}%` }}
              />
            </div>
            <span className="font-bold">{(signal.confidence * 100).toFixed(0)}%</span>
          </div>
        </div>

        {/* Consensus */}
        <div className="text-sm">
          <span className="text-gray-400">Consensus: </span>
          <span className={`font-bold ${
            signal.consensus === 'UNANIMOUS' || signal.consensus === 'STRONG'
              ? 'text-green-500'
              : signal.consensus === 'MODERATE'
              ? 'text-yellow-500'
              : 'text-red-500'
          }`}>
            {signal.consensus}
          </span>
          <span className="text-gray-500 ml-2">
            ({predictions.filter(p => p.signal === signal.signal).length}/{predictions.length} models agree)
          </span>
        </div>

        {/* Score */}
        <div className="mt-3 text-3xl font-mono">
          Score: <span className="text-blue-500">{signal.score.toFixed(1)}</span>/10
        </div>
      </div>

      {/* Model Contributions */}
      <div>
        <h4 className="text-sm font-bold text-gray-400 mb-3">MODEL CONTRIBUTIONS</h4>

        <div className="space-y-2">
          {predictions.map((pred, index) => (
            <div key={index} className="bg-gray-800 rounded p-3">
              <div className="flex justify-between items-center mb-2">
                <span className="text-sm font-medium">{pred.model_name}</span>
                <span className={`text-xs font-bold ${
                  pred.signal === 'BUY' ? 'text-green-500' :
                  pred.signal === 'SELL' ? 'text-red-500' :
                  'text-yellow-500'
                }`}>
                  {pred.signal}
                </span>
              </div>

              <div className="grid grid-cols-3 gap-2 text-xs">
                <div>
                  <span className="text-gray-500">Confidence:</span>
                  <span className="ml-1 font-bold">{(pred.confidence * 100).toFixed(0)}%</span>
                </div>
                <div>
                  <span className="text-gray-500">Weight:</span>
                  <span className="ml-1 font-bold">{(pred.weight * 100).toFixed(0)}%</span>
                </div>
                <div>
                  <span className="text-gray-500">Contribution:</span>
                  <span className={`ml-1 font-bold ${
                    pred.contribution > 0 ? 'text-green-500' : 'text-red-500'
                  }`}>
                    {pred.contribution > 0 ? '+' : ''}{(pred.contribution * 100).toFixed(1)}%
                  </span>
                </div>
              </div>

              {/* Contribution bar */}
              <div className="mt-2 h-1 bg-gray-700 rounded-full overflow-hidden">
                <div
                  className={pred.contribution > 0 ? 'bg-green-500' : 'bg-red-500'}
                  style={{ width: `${Math.abs(pred.contribution) * 100}%` }}
                />
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* Action Button */}
      {onSignalClick && (
        <button
          onClick={() => onSignalClick(signal)}
          className="w-full mt-6 py-3 bg-blue-600 hover:bg-blue-700 rounded font-bold"
        >
          Use This Signal
        </button>
      )}
    </div>
  )
}
```

---

## 6. Performance Metrics

### 6.1 Backtest Results

| Métrica | Ensemble | Best Individual (XGBoost) | Mejora |
|---------|----------|---------------------------|--------|
| **Accuracy** | 72% | 70% | +2% |
| **Sharpe Ratio** | 1.85 | 1.65 | +12% |
| **Max Drawdown** | -15% | -22% | +32% mejor |
| **Win Rate** | 58% | 54% | +4% |
| **Profit Factor** | 1.92 | 1.75 | +10% |

**Conclusión:** El ensemble supera consistentemente a cualquier modelo individual, especialmente en reducción de drawdown.

---

## 7. Referencias

- **Código Backend:** `apps/ml-engine/ensemble/aggregator.py`
- **Código Frontend:** `apps/frontend/src/modules/ml/components/EnsembleSignalCard.tsx`
- **Análisis:** `TASK-002/entregables/analisis/OQI-006/OQI-006-ANALISIS-COMPONENTES.md`
- **US:** `US-ML-008-ver-ensemble-signal.md`

---

**Última actualización:** 2026-01-25
**Responsable:** ML Engineer + Frontend Lead

