Regime Stress Test

Test strategy behavior across trend / chop / crash regimes

Run a strategy through a programmable regime sequence to see how it handles each regime. This is the best single test of whether risk enforcement actually bounds losses under adverse conditions.

The file

python
# regime_stress_test.py
import horizon as hz
from horizon.asset_classes import AssetClass, Equity
from horizon.data import SyntheticRegimes
from horizon.discovery import StaticUniverse
from horizon.discovery.base import Market
from horizon.portfolio import KellyOptimizer
from horizon.quant import BollingerMeanRev, TSMomentum
from horizon.risk import (
    DrawdownGuard,
    RiskConfig,
    StopLoss,
)
from horizon.risk.drawdown import DrawdownAction


def main():
    universe = StaticUniverse([Market(id="A", asset_class=AssetClass.Equity)])

    # Four regimes: uptrend, chop, crash, recovery
    data = SyntheticRegimes(
        market_ids=["A"],
        n_bars=500,
        regimes=[
            (0.25, 0.30, 0.15),    # 125 bars. strong uptrend
            (0.25, 0.00, 0.20),    # 125 bars. chop
            (0.25, -0.40, 0.35),   # 125 bars. CRASH
            (0.25, 0.15, 0.20),    # 125 bars. recovery
        ],
        seed=11,
    )

    # Strict risk to bound the crash
    risk = RiskConfig(
        max_gross_leverage=1.0,
        stop_loss=StopLoss(per_position_pct=0.05),
        drawdown=[
            DrawdownGuard(daily_pct=0.05, action=DrawdownAction.HaltNew),
            DrawdownGuard(weekly_pct=0.10, action=DrawdownAction.ReduceHalf),
            DrawdownGuard(monthly_pct=0.20, action=DrawdownAction.Flatten),
        ],
    )

    result = hz.run(
        mode="backtest",
        strategies=[
            TSMomentum(lookback=20, edge_bps=60),
            BollingerMeanRev(window=20, entry_z=2.0, edge_bps=50),
        ],
        asset_classes=[Equity],
        universe=universe,
        portfolio=KellyOptimizer(kelly_fraction=0.25, max_gross_leverage=1.0),
        risk=risk,
        data_source=data,
        backtest=hz.BacktestConfig(initial_cash_usd=100_000),
    )

    print("=" * 60)
    print("REGIME STRESS TEST. uptrend / chop / crash / recovery")
    print("=" * 60)
    print(f"  Ticks:        {len(result.equity_curve)}")
    print(f"  Trades:       {result.n_trades}")
    print(f"  Start equity: ${result.equity_curve[0][1]:,.2f}")
    print(f"  End equity:   ${result.equity_curve[-1][1]:,.2f}")
    print(f"  Peak equity:  ${max(e for _, e in result.equity_curve):,.2f}")
    print(f"  Total return: {result.total_return:+.2%}")
    print(f"  Sharpe:       {result.sharpe:+.3f}")
    print(f"  Max drawdown: {result.max_drawdown:.2%}")

    # Verify risk layer bounded the crash
    assert result.max_drawdown < 0.35, (
        f"Max drawdown {result.max_drawdown:.2%} exceeded 35%. risk layer failed to bound crash"
    )
    print()
    print("✓ Risk layer successfully bounded the crash to < 35% drawdown")


if __name__ == "__main__":
    main()

What it tests

Uptrend (125 bars)

TSMomentum should profit. BollingerMeanRev might chop a little. Equity climbs.

Chop (125 bars)

Both strategies struggle: no trend for TSMomentum, no sustained mean to revert to for BollingerMeanRev. Equity grinds.

Crash (125 bars)

The hard test. Without risk enforcement, strategies buy the dip repeatedly and bleed. With stops + drawdown guards, losses are bounded.

Recovery (125 bars)

If the strategy is still alive (drawdown didn't flatten it), it should recover some of the losses.

Expected behavior

With the moderate risk config:

  • Max drawdown bounded to ~20-30% (the monthly guard fires, flattening the book)
  • Sharpe probably negative or near zero (mean-reversion in a crash is painful)
  • Trades in the hundreds across all regimes
  • End equity below start equity, but nowhere near zero

The assertion at the bottom is the actual test: max_drawdown < 0.35. If it fails, the risk layer didn’t bound the crash. that’s a regression.

Run it

bash
PYTHONPATH=. python3 regime_stress_test.py

Vary the regime

Experiment with different regime sequences:

python
# Pure crash
regimes=[(1.0, -0.50, 0.40)]

# Double crash
regimes=[
    (0.50, -0.30, 0.30),
    (0.50, -0.30, 0.35),
]

# Crash followed by recovery
regimes=[
    (0.40, -0.40, 0.35),
    (0.60, 0.20, 0.20),
]

Next