Files
Chan/ChanMacro/tests/test_scoring.py
jackyu66gitandClaude 71951019fb 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>
2026-06-24 17:44:55 +08:00

122 lines
4.9 KiB
Python

"""Test all 4 core scorers."""
import pytest
from datetime import date
class TestPriceStructureScorer:
def test_computes_score(self, db_path):
from scoring.price_structure import PriceStructureScorer
scorer = PriceStructureScorer()
result = scorer.compute(date(2026, 3, 15))
assert result.name == "Price Structure"
assert 0 <= result.score <= 100
assert result.trend_strength >= 0
assert result.volatility_compression >= 0
assert result.momentum >= 0
assert result.label
def test_bullish_in_trend(self, db_path):
from scoring.price_structure import PriceStructureScorer
scorer = PriceStructureScorer()
result = scorer.compute(date(2025, 11, 15)) # TREND period
assert result.score > 50 # Should be bullish in uptrend
def test_bearish_in_panic(self, db_path):
from scoring.price_structure import PriceStructureScorer
scorer = PriceStructureScorer()
result = scorer.compute(date(2026, 5, 15)) # PANIC period
# In panic period, EMA alignment should be bearish
assert result.trend_strength < 60
def test_no_data_handling(self, db_path):
from scoring.price_structure import PriceStructureScorer
scorer = PriceStructureScorer()
result = scorer.compute(date(2020, 1, 1))
assert result.score == 50.0
assert result.label == "No Data"
class TestBreadthScorer:
def test_computes_score(self, db_path):
from scoring.breadth_scorer import BreadthScorer
scorer = BreadthScorer()
result = scorer.compute(date(2026, 3, 15))
assert result.name == "Breadth"
assert 0 <= result.score <= 100
assert result.breadth_bucket
assert result.breadth_top20 >= 0
assert result.breadth_top50 >= 0
def test_tier_values(self, db_path):
from scoring.breadth_scorer import BreadthScorer
scorer = BreadthScorer()
result = scorer.compute(date(2025, 11, 15)) # TREND period
# Top20 should generally be higher than Top50 (large caps lead)
assert result.breadth_top20 >= 0
assert result.breadth_top50 >= 0
def test_bucket_assignment(self, db_path):
from scoring.breadth_scorer import BreadthScorer, BreadthBucket
scorer = BreadthScorer()
result = scorer.compute(date(2025, 11, 15)) # TREND: adv=42/50
assert result.breadth_bucket in (
BreadthBucket.EXTREME, BreadthBucket.STRONG, BreadthBucket.NORMAL
)
def test_no_data(self, db_path):
from scoring.breadth_scorer import BreadthScorer
scorer = BreadthScorer()
result = scorer.compute(date(2020, 1, 1))
assert result.score == 50.0
class TestOIMatrixScorer:
def test_computes_state(self, db_path):
from scoring.oi_matrix import OIMatrixScorer, OIState
scorer = OIMatrixScorer()
result = scorer.compute(date(2025, 11, 15)) # TREND period, oi_chg=+3.5
assert result.oi_state in OIState
assert 0 <= result.score <= 100
def test_new_longs_in_trend(self, db_path):
from scoring.oi_matrix import OIMatrixScorer, OIState
scorer = OIMatrixScorer()
# Test multiple dates in TREND period — at least one should be NEW_LONGS or NEUTRAL
found_bullish = False
for d in ["2025-11-15", "2025-11-20", "2025-12-01", "2025-12-15"]:
result = scorer.compute(date.fromisoformat(d))
if result.oi_state in (OIState.NEW_LONGS, OIState.SHORT_COVERING, OIState.NEUTRAL):
found_bullish = True
break
assert found_bullish, "No bullish OI state found in TREND period"
def test_no_data(self, db_path):
from scoring.oi_matrix import OIMatrixScorer
scorer = OIMatrixScorer()
result = scorer.compute(date(2020, 1, 1))
assert result.score == 50.0
assert result.label == "No Data"
class TestVolatilityRegimeScorer:
def test_computes_regime(self, db_path):
from scoring.volatility_regime import VolatilityRegimeScorer, VolRegime
scorer = VolatilityRegimeScorer()
result = scorer.compute(date(2026, 3, 15))
assert result.vol_regime in VolRegime
assert 0 <= result.score <= 100
def test_higher_vol_in_panic(self, db_path):
from scoring.volatility_regime import VolatilityRegimeScorer, VolRegime
scorer = VolatilityRegimeScorer()
trend_result = scorer.compute(date(2025, 11, 15))
panic_result = scorer.compute(date(2026, 5, 15))
# PANIC period has higher ATR → higher vol regime or score
assert panic_result.atr_pct >= trend_result.atr_pct * 0.5 # at least comparable
def test_no_data(self, db_path):
from scoring.volatility_regime import VolatilityRegimeScorer
scorer = VolatilityRegimeScorer()
result = scorer.compute(date(2020, 1, 1))
assert result.score == 50.0