|
|
"""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()),
|
|
|
}
|