"""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.")