"""Test SignalTracker, TimeDecay, and BayesianExpectancyEngine.""" import pytest from datetime import date, timedelta import numpy as np class TestTimeDecay: def test_recent_weight_near_one(self): from expectancy.decay import TimeDecay d = TimeDecay(180) w = d.weight(date(2026, 6, 20), date(2026, 6, 24)) assert 0.95 < w < 1.0 def test_old_weight_decays(self): from expectancy.decay import TimeDecay d = TimeDecay(180) w = d.weight(date(2025, 6, 24), date(2026, 6, 24)) assert 0.2 < w < 0.3 # ~365 days at half_life=180 def test_effective_samples(self): from expectancy.decay import TimeDecay d = TimeDecay(180) dates = [date(2026, 6, 24)] * 10 weights = d.weights(dates, date(2026, 6, 24)) eff = d.effective_samples(weights) assert eff == pytest.approx(10.0, rel=0.01) def test_weighted_win_rate(self): from expectancy.decay import TimeDecay d = TimeDecay(180) wins = np.array([1, 0, 1, 0]) weights = np.array([1.0, 1.0, 1.0, 1.0]) wr = d.weighted_win_rate(wins, weights) assert wr == 0.5 def test_weight_at_age(self): from expectancy.decay import TimeDecay w = TimeDecay.weight_at_age(180, 180) assert w == pytest.approx(0.5, rel=0.01) class TestSignalTracker: def test_record_signal(self, db_path, sample_state): from expectancy.tracker import SignalTracker tracker = SignalTracker() rid = tracker.record( date(2026, 3, 15), "B3", 98000.0, sample_state, signal_grade="A", signal_strength=75.0, ) assert rid is not None assert rid > 0 def test_get_samples(self, db_path, sample_state): from expectancy.tracker import SignalTracker tracker = SignalTracker() tracker.record(date(2026, 3, 15), "B3", 98000.0, sample_state) tracker.record(date(2026, 3, 16), "B2", 98500.0, sample_state) samples = tracker.get_samples(signal_type="B3") assert len(samples) == 1 assert samples[0]["signal_type"] == "B3" def test_count_samples(self, db_path, sample_state): from expectancy.tracker import SignalTracker tracker = SignalTracker() tracker.record(date(2026, 3, 15), "B3", 98000.0, sample_state) tracker.record(date(2026, 3, 16), "B3", 98500.0, sample_state) counts = tracker.count_samples() assert "B3/TREND" in counts assert counts["B3/TREND"] == 2 def test_filter_by_regime(self, db_path, sample_state): from expectancy.tracker import SignalTracker tracker = SignalTracker() tracker.record(date(2026, 3, 15), "B3", 98000.0, sample_state) samples = tracker.get_samples(signal_type="B3", regime="TREND") assert len(samples) == 1 samples = tracker.get_samples(signal_type="B3", regime="PANIC") assert len(samples) == 0 def test_backfill_signals(self, db_path, sample_state): from expectancy.tracker import SignalTracker tracker = SignalTracker() signals = [ {"date": date(2026, 3, 15), "signal_type": "B3", "entry_price": 98000}, {"date": date(2026, 3, 20), "signal_type": "B2", "entry_price": 99000}, ] count = tracker.backfill_signals(signals) assert count == 2 class TestBayesianExpectancyEngine: def test_estimate_returns_report(self, db_path, sample_state): from expectancy.tracker import SignalTracker from expectancy.engine import BayesianExpectancyEngine # Record some signals first tracker = SignalTracker() for i in range(10): tracker.record( date(2026, 3, 15) + timedelta(days=i), "B3", 98000.0, sample_state, ) engine = BayesianExpectancyEngine(level_min_samples=3) report = engine.estimate(sample_state, "B3", date(2026, 3, 25)) assert report.signal_type == "B3" assert len(report.layers) > 0 assert report.source in ("bayesian", "insufficient") def test_insufficient_with_no_samples(self, db_path, sample_state): from expectancy.engine import BayesianExpectancyEngine engine = BayesianExpectancyEngine(level_min_samples=10) report = engine.estimate(sample_state, "B1", date(2026, 3, 25)) assert report.sufficiency.value in ("INSUFFICIENT", "LOW", "MEDIUM", "HIGH") def test_empirical_bayes_shrinks_small_samples(self, db_path, sample_state): """With N=3, raw=100%, posterior should be pulled toward prior.""" from expectancy.tracker import SignalTracker from expectancy.engine import BayesianExpectancyEngine tracker = SignalTracker() for i in range(3): tracker.record( date(2026, 3, 15) + timedelta(days=i), "B3", 98000.0, sample_state, ) engine = BayesianExpectancyEngine(level_min_samples=1) report = engine.estimate(sample_state, "B3", date(2026, 3, 25)) # With small N, posterior should differ from raw base_layer = report.layers[0] if base_layer.raw_winrate and base_layer.samples < 50: # Posterior should be pulled toward prior (50% or global rate) if base_layer.raw_winrate > 0.8: assert base_layer.posterior_winrate < base_layer.raw_winrate def test_leveled_fallback_stops_at_min_samples(self, db_path, sample_state): from expectancy.tracker import SignalTracker from expectancy.engine import BayesianExpectancyEngine tracker = SignalTracker() for i in range(20): tracker.record(date(2026, 3, 15) + timedelta(days=i), "B3", 98000.0, sample_state) engine = BayesianExpectancyEngine(level_min_samples=15) report = engine.estimate(sample_state, "B3", date(2026, 3, 25)) # Should have stopped at a level with >= 15 effective samples assert report.final_estimate >= 0 class TestSufficiencyGuard: def test_insufficient(self): from expectancy.engine import SufficiencyGuard from models import SufficiencyLevel g = SufficiencyGuard() assert g.evaluate(10) == SufficiencyLevel.INSUFFICIENT def test_low(self): from expectancy.engine import SufficiencyGuard from models import SufficiencyLevel g = SufficiencyGuard() assert g.evaluate(40) == SufficiencyLevel.LOW def test_high(self): from expectancy.engine import SufficiencyGuard from models import SufficiencyLevel g = SufficiencyGuard() assert g.evaluate(200) == SufficiencyLevel.HIGH