Arquitectura Multi-Estrategia para SignalDashPro#
Fecha: 2025-03-09
Estado: RFC / Propuesta de diseño
Prioridad: Media (post-estabilización)
Vigencia: este documento es una propuesta de arquitectura, no un runbook operativo.
Para estado real de producción usar:
HANDOFF.md,README.mdy endpoints runtime.
1. ESTADO ACTUAL#
Nota de contexto (2026-03-15): varias premisas históricas de esta sección han cambiado en runtime.
Actualmente la estrategia activa principal está en paxg_mean_reversion con M15 para PAXGUSDT.
Mantener este RFC como diseño objetivo y no como reflejo exacto del estado vigente.
Limitaciones históricas detectadas (momento del RFC)#
- Modo global:
AUTOPILOT_MODEse aplicaba a todos los símbolos - Estrategia única: en ese momento predominaba
range_breakout - Configuración por símbolo: Soportada SOLO para parámetros, no para estrategia/modo
- Código:
autopilot.pytenía resolución parcial de config por símbolo
Lo Que Funciona Bien#
✅ Sistema de configuración jerárquico (env/manual/auto)
✅ Runtime settings con overrides por símbolo
✅ Múltiples estrategias ya implementadas en backend/services/
✅ Filters por símbolo (spread, volume, hours)
2. DESCUBRIMIENTOS CRÍTICOS DEL BACKTEST#
Incompatibilidad Estrategia-Símbolo#
range_breakout sobre EURUSDT (alta volatilidad):
- Win rate: 4.35% (catastrófico)
- 69 trades en 90 días
- PnL: -$3.12
- Conclusión: Estrategia incompatible con par volátil
range_breakout sobre PAXGUSDT (baja volatilidad):
- Win rate: 39.53% (excelente)
- 129 trades en 90 días
- PnL: -$3.49 (pero avg_win/avg_loss = 0.94:1, mejorable con TP 2.0x)
- Conclusión: Estrategia adecuada para pares estables
Implicación#
Diferentes símbolos necesitan diferentes estrategias, no solo diferentes parámetros.
3. VISION ARQUITECTURA MULTI-ESTRATEGIA#
Objetivo#
Permitir que cada símbolo ejecute una estrategia diferente con modo diferente (live/shadow/disabled):
EURUSDT → ma_crossover (LIVE) # Mejor para tendencias fuertes
PAXGUSDT → range_breakout (LIVE) # Mejor para rangos
BTCUSDT → range_breakout (SHADOW) # Probando sin riesgo
ETHUSDT → none (DISABLED)
Beneficios#
- Especialización: Cada estrategia en el contexto adecuado
- Diversificación: Reducción de riesgo correlacionado
- Testing seguro: Shadow mode por símbolo
- Escalabilidad: Agregar estrategias sin afectar existentes
4. DISEÑO PROPUESTO#
4.1 Configuración Runtime Settings#
Claves nuevas:
# Configuración por símbolo
CFG_MANUAL::EURUSDT::AUTOPILOT_STRATEGY = "ma_crossover"
CFG_MANUAL::EURUSDT::AUTOPILOT_MODE = "live"
CFG_MANUAL::PAXGUSDT::AUTOPILOT_STRATEGY = "range_breakout"
CFG_MANUAL::PAXGUSDT::AUTOPILOT_MODE = "live"
CFG_MANUAL::BTCUSDT::AUTOPILOT_STRATEGY = "range_breakout"
CFG_MANUAL::BTCUSDT::AUTOPILOT_MODE = "shadow"
# Modo selector (nueva sintaxis)
CFG_MODE::EURUSDT::AUTOPILOT_STRATEGY = "manual" # env|manual|auto
CFG_MODE::EURUSDT::AUTOPILOT_MODE = "manual"
# Fallback global (retrocompatibilidad)
CFG_MANUAL::AUTOPILOT_DEFAULT_STRATEGY = "range_breakout"
CFG_MANUAL::AUTOPILOT_DEFAULT_MODE = "shadow"
4.2 Modificaciones en autopilot.py#
Cambios en _resolve_effective_config() (líneas ~1095-1200):
def _resolve_effective_config(self, symbol: str) -> dict:
"""
Resuelve configuración efectiva para UN símbolo específico.
Ahora soporta STRATEGY y MODE por símbolo.
"""
config = {}
# Lista de parámetros configurables POR SÍMBOLO
symbol_scoped_params = [
"AUTOPILOT_STRATEGY", # NUEVO
"AUTOPILOT_MODE", # NUEVO (antes era global)
"AUTOPILOT_LOOKBACK_BARS",
"AUTOPILOT_ENTRY_ATR_FRACTION",
"AUTOPILOT_ATR_MULT",
"AUTOPILOT_TAKE_PROFIT_MULT",
"AUTOPILOT_MIN_VOL_REL",
"AUTOPILOT_MAX_SPREAD_PCT",
"AUTOPILOT_ALLOW_HOURS_UTC",
]
# Parámetros globales (no por símbolo)
global_params = [
"AUTOPILOT_SYMBOLS", # Mantiene como global
"AUTOPILOT_TIMEFRAME", # Global por ahora
"AUTOPILOT_RISK_PCT",
"AUTOPILOT_MAX_POSITIONS",
]
# Resolver parámetros por símbolo
for param in symbol_scoped_params:
mode_key = f"CFG_MODE::{symbol}::{param}"
mode = self._get_runtime_setting(mode_key) or "env"
if mode == "manual":
manual_key = f"CFG_MANUAL::{symbol}::{param}"
value = self._get_runtime_setting(manual_key)
elif mode == "auto":
auto_key = f"CFG_AUTO::{symbol}::{param}"
value = self._get_runtime_setting(auto_key)
else: # env
value = getattr(self.config, param, None)
# Fallback a default global si no existe para símbolo
if value is None:
default_key = f"CFG_MANUAL::AUTOPILOT_DEFAULT_{param.replace('AUTOPILOT_', '')}"
value = self._get_runtime_setting(default_key)
if value is None:
value = getattr(self.config, param, None)
config[param] = value
# Resolver parámetros globales
for param in global_params:
mode_key = f"CFG_MODE::{param}"
mode = self._get_runtime_setting(mode_key) or "env"
if mode == "manual":
manual_key = f"CFG_MANUAL::{param}"
value = self._get_runtime_setting(manual_key)
elif mode == "auto":
auto_key = f"CFG_AUTO::{param}"
value = self._get_runtime_setting(auto_key)
else:
value = getattr(self.config, param, None)
config[param] = value
return config
Cambios en run_cycle() (líneas ~400-500):
async def run_cycle(self):
"""
Ciclo principal del autopilot.
Ahora procesa cada símbolo con su estrategia y modo específicos.
"""
symbols = self._resolve_symbols()
for symbol in symbols:
try:
# Resolver configuración específica para este símbolo
cfg = self._resolve_effective_config(symbol)
# Obtener estrategia y modo para este símbolo
strategy_name = cfg.get("AUTOPILOT_STRATEGY", "range_breakout")
mode = cfg.get("AUTOPILOT_MODE", "shadow")
# Saltar si está deshabilitado
if mode == "disabled":
logger.info(f"{symbol}: Skipped (mode=disabled)")
continue
# Instanciar estrategia apropiada
if strategy_name == "range_breakout":
strategy = RangeBreakoutStrategy(cfg)
elif strategy_name == "ma_crossover":
strategy = MACrossoverStrategy(cfg)
else:
logger.error(f"{symbol}: Unknown strategy '{strategy_name}'")
continue
# Generar señal usando la estrategia
signal = await strategy.generate_signal(symbol)
# Ejecutar según modo
if signal:
if mode == "live":
await self._execute_live_signal(symbol, signal, cfg)
elif mode == "shadow":
await self._execute_shadow_signal(symbol, signal, cfg)
# Registrar decisión en autopilot_decisions
await self._log_decision(symbol, signal, strategy_name, mode, cfg)
except Exception as e:
logger.error(f"{symbol}: Error in strategy execution: {e}")
await self._log_error(symbol, str(e))
4.3 Estrategias como Clases#
Refactorizar estrategias a interfaz común:
# backend/services/strategies/base.py
from abc import ABC, abstractmethod
class TradingStrategy(ABC):
def __init__(self, config: dict):
self.config = config
@abstractmethod
async def generate_signal(self, symbol: str) -> dict | None:
"""
Genera señal de trading para el símbolo.
Returns:
dict con keys: action, price, reason, params
None si no hay señal
"""
pass
@abstractmethod
def name(self) -> str:
"""Nombre de la estrategia"""
pass
# backend/services/strategies/range_breakout.py
class RangeBreakoutStrategy(TradingStrategy):
def name(self) -> str:
return "range_breakout"
async def generate_signal(self, symbol: str) -> dict | None:
# Lógica actual de range_breakout_signal()
# ... código existente ...
pass
# backend/services/strategies/ma_crossover.py
class MACrossoverStrategy(TradingStrategy):
def name(self) -> str:
return "ma_crossover"
async def generate_signal(self, symbol: str) -> dict | None:
# Lógica actual de ma_crossover_signal()
# ... código existente ...
pass
4.4 Registro en Base de Datos#
Añadir columnas a autopilot_decisions:
ALTER TABLE autopilot_decisions
ADD COLUMN strategy_name VARCHAR(50),
ADD COLUMN mode VARCHAR(20);
5. PLAN DE IMPLEMENTACIÓN#
Fase 1: Diseño y Testing (1-2 días)#
- ✅ Documento de arquitectura (este archivo)
- ⏳ Refactorizar estrategias a clases OOP
- ⏳ Unit tests para cada estrategia
- ⏳ Tests de integración para config resolution
Fase 2: Backend Core (2-3 días)#
- ⏳ Modificar
_resolve_effective_config()para STRATEGY y MODE por símbolo - ⏳ Modificar
run_cycle()para instanciar estrategias dinámicamente - ⏳ Actualizar
autopilot_decisionsschema - ⏳ Migración de configs existentes
Fase 3: API y Frontend (2-3 días)#
- ⏳ Endpoint
/api/config/strategies- CRUD estrategias por símbolo - ⏳ UI para configurar estrategia por símbolo
- ⏳ Dashboard mostrando estrategia activa por símbolo
- ⏳ Gráficos comparativos por estrategia
Fase 4: Testing en Shadow (1 semana)#
- ⏳ Configurar EURUSDT → ma_crossover (shadow)
- ⏳ Configurar PAXGUSDT → range_breakout (shadow)
- ⏳ Recolectar métricas 7 días
- ⏳ Comparar performance vs backtests
Fase 5: Producción (1 día)#
- ⏳ Deploy con feature flag
- ⏳ Migrar símbolos uno por uno a multi-strategy
- ⏳ Monitoreo intensivo 48h
- ⏳ Documentación y handoff
Estimación total: ~2 semanas
6. RETROCOMPATIBILIDAD#
Estrategia de Migración#
- Configuración antigua sigue funcionando:
# Si no existe CFG_MANUAL::{SYMBOL}::AUTOPILOT_STRATEGY
# usa CFG_MANUAL::AUTOPILOT_DEFAULT_STRATEGY
# que por defecto sería "range_breakout"
- Modo global fallback:
# Si no existe CFG_MANUAL::{SYMBOL}::AUTOPILOT_MODE
# usa valor global de AUTOPILOT_MODE del env
- Sin cambios en DB inmediatamente:
- Código nuevo lee columnas viejas
- Columnas nuevas (strategy_name, mode) son nullable
- Población gradual durante 1-2 semanas
7. RIESGOS Y MITIGACIONES#
| Riesgo | Impacto | Probabilidad | Mitigación |
|---|---|---|---|
| Bug en resolución de config causa trading incorrecto | Alto | Media | Testing exhaustivo + shadow mode 1 semana |
| Performance degradation (más queries a DB) | Medio | Baja | Cache de runtime_settings en memoria |
| Estrategias ejecutan trades conflictivos en mismo símbolo | Alto | Muy Baja | Validación: 1 estrategia por símbolo |
| Migración rompe config existente | Alto | Media | Rollback plan + backup DB antes de deploy |
8. MÉTRICAS DE ÉXITO#
Pre-Implementación (baseline actual)#
- PAXGUSDT range_breakout: 39.5% win rate (shadow)
- EURUSDT range_breakout: 4% win rate (disabled)
Post-Implementación (objetivo 30 días)#
- PAXGUSDT range_breakout: ≥35% win rate (live)
- EURUSDT ma_crossover: ≥45% win rate (live)
- PnL mensual combinado: >0 (break-even o positivo)
- Reducción drawdown: ≥20% vs estrategia única
9. PREGUNTAS ABIERTAS#
- ¿Permitir múltiples estrategias en el mismo símbolo?
- Propuesta inicial: NO (1 estrategia activa por símbolo)
-
Futuro: Considerar ensemble strategies
-
¿Timeframe por símbolo o global?
- Actual: Global (M15)
-
Propuesta: Empezar global, permitir override futuro
-
¿Auto-selection de estrategias?
- Propuesta: ML model entrena y selecciona mejor estrategia por símbolo
-
Timeframe: Fase 2 (post multi-strategy manual)
-
¿Portfolio rebalancing automático?
- Si una estrategia falla, ¿auto-switch a otra?
- Requiere monitoring y alertas robustas
10. SIGUIENTES PASOS INMEDIATOS#
Para aprobación del usuario:
- ✅ Opción 1 implementada (PAXGUSDT only)
- Revisar este documento de arquitectura
- Aprobar plan de implementación
- Definir prioridad vs otras mejoras
Post-aprobación:
- Crear branch
feature/multi-strategy - Implementar Fase 1 (refactoring estrategias)
- PR para revisión de código
- Comenzar Fase 2
11. Criterio de activación de este RFC#
Este RFC debe activarse cuando se cumpla al menos uno de estos casos:
- Se habilite nuevamente EURUSDT en producción.
- Se requiera más de una estrategia activa simultánea por universo de símbolos.
- La performance de estrategia única en PAXGUSDT se mantenga débil por múltiples ventanas de evaluación.
Antes de activar implementación, abrir CaseReview técnico y definir:
- alcance de migración en DB,
- estrategia de rollback,
- plan de shadow testing por símbolo.
REFERENCIAS#
- Backtest results:
scripts/backtest_range_breakout.pyoutputs 2025-03-09 - Configuración actual:
runtime_settingstable en CT107 DB - Código autopilot:
backend/services/autopilot.pylíneas 400-700, 1095-1200 - Estrategias implementadas:
backend/services/signals.py