"""Pre-built scenario library for synthetic market backtesting. Defines 12 scenarios covering diverse market regimes. Each scenario is a ScenarioConfig that specifies market dynamics, event characteristics, and signal coupling strength. Key scenarios for overfitting detection: no_signal → signal_strength=0.0 → strategy must return ~0 strong_signal → signal_strength=0.6 → strategy must capture alpha Scenario groups for targeted analysis: trend → steady_bull, steady_bear, prolonged_bear volatility → low_vol_grind, high_vol_chop, vix_spike regime → regime_switch, crash_v_recovery signal → no_signal, strong_signal structural → sector_rotation, liquidity_drought quick → steady_bull, steady_bear, no_signal (fast validation) """ from __future__ import annotations from dataclasses import dataclass, field from libs.backtest.scenarios.price_gen import PriceRegime from libs.backtest.scenarios.macro_gen import VIXConfig, HYSpreadConfig from libs.backtest.scenarios.event_gen import EventDistribution @dataclass class ScenarioConfig: """Complete specification of a synthetic market scenario.""" name: str description: str # Market dynamics price_regimes: list[PriceRegime] """Ordered sequence of market regime segments.""" vix_config: VIXConfig = field(default_factory=VIXConfig) hy_config: HYSpreadConfig = field(default_factory=HYSpreadConfig) # Event characteristics event_distribution: EventDistribution = field(default_factory=EventDistribution) # Signal coupling signal_strength: float = 0.35 """0.0=pure noise, ~0.35=realistic SNR, 0.6=strong alpha.""" signal_decay_days: int = 10 """Days over which post-event signal drift decays.""" false_positive_rate: float = 0.15 """Fraction of qualifying events that produce negative returns (traps).""" # Simulation parameters n_symbols: int = 200 """Number of unique synthetic tickers in the universe.""" seed: int | None = None """Random seed for reproducibility (None = non-deterministic).""" # --------------------------------------------------------------------------- # Helper: bear-market event distribution (weaker signals, more negative reactions) # --------------------------------------------------------------------------- def _bear_event_dist(reaction_return_mean: float = -0.010) -> EventDistribution: return EventDistribution( reaction_return_mean=reaction_return_mean, reaction_return_std=0.065, event_directions={ "bullish": 0.30, "mixed": 0.30, "unknown": 0.25, "bearish": 0.15, }, volume_ratio_mean=2.2, signal_strength_mean=0.58, market_temperature_mean=1.2, volatility_20d_mean=0.38, ) def _stress_event_dist() -> EventDistribution: return EventDistribution( reaction_return_mean=0.000, reaction_return_std=0.080, event_directions={ "bullish": 0.35, "mixed": 0.35, "unknown": 0.20, "bearish": 0.10, }, volume_ratio_mean=2.5, signal_strength_mean=0.60, market_temperature_mean=1.5, volatility_20d_mean=0.42, rsi_14_mean=45.0, bb_position_mean=0.40, ) # --------------------------------------------------------------------------- # Pre-built scenarios # --------------------------------------------------------------------------- STEADY_BULL = ScenarioConfig( name="steady_bull", description="Sustained bull market: +15% drift, 14% vol. Baseline profitable environment.", price_regimes=[ PriceRegime(annualized_drift=0.15, annualized_vol=0.14, duration_days=252), ], vix_config=VIXConfig(base_level=14.0, mean_reversion=5.0, volatility=4.0), hy_config=HYSpreadConfig(base_level=3.5, mean_reversion=3.0, volatility=0.8), signal_strength=0.35, seed=1001, ) STEADY_BEAR = ScenarioConfig( name="steady_bear", description="Sustained bear market: -20% drift, 22% vol. Tests macro regime filter (Gate 0).", price_regimes=[ PriceRegime(annualized_drift=-0.20, annualized_vol=0.22, duration_days=252), ], vix_config=VIXConfig(base_level=28.0, mean_reversion=4.0, volatility=7.0), hy_config=HYSpreadConfig(base_level=7.0, mean_reversion=2.5, volatility=2.0), event_distribution=_bear_event_dist(reaction_return_mean=-0.010), signal_strength=0.35, seed=1002, ) CRASH_V_RECOVERY = ScenarioConfig( name="crash_v_recovery", description=( "V-shaped crash + recovery: 60d normal → 20d crash (-40%/40% vol) → 172d recovery. " "Tests kill switch and drawdown protection." ), price_regimes=[ PriceRegime(annualized_drift=0.10, annualized_vol=0.15, duration_days=60), PriceRegime( annualized_drift=-0.40, annualized_vol=0.40, duration_days=20, jump_prob=0.08, jump_mean=-0.05, jump_std=0.03, ), PriceRegime(annualized_drift=0.25, annualized_vol=0.20, duration_days=172), ], vix_config=VIXConfig(base_level=15.0, mean_reversion=3.0, volatility=8.0), hy_config=HYSpreadConfig(base_level=4.0, mean_reversion=2.0, volatility=2.5), signal_strength=0.35, seed=1003, ) PROLONGED_BEAR = ScenarioConfig( name="prolonged_bear", description=( "2-year bear market: -15% drift, 25% vol over 504 trading days. " "Exceeds duration of any historical bear in training data." ), price_regimes=[ PriceRegime(annualized_drift=-0.15, annualized_vol=0.25, duration_days=504), ], vix_config=VIXConfig(base_level=30.0, mean_reversion=3.5, volatility=8.0), hy_config=HYSpreadConfig(base_level=8.5, mean_reversion=2.0, volatility=2.5), event_distribution=_bear_event_dist(reaction_return_mean=-0.015), signal_strength=0.30, seed=1004, ) LOW_VOL_GRIND = ScenarioConfig( name="low_vol_grind", description=( "Low-volatility grind: +8% drift, 8% vol. " "ATR shrinks → stop distances compress → fewer trades qualify." ), price_regimes=[ PriceRegime(annualized_drift=0.08, annualized_vol=0.08, duration_days=252), ], vix_config=VIXConfig(base_level=11.0, mean_reversion=6.0, volatility=2.5), hy_config=HYSpreadConfig(base_level=2.8, mean_reversion=4.0, volatility=0.5), event_distribution=EventDistribution( volatility_20d_mean=0.15, volatility_20d_std=0.04, reaction_return_std=0.03, ), signal_strength=0.20, seed=1005, ) HIGH_VOL_CHOP = ScenarioConfig( name="high_vol_chop", description=( "High-volatility sideways chop: 0% drift, 30% vol. " "Whipsaws test stop-loss resilience." ), price_regimes=[ PriceRegime(annualized_drift=0.00, annualized_vol=0.30, duration_days=252), ], vix_config=VIXConfig(base_level=32.0, mean_reversion=4.0, volatility=9.0), hy_config=HYSpreadConfig(base_level=6.5, mean_reversion=2.5, volatility=2.0), event_distribution=_stress_event_dist(), signal_strength=0.25, seed=1006, ) REGIME_SWITCH = ScenarioConfig( name="regime_switch", description=( "Rapid regime alternation: 4 × (60d bull / 63d bear) cycles. " "Tests whether macro gate adapts quickly to changing conditions." ), price_regimes=[ PriceRegime(annualized_drift=0.15, annualized_vol=0.16, duration_days=60), PriceRegime(annualized_drift=-0.18, annualized_vol=0.24, duration_days=63), PriceRegime(annualized_drift=0.12, annualized_vol=0.16, duration_days=60), PriceRegime(annualized_drift=-0.15, annualized_vol=0.22, duration_days=69), ], vix_config=VIXConfig(base_level=20.0, mean_reversion=4.0, volatility=8.0), hy_config=HYSpreadConfig(base_level=5.0, mean_reversion=2.5, volatility=1.8), signal_strength=0.30, seed=1007, ) VIX_SPIKE = ScenarioConfig( name="vix_spike", description=( "Normal market with 5 random VIX spike weeks (VIX 40+). " "Tests VIX continuous scaler and kill switch under stress clusters." ), price_regimes=[ PriceRegime(annualized_drift=0.08, annualized_vol=0.16, duration_days=252), ], vix_config=VIXConfig(base_level=18.0, mean_reversion=3.0, volatility=12.0), hy_config=HYSpreadConfig(base_level=4.5, mean_reversion=2.5, volatility=2.0), signal_strength=0.35, seed=1008, ) NO_SIGNAL = ScenarioConfig( name="no_signal", description=( "Pure noise: 0% drift, 16% vol, signal_strength=0.0. " "Market is flat so any positive return = OVERFIT to price patterns, not event alpha." ), price_regimes=[ PriceRegime(annualized_drift=0.00, annualized_vol=0.16, duration_days=252), ], vix_config=VIXConfig(base_level=16.0, mean_reversion=5.0, volatility=4.5), hy_config=HYSpreadConfig(base_level=4.0, mean_reversion=3.0, volatility=1.0), signal_strength=0.00, seed=1009, ) STRONG_SIGNAL = ScenarioConfig( name="strong_signal", description=( "Strong alpha: +10% drift, 16% vol, signal_strength=0.6. " "Strategy must capture meaningful positive returns." ), price_regimes=[ PriceRegime(annualized_drift=0.10, annualized_vol=0.16, duration_days=252), ], vix_config=VIXConfig(base_level=16.0, mean_reversion=5.0, volatility=4.5), hy_config=HYSpreadConfig(base_level=4.0, mean_reversion=3.0, volatility=1.0), signal_strength=0.60, seed=1010, ) SECTOR_ROTATION = ScenarioConfig( name="sector_rotation", description=( "Quarterly sector rotation: Tech underperforms while Healthcare/Financials rally. " "Tests sector concentration limits." ), price_regimes=[ PriceRegime(annualized_drift=0.05, annualized_vol=0.18, duration_days=63), PriceRegime(annualized_drift=0.12, annualized_vol=0.16, duration_days=63), PriceRegime(annualized_drift=-0.05, annualized_vol=0.20, duration_days=63), PriceRegime(annualized_drift=0.08, annualized_vol=0.15, duration_days=63), ], vix_config=VIXConfig(base_level=20.0, mean_reversion=4.5, volatility=5.0), hy_config=HYSpreadConfig(base_level=4.5, mean_reversion=3.0, volatility=1.2), signal_strength=0.35, seed=1011, ) LIQUIDITY_DROUGHT = ScenarioConfig( name="liquidity_drought", description=( "Liquidity drought: +5% drift, 18% vol, stock volumes drop 60%. " "Tests ADV fraction limits and position sizing." ), price_regimes=[ PriceRegime(annualized_drift=0.05, annualized_vol=0.18, duration_days=252), ], vix_config=VIXConfig(base_level=22.0, mean_reversion=4.0, volatility=5.5), hy_config=HYSpreadConfig(base_level=5.0, mean_reversion=2.5, volatility=1.5), event_distribution=EventDistribution( avg_dollar_volume_mean=1_500_000.0, # 70% lower than default avg_dollar_volume_std=800_000.0, volume_ratio_mean=1.2, ), signal_strength=0.30, seed=1012, ) # --------------------------------------------------------------------------- # Registry and groups # --------------------------------------------------------------------------- SCENARIO_REGISTRY: dict[str, ScenarioConfig] = { s.name: s for s in [ STEADY_BULL, STEADY_BEAR, CRASH_V_RECOVERY, PROLONGED_BEAR, LOW_VOL_GRIND, HIGH_VOL_CHOP, REGIME_SWITCH, VIX_SPIKE, NO_SIGNAL, STRONG_SIGNAL, SECTOR_ROTATION, LIQUIDITY_DROUGHT, ] } SCENARIO_GROUPS: dict[str, list[str]] = { "trend": ["steady_bull", "steady_bear", "prolonged_bear"], "volatility": ["low_vol_grind", "high_vol_chop", "vix_spike"], "regime": ["regime_switch", "crash_v_recovery"], "signal": ["no_signal", "strong_signal"], "structural": ["sector_rotation", "liquidity_drought"], "quick": ["steady_bull", "steady_bear", "no_signal"], "all": list(SCENARIO_REGISTRY.keys()), }