85 lines
2.7 KiB
Python
85 lines
2.7 KiB
Python
"""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.")
|