122 lines
4.9 KiB
Python
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
|