chanmacro: Signal Expectancy Engine V1 — Market Memory System
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>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user