543537e33f
Three-module quant framework replacing 'sort by Sharpe' with proper statistical validation: quant/significance.py (15 tests): - deflated_sharpe_ratio(): adjusts for N trials (Harvey & Liu 2015) - probabilistic_sharpe_ratio(): P(True SR > benchmark) given T, skew, kurt - sharpe_haircut(): expected OOS Sharpe after selection bias deflation - QuantVerdict: DEPLOY / SIMULATE / DISCARD with 5-point scoring - validate_strategy(): one-shot validation function quant/regimes.py (8 tests): - classify_regime(): trending_up/down, ranging, volatile - RegimeClassifier: stateful rolling-window classifier - conditional_performance(): per-regime trade statistics quant/walkforward.py (5 tests): - WalkForwardRunner: sequential IS/OOS window optimization - WFWindow/WFReport: structured walk-forward results - consistency score, performance decay, concatenated OOS equity - significance_report() integration Walk-forward results (real HL data with date-sliced windows): grid_mm 1h: 2/4 pos, OOS S=-0.45, 74t, haircut=-22.66 → DISCARD momentum 4h: 2/4 pos, OOS S=-1.47, 116t, haircut=-45.35 → DISCARD composite_mm 1h: 2/4 pos, OOS S=+2.97, 6t, haircut=+43.25 → SIMULATE 28 tests total
88 lines
3.0 KiB
Python
88 lines
3.0 KiB
Python
"""
|
|
Tests for quant/regimes.py and quant/walkforward.py.
|
|
"""
|
|
import numpy as np
|
|
from quant.regimes import RegimeClassifier, classify_regime, conditional_performance
|
|
|
|
|
|
class TestRegimeClassifier:
|
|
def test_initial_state(self):
|
|
rc = RegimeClassifier()
|
|
assert rc.current_regime == "unknown"
|
|
|
|
def test_classify_trending(self):
|
|
"""Rising prices with low volatility → trending_up."""
|
|
regime = classify_regime(
|
|
returns_20=0.15, # +15% over 20 bars
|
|
vol_20=0.02, # 2% vol
|
|
vol_ratio=1.0, # normal volume
|
|
)
|
|
assert regime == "trending_up"
|
|
|
|
def test_classify_ranging(self):
|
|
"""Flat prices with low volatility → ranging."""
|
|
regime = classify_regime(
|
|
returns_20=0.005, # near flat
|
|
vol_20=0.01,
|
|
vol_ratio=1.0,
|
|
)
|
|
assert regime == "ranging"
|
|
|
|
def test_classify_volatile(self):
|
|
"""High volatility regardless of direction → volatile."""
|
|
regime = classify_regime(
|
|
returns_20=0.02,
|
|
vol_20=0.08, # high vol
|
|
vol_ratio=2.5, # volume spike
|
|
)
|
|
assert regime == "volatile"
|
|
|
|
def test_classify_trending_down(self):
|
|
"""Falling prices → trending_down."""
|
|
regime = classify_regime(
|
|
returns_20=-0.12, # -12%
|
|
vol_20=0.03,
|
|
vol_ratio=1.0,
|
|
)
|
|
assert regime == "trending_down"
|
|
|
|
def test_feed_updates_regime(self):
|
|
rc = RegimeClassifier(window=20)
|
|
# Feed 21 downtrend bars (need window+1 for first classification)
|
|
for i in range(21):
|
|
px = 100 - i * 2
|
|
rc.feed(px, close=px, open_px=px - 0.5, vol=10)
|
|
assert rc.current_regime == "trending_down"
|
|
|
|
# Feed 21 uptrend bars
|
|
for i in range(21):
|
|
px = 58 + i * 3
|
|
rc.feed(px, close=px, open_px=px + 0.5, vol=10)
|
|
assert rc.current_regime == "trending_up"
|
|
|
|
def test_regime_counts(self):
|
|
rc = RegimeClassifier(window=20)
|
|
for i in range(100):
|
|
if i < 30:
|
|
px = 100 + i
|
|
elif i < 60:
|
|
px = 130 - (i - 30) * 0.5
|
|
else:
|
|
px = 115 + (i - 60) * 2
|
|
rc.feed(px, close=px, open_px=px, vol=10)
|
|
counts = rc.regime_counts()
|
|
assert sum(counts.values()) > 0
|
|
|
|
def test_conditional_performance(self):
|
|
"""Verify per-regime statistics computation."""
|
|
trades = [
|
|
{"time": "2026-01-01T00:00:00", "pnl_net": 100, "pnl_gross": 120},
|
|
{"time": "2026-01-02T00:00:00", "pnl_net": -50, "pnl_gross": -40},
|
|
{"time": "2026-01-03T00:00:00", "pnl_net": 200, "pnl_gross": 220},
|
|
]
|
|
# 3 trades all in trending_up regime
|
|
result = conditional_performance(trades, {"trending_up": 3, "volatile": 0})
|
|
assert result["regimes"]["trending_up"]["count"] == 3
|
|
assert result["regimes"]["trending_up"]["avg_pnl"] > 0
|
|
assert result["best_regime"] == "trending_up"
|