This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Unit tests for V36 market-anchored score matrix (pure, no DB/model deps)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||
|
||||
from models.score_matrix import (
|
||||
MAX_GOALS,
|
||||
_raw_matrix,
|
||||
_outcome_sums,
|
||||
build_calibrated_score_package,
|
||||
ipf_to_outcomes,
|
||||
split_lambdas,
|
||||
top_scores,
|
||||
total_lambda_from_over25,
|
||||
)
|
||||
|
||||
|
||||
def _approx(a, b, tol=1e-6):
|
||||
return abs(a - b) <= tol
|
||||
|
||||
|
||||
def test_total_lambda_solver_roundtrip():
|
||||
import math
|
||||
for t_true in (1.5, 2.4, 3.5):
|
||||
p_over = 1.0 - math.exp(-t_true) * (1 + t_true + t_true * t_true / 2)
|
||||
assert _approx(total_lambda_from_over25(p_over), t_true, 1e-3)
|
||||
|
||||
|
||||
def test_split_matches_win_gap_direction():
|
||||
lh, la = split_lambdas(2.6, 0.60, 0.18) # strong home side
|
||||
assert lh > la
|
||||
lh2, la2 = split_lambdas(2.6, 0.18, 0.60) # strong away side
|
||||
assert la2 > lh2
|
||||
|
||||
|
||||
def test_ipf_makes_matrix_exactly_consistent_with_1x2():
|
||||
p1, px, p2 = 0.62, 0.21, 0.17
|
||||
lh, la = split_lambdas(2.7, p1, p2)
|
||||
mat = ipf_to_outcomes(_raw_matrix(lh, la), p1, px, p2)
|
||||
w, d, l = _outcome_sums(mat)
|
||||
assert _approx(w, p1, 1e-9) and _approx(d, px, 1e-9) and _approx(l, p2, 1e-9)
|
||||
|
||||
|
||||
def test_top_scores_sorted_and_shaped():
|
||||
mat = _raw_matrix(1.6, 1.1)
|
||||
top = top_scores(mat, 5)
|
||||
assert len(top) == 5
|
||||
probs = [t["prob"] for t in top]
|
||||
assert probs == sorted(probs, reverse=True)
|
||||
assert all("-" in t["score"] for t in top)
|
||||
|
||||
|
||||
def test_package_full_fields_and_consistency():
|
||||
pkg = build_calibrated_score_package(0.526, 0.258, 0.216, 0.55)
|
||||
assert pkg["ft"] and pkg["ht"]
|
||||
assert pkg["xg_home"] > pkg["xg_away"] # home is favourite
|
||||
assert _approx(pkg["xg_total"], pkg["xg_home"] + pkg["xg_away"], 0.02)
|
||||
assert len(pkg["scenario_top5"]) == 5
|
||||
assert pkg["calibration_source"] == "market_anchor_v36_score"
|
||||
# HT must be a lower-scoring line than FT on average
|
||||
fh, fa = map(int, str(pkg["ft"]).split("-"))
|
||||
hh, ha = map(int, str(pkg["ht"]).split("-"))
|
||||
assert hh + ha <= fh + fa
|
||||
|
||||
|
||||
def test_ht_ipf_applied_when_probs_given():
|
||||
base = build_calibrated_score_package(0.40, 0.30, 0.30, 0.50)
|
||||
forced = build_calibrated_score_package(
|
||||
0.40, 0.30, 0.30, 0.50, ht_probs=(0.05, 0.90, 0.05)
|
||||
)
|
||||
# forcing a near-certain HT draw must make the modal HT score a draw line
|
||||
hh, ha = map(int, str(forced["ht"]).split("-"))
|
||||
assert hh == ha
|
||||
assert base["ft"] == forced["ft"] # FT untouched by HT anchoring
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||
for fn in fns:
|
||||
fn()
|
||||
print(f"PASS {fn.__name__}")
|
||||
print(f"\nAll {len(fns)} tests passed.")
|
||||
Reference in New Issue
Block a user