174 lines
6.6 KiB
Python
174 lines
6.6 KiB
Python
"""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
|