# 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 = ({ symbol, timeframe = '1h', onSignalClick, className }) => { const [signal, setSignal] = useState(null) const [predictions, setPredictions] = useState([]) 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 if (!signal) return
No signal available
return (
{/* Ensemble Decision */}

ENSEMBLE SIGNAL

{signal.signal === 'BUY' && '🟢'} {signal.signal}
{/* Confidence Bar */}
Confidence:
= 0.8 ? 'bg-green-500' : signal.confidence >= 0.6 ? 'bg-yellow-500' : 'bg-red-500' }`} style={{ width: `${signal.confidence * 100}%` }} />
{(signal.confidence * 100).toFixed(0)}%
{/* Consensus */}
Consensus: {signal.consensus} ({predictions.filter(p => p.signal === signal.signal).length}/{predictions.length} models agree)
{/* Score */}
Score: {signal.score.toFixed(1)}/10
{/* Model Contributions */}

MODEL CONTRIBUTIONS

{predictions.map((pred, index) => (
{pred.model_name} {pred.signal}
Confidence: {(pred.confidence * 100).toFixed(0)}%
Weight: {(pred.weight * 100).toFixed(0)}%
Contribution: 0 ? 'text-green-500' : 'text-red-500' }`}> {pred.contribution > 0 ? '+' : ''}{(pred.contribution * 100).toFixed(1)}%
{/* Contribution bar */}
0 ? 'bg-green-500' : 'bg-red-500'} style={{ width: `${Math.abs(pred.contribution) * 100}%` }} />
))}
{/* Action Button */} {onSignalClick && ( )}
) } ``` --- ## 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