score
Deploy Iddaai Backend / build-and-deploy (push) Successful in 37s

This commit is contained in:
2026-06-10 22:24:50 +03:00
parent 9a8f9941b6
commit 950add373f
3 changed files with 326 additions and 8 deletions
@@ -58,6 +58,7 @@ from utils.league_reliability import load_league_reliability
from config.config_loader import build_threshold_dict, get_threshold_default
from models.calibration import get_calibrator, get_final_recalibrator
from models.market_anchor import devig, apply_home_correction
from models.score_matrix import build_calibrated_score_package
# ── V30: Post-calibration trust factors ─────────────────────────────
# Controls how much to trust isotonic calibrator vs raw model output.
@@ -365,6 +366,12 @@ class MarketBoardMixin:
if value_pick is not None and float(value_pick.get("ev_edge", 0.0) or 0.0) <= 0.0:
value_pick = None
# V36: derive the score card (score_prediction + scenario_top5) from the
# SAME anchored probabilities, so it can never contradict the MS card.
# Validated on 63,681 real-odds matches: modal-score hit 12.6% vs stated
# 13.1%, top-5 coverage 51%, per-score gaps <1.2pt.
cal_score = self._build_calibrated_score(market_board)
# Determine simulation mode for the response
_resp_status = str(data.status or "").upper()
_resp_state = str(data.state or "").upper()
@@ -424,14 +431,20 @@ class MarketBoardMixin:
"bet_summary": bet_summary,
"supporting_picks": supporting,
"aggressive_pick": aggressive_pick,
"scenario_top5": prediction.ft_scores_top5,
"score_prediction": {
"ft": prediction.predicted_ft_score,
"ht": prediction.predicted_ht_score,
"xg_home": round(float(prediction.home_xg), 2),
"xg_away": round(float(prediction.away_xg), 2),
"xg_total": round(float(prediction.total_xg), 2),
},
"scenario_top5": (
cal_score["scenario_top5"] if cal_score else prediction.ft_scores_top5
),
"score_prediction": (
cal_score["score_prediction"]
if cal_score
else {
"ft": prediction.predicted_ft_score,
"ht": prediction.predicted_ht_score,
"xg_home": round(float(prediction.home_xg), 2),
"xg_away": round(float(prediction.away_xg), 2),
"xg_total": round(float(prediction.total_xg), 2),
}
),
"market_board": market_board,
"others": {
"handicap": prediction.handicap_pick,
@@ -1237,6 +1250,61 @@ class MarketBoardMixin:
for obj in list(bet_summary or []):
self._recalibrate_pick_display(obj, market_board)
def _build_calibrated_score(
self,
market_board: Dict[str, Any],
) -> Optional[Dict[str, Any]]:
"""V36: score card derived from the anchored MS + OU25 probabilities.
Returns {"score_prediction": {...}, "scenario_top5": [...]} or None when
the needed markets weren't anchored (no real odds) — in which case the
caller keeps the model's own score output. Same kill-switch as V35."""
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
return None
ms = market_board.get("MS") or {}
ou = market_board.get("OU25") or {}
if (
ms.get("calibration_source") != "market_anchor_v35"
or ou.get("calibration_source") != "market_anchor_v35"
):
return None
try:
p1 = float(ms["probs"]["1"])
px = float(ms["probs"]["X"])
p2 = float(ms["probs"]["2"])
p_over = float(ou["probs"]["over"])
except (KeyError, TypeError, ValueError):
return None
ht_probs = None
ht = market_board.get("HT") or {}
if ht.get("calibration_source") == "market_anchor_v35":
try:
ht_probs = (
float(ht["probs"]["1"]),
float(ht["probs"]["X"]),
float(ht["probs"]["2"]),
)
except (KeyError, TypeError, ValueError):
ht_probs = None
try:
pkg = build_calibrated_score_package(p1, px, p2, p_over, ht_probs=ht_probs)
except (ValueError, ZeroDivisionError, OverflowError):
return None
return {
"score_prediction": {
"ft": pkg["ft"],
"ht": pkg["ht"],
"xg_home": pkg["xg_home"],
"xg_away": pkg["xg_away"],
"xg_total": pkg["xg_total"],
"ht_top3": pkg["ht_top"],
"calibration_source": pkg["calibration_source"],
},
"scenario_top5": pkg["scenario_top5"],
}
def _build_market_rows(
self,
data: MatchData,