wow
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m7s

This commit is contained in:
2026-06-11 00:25:45 +03:00
parent bb911176df
commit 4c137fbab6
9 changed files with 1246 additions and 6 deletions
@@ -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,