You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

333 lines
12 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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