@@ -57,8 +57,9 @@ from utils.top_leagues import load_top_league_ids
|
||||
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.market_anchor import devig, apply_corrections
|
||||
from models.score_matrix import build_calibrated_score_package
|
||||
from models.live_matrix import build_live_projection, estimate_minute
|
||||
|
||||
# ── V30: Post-calibration trust factors ─────────────────────────────
|
||||
# Controls how much to trust isotonic calibrator vs raw model output.
|
||||
@@ -372,6 +373,12 @@ class MarketBoardMixin:
|
||||
# 13.1%, top-5 coverage 51%, per-score gaps <1.2pt.
|
||||
cal_score = self._build_calibrated_score(market_board)
|
||||
|
||||
# V38: while the match is LIVE, also project score/minute-conditioned
|
||||
# probabilities (P(side scores again), live 1X2, comeback, scenarios).
|
||||
# OOS-validated on 70,410 reconstructed live moments: ECE 0.5-0.8%;
|
||||
# "one-goal lead at 80'" case: said 21.7% vs actual 23.0%.
|
||||
live_projection = self._build_live_projection(market_board, data)
|
||||
|
||||
# Determine simulation mode for the response
|
||||
_resp_status = str(data.status or "").upper()
|
||||
_resp_state = str(data.state or "").upper()
|
||||
@@ -446,6 +453,9 @@ class MarketBoardMixin:
|
||||
}
|
||||
),
|
||||
"market_board": market_board,
|
||||
# V38: score/minute-aware live probabilities (None when not live or
|
||||
# no real odds). FE can render "deplasman gol atar: %X / dönme: %Y".
|
||||
"live_projection": live_projection,
|
||||
"others": {
|
||||
"handicap": prediction.handicap_pick,
|
||||
"cards": {
|
||||
@@ -1115,10 +1125,10 @@ class MarketBoardMixin:
|
||||
val = self._real_market_odds(odds, key)
|
||||
return val if val > 1.01 else None
|
||||
|
||||
# MS (3-way) + home-favourite correction; DC derived from the same vector
|
||||
# MS (3-way) + favourite corrections; DC derived from the same vector
|
||||
ms = devig([real("ms_h"), real("ms_d"), real("ms_a")])
|
||||
if ms is not None:
|
||||
p1, px, p2 = apply_home_correction(*ms)
|
||||
p1, px, p2 = apply_corrections(*ms)
|
||||
if "MS" in market_board:
|
||||
self._set_board(market_board, "MS", {"1": p1, "X": px, "2": p2})
|
||||
if "DC" in market_board:
|
||||
@@ -1305,6 +1315,43 @@ class MarketBoardMixin:
|
||||
"scenario_top5": pkg["scenario_top5"],
|
||||
}
|
||||
|
||||
def _build_live_projection(
|
||||
self,
|
||||
market_board: Dict[str, Any],
|
||||
data: MatchData,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""V38: score/minute-conditioned live projection from the anchored
|
||||
probabilities. None unless the match is live, both MS and OU25 were
|
||||
anchored (real odds) and a minute estimate exists. Same kill-switch."""
|
||||
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
|
||||
return None
|
||||
if not self._is_live_match(data):
|
||||
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
|
||||
minute = estimate_minute(
|
||||
getattr(data, "match_date_ms", None), int(time.time() * 1000)
|
||||
)
|
||||
if minute is None:
|
||||
return None
|
||||
try:
|
||||
return build_live_projection(
|
||||
float(ms["probs"]["1"]),
|
||||
float(ms["probs"]["X"]),
|
||||
float(ms["probs"]["2"]),
|
||||
float(ou["probs"]["over"]),
|
||||
int(data.current_score_home or 0),
|
||||
int(data.current_score_away or 0),
|
||||
minute,
|
||||
)
|
||||
except (KeyError, TypeError, ValueError, ZeroDivisionError, OverflowError):
|
||||
return None
|
||||
|
||||
def _build_market_rows(
|
||||
self,
|
||||
data: MatchData,
|
||||
|
||||
Reference in New Issue
Block a user