This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user