Saltar a contenido

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.md y 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_MODE se 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.py tení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#

  1. Especialización: Cada estrategia en el contexto adecuado
  2. Diversificación: Reducción de riesgo correlacionado
  3. Testing seguro: Shadow mode por símbolo
  4. 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)#

  1. ✅ Documento de arquitectura (este archivo)
  2. ⏳ Refactorizar estrategias a clases OOP
  3. ⏳ Unit tests para cada estrategia
  4. ⏳ Tests de integración para config resolution

Fase 2: Backend Core (2-3 días)#

  1. ⏳ Modificar _resolve_effective_config() para STRATEGY y MODE por símbolo
  2. ⏳ Modificar run_cycle() para instanciar estrategias dinámicamente
  3. ⏳ Actualizar autopilot_decisions schema
  4. ⏳ Migración de configs existentes

Fase 3: API y Frontend (2-3 días)#

  1. ⏳ Endpoint /api/config/strategies - CRUD estrategias por símbolo
  2. ⏳ UI para configurar estrategia por símbolo
  3. ⏳ Dashboard mostrando estrategia activa por símbolo
  4. ⏳ Gráficos comparativos por estrategia

Fase 4: Testing en Shadow (1 semana)#

  1. ⏳ Configurar EURUSDT → ma_crossover (shadow)
  2. ⏳ Configurar PAXGUSDT → range_breakout (shadow)
  3. ⏳ Recolectar métricas 7 días
  4. ⏳ Comparar performance vs backtests

Fase 5: Producción (1 día)#

  1. ⏳ Deploy con feature flag
  2. ⏳ Migrar símbolos uno por uno a multi-strategy
  3. ⏳ Monitoreo intensivo 48h
  4. ⏳ Documentación y handoff

Estimación total: ~2 semanas


6. RETROCOMPATIBILIDAD#

Estrategia de Migración#

  1. 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"
  1. Modo global fallback:
# Si no existe CFG_MANUAL::{SYMBOL}::AUTOPILOT_MODE
# usa valor global de AUTOPILOT_MODE del env
  1. Sin cambios en DB inmediatamente:
  2. Código nuevo lee columnas viejas
  3. Columnas nuevas (strategy_name, mode) son nullable
  4. 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#

  1. ¿Permitir múltiples estrategias en el mismo símbolo?
  2. Propuesta inicial: NO (1 estrategia activa por símbolo)
  3. Futuro: Considerar ensemble strategies

  4. ¿Timeframe por símbolo o global?

  5. Actual: Global (M15)
  6. Propuesta: Empezar global, permitir override futuro

  7. ¿Auto-selection de estrategias?

  8. Propuesta: ML model entrena y selecciona mejor estrategia por símbolo
  9. Timeframe: Fase 2 (post multi-strategy manual)

  10. ¿Portfolio rebalancing automático?

  11. Si una estrategia falla, ¿auto-switch a otra?
  12. Requiere monitoring y alertas robustas

10. SIGUIENTES PASOS INMEDIATOS#

Para aprobación del usuario:

  1. ✅ Opción 1 implementada (PAXGUSDT only)
  2. Revisar este documento de arquitectura
  3. Aprobar plan de implementación
  4. Definir prioridad vs otras mejoras

Post-aprobación:

  1. Crear branch feature/multi-strategy
  2. Implementar Fase 1 (refactoring estrategias)
  3. PR para revisión de código
  4. Comenzar Fase 2

11. Criterio de activación de este RFC#

Este RFC debe activarse cuando se cumpla al menos uno de estos casos:

  1. Se habilite nuevamente EURUSDT en producción.
  2. Se requiera más de una estrategia activa simultánea por universo de símbolos.
  3. 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.py outputs 2025-03-09
  • Configuración actual: runtime_settings table en CT107 DB
  • Código autopilot: backend/services/autopilot.py líneas 400-700, 1095-1200
  • Estrategias implementadas: backend/services/signals.py