--- title: "Especificación Técnica - Refactoring Mínimo Viable" version: "1.0.0" date: "2026-01-06" status: "Proposed" author: "ML-Specialist + Orquestador" epic: "OQI-006" tags: ["refactoring", "integration", "ml", "attention"] --- # REFACTORING MÍNIMO VIABLE - ML PREDICTION SYSTEM ## 1. RESUMEN EJECUTIVO Este documento define el plan de refactoring de bajo riesgo para integrar la infraestructura ML existente y lograr mejoras inmediatas en win rate y R:R ratio. **Objetivo**: Pasar de 33-44% win rate a 60%+ con cambios mínimos. **Principio**: NO reescribir, INTEGRAR código existente. --- ## 2. CAMBIOS PROPUESTOS ### 2.1 CAMBIO 1: Cargar Modelos Entrenados (Prioridad ALTA) **Archivo**: `src/services/prediction_service.py` **Antes (línea ~157)**: ```python from ..models.range_predictor import RangePredictor self._range_predictor = RangePredictor() # Modelo genérico ``` **Después**: ```python from ..training.symbol_timeframe_trainer import SymbolTimeframeTrainer class PredictionService: def __init__(self): self._trainers: Dict[str, SymbolTimeframeTrainer] = {} self._load_trained_models() def _load_trained_models(self): """Cargar modelos entrenados por símbolo/timeframe""" models_path = Path(__file__).parent.parent.parent / 'models' / 'ml_first' for symbol_dir in models_path.iterdir(): if symbol_dir.is_dir(): symbol = symbol_dir.name trainer = SymbolTimeframeTrainer() trainer.load(str(symbol_dir)) self._trainers[symbol] = trainer logger.info(f"✅ Loaded models for {symbol}") def predict_range(self, symbol: str, timeframe: str, df: pd.DataFrame): """Usar modelo específico por símbolo""" if symbol in self._trainers: return self._trainers[symbol].predict(df, symbol, timeframe) else: # Fallback a modelo legacy return self._range_predictor.predict(df) ``` **Impacto Estimado**: +5-10% precisión **Riesgo**: Bajo (fallback a legacy) **Esfuerzo**: 2 horas --- ### 2.2 CAMBIO 2: Eliminar Factores Hardcodeados (Prioridad ALTA) **Archivo**: `src/models/range_predictor_factor.py` **Antes (línea 598-601)**: ```python class PriceDataGenerator: SYMBOLS = { 'XAUUSD': {'base': 2650.0, 'volatility': 0.0012, 'factor': 2.5}, 'EURUSD': {'base': 1.0420, 'volatility': 0.0004, 'factor': 0.0003}, } ``` **Después**: ```python from ..training.symbol_timeframe_trainer import SYMBOL_CONFIGS class PriceDataGenerator: def __init__(self, symbol: str, seed: int = 42): self.symbol = symbol # Usar configuración centralizada config = SYMBOL_CONFIGS.get(symbol) if config: self.config = { 'base': 2650.0 if symbol == 'XAUUSD' else 1.0, # Precio actual dinámico 'volatility': config.base_factor / 1000, # Normalizar 'factor': config.base_factor } else: # Default para símbolos nuevos self.config = self._compute_dynamic_config(symbol) ``` **Impacto**: Soporte para 5+ símbolos (vs 2) **Riesgo**: Bajo **Esfuerzo**: 1 hora --- ### 2.3 CAMBIO 3: Activar Filtros Direccionales (Prioridad ALTA) **Archivo**: `src/models/signal_generator.py` **Agregar filtros basados en backtests exitosos**: ```python class DirectionalFilters: """Filtros direccionales validados en backtests""" @staticmethod def is_short_valid(indicators: Dict, symbol: str) -> Tuple[bool, int]: """ Validar señal SHORT (2+ confirmaciones) Returns: (is_valid, confirmation_count) """ confirmations = 0 # Filtros que funcionaron en XAUUSD if indicators.get('rsi', 50) > 55: confirmations += 1 if indicators.get('sar_above_price', False): confirmations += 1 if indicators.get('cmf', 0) < 0: confirmations += 1 if indicators.get('mfi', 50) > 55: confirmations += 1 return confirmations >= 2, confirmations @staticmethod def is_long_valid(indicators: Dict, symbol: str) -> Tuple[bool, int]: """ Validar señal LONG (3+ confirmaciones, más estricto) """ confirmations = 0 if indicators.get('rsi', 50) < 35: confirmations += 1 if not indicators.get('sar_above_price', True): confirmations += 1 if indicators.get('cmf', 0) > 0.1: confirmations += 1 if indicators.get('mfi', 50) < 35: confirmations += 1 return confirmations >= 3, confirmations # En SignalGenerator.generate() def generate(self, df: pd.DataFrame, symbol: str, ...): # ... código existente ... # Aplicar filtros direccionales indicators = self._compute_indicators(df) if direction == Direction.SHORT: is_valid, conf_count = DirectionalFilters.is_short_valid(indicators, symbol) if not is_valid: return self._neutral_signal() confidence *= (1 + 0.1 * conf_count) # Boost por confirmaciones elif direction == Direction.LONG: is_valid, conf_count = DirectionalFilters.is_long_valid(indicators, symbol) if not is_valid: return self._neutral_signal() confidence *= (1 + 0.1 * conf_count) # ... resto del código ... ``` **Impacto**: +10-15% win rate (demostrado en backtests) **Riesgo**: Bajo (solo filtra, no cambia lógica) **Esfuerzo**: 3 horas --- ### 2.4 CAMBIO 4: Integrar DynamicFactorWeighter (Prioridad MEDIA) **Archivo**: `src/models/enhanced_range_predictor.py` y otros **Agregar attention weighting a XGBoost**: ```python from ..training.dynamic_factor_weighting import DynamicFactorWeighter, DynamicFactorConfig class AttentionWeightedPredictor: """Wrapper para agregar attention a cualquier modelo""" def __init__(self, base_model, config: DynamicFactorConfig = None): self.base_model = base_model self.weighter = DynamicFactorWeighter(config or DynamicFactorConfig()) def fit(self, X: np.ndarray, y: np.ndarray, df: pd.DataFrame): """Entrenar con sample weights de atención""" # Calcular pesos de atención weights = self.weighter.compute_weights(df) # Entrenar modelo base con pesos self.base_model.fit(X, y, sample_weight=weights) return self def predict(self, X: np.ndarray, df: pd.DataFrame = None): """Predecir (opcional: devolver attention weight)""" predictions = self.base_model.predict(X) if df is not None: weights = self.weighter.compute_weights(df, normalize=False) return predictions, weights return predictions ``` **Impacto**: +5-10% enfoque en movimientos significativos **Riesgo**: Medio (requiere reentrenamiento) **Esfuerzo**: 4 horas --- ## 3. ORDEN DE IMPLEMENTACIÓN ``` Semana 1: ├── Día 1-2: CAMBIO 3 - Filtros direccionales │ └── Test: Backtesting XAUUSD con filtros ├── Día 3-4: CAMBIO 1 - Cargar modelos entrenados │ └── Test: Verificar predicciones por símbolo └── Día 5: CAMBIO 2 - Eliminar hardcoding └── Test: Agregar BTCUSD como nuevo símbolo Semana 2: ├── Día 1-3: CAMBIO 4 - Integrar DynamicFactorWeighter │ └── Test: Reentrenar modelo XAUUSD 5m con attention └── Día 4-5: Backtesting completo + ajustes ``` --- ## 4. TESTS DE REGRESIÓN ### 4.1 Tests Unitarios ```python # tests/test_prediction_integration.py def test_symbol_specific_model_loaded(): """Verificar que se cargan modelos por símbolo""" service = PredictionService() assert 'XAUUSD' in service._trainers assert service._trainers['XAUUSD'] is not None def test_directional_filters_short(): """Verificar filtros SHORT""" indicators = {'rsi': 60, 'sar_above_price': True, 'cmf': -0.1, 'mfi': 60} is_valid, count = DirectionalFilters.is_short_valid(indicators, 'XAUUSD') assert is_valid == True assert count >= 2 def test_directional_filters_long(): """Verificar filtros LONG (más estrictos)""" indicators = {'rsi': 30, 'sar_above_price': False, 'cmf': 0.15, 'mfi': 30} is_valid, count = DirectionalFilters.is_long_valid(indicators, 'XAUUSD') assert is_valid == True assert count >= 3 def test_attention_weights_computation(): """Verificar cálculo de attention weights""" df = create_sample_ohlcv(n=500) weighter = DynamicFactorWeighter() weights = weighter.compute_weights(df) assert len(weights) == len(df) assert weights.mean() > 0 assert weights.max() <= 3.0 # w_max def test_fallback_to_legacy(): """Verificar fallback para símbolos no entrenados""" service = PredictionService() df = create_sample_ohlcv() # Símbolo no entrenado result = service.predict_range('UNKNOWN', '5m', df) assert result is not None # Fallback funciona ``` ### 4.2 Tests de Integración ```python # tests/test_backtesting_regression.py def test_xauusd_5m_win_rate(): """Verificar win rate no disminuye""" results = run_backtest('XAUUSD', '5m', period='2025-01') # Baseline: 44% con filtros actuales assert results['win_rate'] >= 0.40 def test_xauusd_5m_profit_factor(): """Verificar profit factor""" results = run_backtest('XAUUSD', '5m', period='2025-01') # Baseline: 1.07 assert results['profit_factor'] >= 1.0 def test_attention_improves_signal_quality(): """Verificar que attention mejora selección""" # Sin attention signals_no_attn = generate_signals(use_attention=False) # Con attention signals_with_attn = generate_signals(use_attention=True) # Debe haber menos señales pero mejor calidad assert len(signals_with_attn) <= len(signals_no_attn) assert signals_with_attn.mean_confidence >= signals_no_attn.mean_confidence ``` ### 4.3 Tests de Performance ```python # tests/test_performance.py def test_prediction_latency(): """Verificar latencia de predicción < 100ms""" service = PredictionService() df = create_sample_ohlcv(n=500) start = time.time() for _ in range(100): service.predict_range('XAUUSD', '5m', df) elapsed = (time.time() - start) / 100 assert elapsed < 0.1 # < 100ms por predicción def test_model_loading_time(): """Verificar tiempo de carga < 5 segundos""" start = time.time() service = PredictionService() elapsed = time.time() - start assert elapsed < 5.0 ``` --- ## 5. CRITERIOS DE ÉXITO | Métrica | Baseline | Post-Refactoring | Meta Final | |---------|----------|------------------|------------| | Win Rate | 33-44% | ≥ 55% | 80% | | Profit Factor | 1.07 | ≥ 1.2 | 1.8 | | R:R Ratio | 1.2:1 | ≥ 1.8:1 | 2.5:1 | | Latencia | 50ms | < 100ms | < 50ms | | Símbolos | 2-3 | 5+ | 100+ | --- ## 6. ROLLBACK PLAN En caso de regresión: 1. **Rollback inmediato**: Revertir a rama `main` anterior 2. **Fallback en código**: Cada cambio tiene fallback a comportamiento legacy 3. **Feature flags**: ```python USE_TRAINED_MODELS = os.getenv('USE_TRAINED_MODELS', 'true') == 'true' USE_DIRECTIONAL_FILTERS = os.getenv('USE_DIRECTIONAL_FILTERS', 'true') == 'true' USE_ATTENTION_WEIGHTING = os.getenv('USE_ATTENTION_WEIGHTING', 'false') == 'true' ``` --- ## 7. MONITOREO POST-DEPLOY ### 7.1 Métricas a Monitorear ```python # Agregar a prediction_service.py from prometheus_client import Counter, Histogram PREDICTIONS_TOTAL = Counter('ml_predictions_total', 'Total predictions', ['symbol', 'direction']) PREDICTION_LATENCY = Histogram('ml_prediction_latency_seconds', 'Prediction latency') SIGNALS_FILTERED = Counter('ml_signals_filtered', 'Signals filtered by direction', ['reason']) ``` ### 7.2 Alertas | Alerta | Condición | Acción | |--------|-----------|--------| | Win Rate Drop | win_rate < 0.40 por 24h | Review filtros | | Latency High | p99 > 200ms | Check model loading | | No Signals | 0 signals en 8h | Check filters/data | --- *Documento generado: 2026-01-06* *Estado: Propuesto para revisión*