Phase A-C complete: 4 core factors, regime detection, signal tracking, Bayesian expectancy. chanmacro/ (32 files, ~4000 lines): - models: 12 enums + 15 Pydantic v2 models (DateAwareModel, MarketStateVector, etc.) - fetchers: OHLCV + Breadth (from data_provider) + Derivatives (new endpoint) - scoring: Price Structure / Breadth (quantile buckets) / OI Matrix (5 discrete states) / Volatility Regime - regime_detector: 3-state (TREND/RANGE/PANIC), factor-locked (Price+Breadth+Vol), versioned, 2-day confirmation - expectancy: SignalTracker (record+outcomes), TimeDecay (half-life=180d), BayesianExpectancyEngine (Empirical Bayes, Leveled, SufficiencyGuard) - validation: FactorValidator (IC/ICIR/Hit Ratio), RegimeValidator (MI/KL/ANOVA), TransitionValidator (stability) - CLI: fetch|score|regime|track|backfill|expectancy|validate|serve - tests: 52 passing (models, scoring, regime, expectancy) data_provider: - /api/derivatives endpoint: funding rate, OI, OI change, basis - _derivatives storage: same persist pattern as K-line (merge→lock→snapshot→atomic write) - background refresh every 60s Co-Authored-By: Claude <noreply@anthropic.com>
110 lines
4.3 KiB
Python
110 lines
4.3 KiB
Python
"""Test regime detector and validation."""
|
|
import pytest
|
|
from datetime import date
|
|
import pandas as pd
|
|
import numpy as np
|
|
|
|
|
|
class TestRegimeDetector:
|
|
def test_detects_trend(self):
|
|
from regime_detector import RegimeDetector
|
|
from models import MarketRegime
|
|
d = RegimeDetector()
|
|
r = d.detect(75.0, 80.0, "NORMAL_VOL", date(2026, 6, 24))
|
|
assert r.regime == MarketRegime.TREND
|
|
assert r.confidence > 0.5
|
|
|
|
def test_detects_range(self):
|
|
from regime_detector import RegimeDetector
|
|
from models import MarketRegime
|
|
d = RegimeDetector()
|
|
r = d.detect(50.0, 50.0, "LOW_VOL", date(2026, 6, 24))
|
|
assert r.regime in (MarketRegime.RANGE, MarketRegime.TREND)
|
|
|
|
def test_detects_panic(self):
|
|
from regime_detector import RegimeDetector
|
|
from models import MarketRegime
|
|
d = RegimeDetector()
|
|
r = d.detect(15.0, 10.0, "EXPLOSIVE_VOL", date(2026, 6, 24))
|
|
assert r.regime == MarketRegime.PANIC
|
|
|
|
def test_2day_confirmation(self):
|
|
from regime_detector import RegimeDetector
|
|
from models import MarketRegime
|
|
d = RegimeDetector()
|
|
# Day 1: RANGE
|
|
r1 = d.detect(50.0, 50.0, "LOW_VOL", date(2026, 6, 24))
|
|
assert r1.regime == MarketRegime.RANGE # first run, no confirmation needed
|
|
# Day 2: still RANGE
|
|
r2 = d.detect(50.0, 50.0, "LOW_VOL", date(2026, 6, 25))
|
|
assert r2.regime == MarketRegime.RANGE
|
|
assert r2.confirmation_days == 2
|
|
|
|
def test_transition_needs_confirmation(self):
|
|
from regime_detector import RegimeDetector
|
|
from models import MarketRegime
|
|
d = RegimeDetector()
|
|
# Establish TREND
|
|
d.detect(75.0, 80.0, "NORMAL_VOL", date(2026, 6, 24))
|
|
d.detect(75.0, 80.0, "NORMAL_VOL", date(2026, 6, 25))
|
|
# Day 3: weak scores → raw best = RANGE, but TREND should persist
|
|
r3 = d.detect(35.0, 40.0, "NORMAL_VOL", date(2026, 6, 26))
|
|
# First day of pending transition — should still be TREND
|
|
assert r3.regime == MarketRegime.TREND
|
|
assert d.pending_regime is not None
|
|
|
|
def test_version_is_stored(self):
|
|
from regime_detector import RegimeDetector
|
|
d = RegimeDetector(regime_version="v1_price_breadth_vol")
|
|
r = d.detect(75.0, 80.0, "NORMAL_VOL", date(2026, 6, 24))
|
|
assert r.regime_version == "v1_price_breadth_vol"
|
|
|
|
def test_load_state(self, db_path):
|
|
from regime_detector import RegimeDetector
|
|
from models import MarketRegime
|
|
d = RegimeDetector()
|
|
d.load_state(db_path)
|
|
# DB has TREND for first 100 days, so most recent should load
|
|
assert d.current_regime is not None
|
|
|
|
def test_confidence_for_confirmed_regime(self):
|
|
"""Confidence should be for the confirmed regime, not raw best."""
|
|
from regime_detector import RegimeDetector
|
|
from models import MarketRegime
|
|
d = RegimeDetector()
|
|
# Establish TREND
|
|
d.detect(75.0, 80.0, "NORMAL_VOL", date(2026, 6, 24))
|
|
d.detect(75.0, 80.0, "NORMAL_VOL", date(2026, 6, 25))
|
|
# Now feed weak scores → raw best would be PANIC or RANGE
|
|
r = d.detect(15.0, 10.0, "EXPLOSIVE_VOL", date(2026, 6, 26))
|
|
# Should still report TREND (need 2 confirmations to switch)
|
|
assert r.regime == MarketRegime.TREND
|
|
|
|
|
|
class TestTransitionValidator:
|
|
def test_stable_regime_passes(self):
|
|
from validation.transition_validator import TransitionValidator
|
|
# Create stable regime sequence: long periods
|
|
seq = pd.Series(
|
|
["TREND"] * 50 + ["RANGE"] * 50 + ["PANIC"] * 40,
|
|
index=pd.date_range("2026-01-01", periods=140),
|
|
)
|
|
tv = TransitionValidator()
|
|
report = tv.validate(seq)
|
|
assert report.is_stable
|
|
assert report.avg_duration > 20
|
|
assert report.flip_rate < 0.05
|
|
|
|
def test_unstable_regime_fails(self):
|
|
from validation.transition_validator import TransitionValidator
|
|
# Create unstable sequence: flips every 2 days
|
|
seq = pd.Series(
|
|
["TREND", "TREND", "RANGE", "RANGE", "TREND", "TREND",
|
|
"PANIC", "PANIC", "RANGE", "RANGE"] * 5,
|
|
index=pd.date_range("2026-01-01", periods=50),
|
|
)
|
|
tv = TransitionValidator()
|
|
report = tv.validate(seq)
|
|
assert not report.is_stable
|
|
assert report.flip_rate > 0.15
|