gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m4s

This commit is contained in:
2026-06-10 03:01:33 +03:00
parent c3e44ee697
commit b62a4f2161
27 changed files with 366 additions and 4540 deletions
@@ -57,6 +57,7 @@ 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
# ── V30: Post-calibration trust factors ─────────────────────────────
# Controls how much to trust isotonic calibrator vs raw model output.
@@ -348,6 +349,22 @@ class MarketBoardMixin:
if market in available_markets
}
# V35: anchor the DISPLAYED per-market probabilities to the de-vigged
# market price (+ proven home-favourite correction). The model's own
# numbers were measured ~25-30% mis-calibrated; the de-vigged market is
# ~1.5% (out-of-sample). This only rewrites what the user sees.
market_board = self._apply_market_anchor(market_board, data)
# V35b: make the DISPLAYED confidence/edge fields on every pick object
# consistent with the calibrated board (Güven Skoru, Güven Aralığı,
# Model%/Teorik-avantaj), then drop a "value pick" that has no real edge
# once priced honestly — no fabricated value bets.
self._apply_anchor_to_picks(
market_board, main_pick, value_pick, aggressive_pick, supporting, bet_summary,
)
if value_pick is not None and float(value_pick.get("ev_edge", 0.0) or 0.0) <= 0.0:
value_pick = None
# Determine simulation mode for the response
_resp_status = str(data.status or "").upper()
_resp_state = str(data.state or "").upper()
@@ -1017,6 +1034,209 @@ class MarketBoardMixin:
}
return merged
# ── V35 market-anchored calibration ────────────────────────────────
# Maps a board pick label -> the probs key it refers to, so the displayed
# confidence can be set to the EXISTING pick's now-calibrated probability.
_ANCHOR_PICK_KEY: Dict[str, Dict[str, str]] = {
"MS": {"1": "1", "X": "X", "0": "X", "2": "2"},
"HT": {"1": "1", "X": "X", "0": "X", "2": "2"},
"DC": {"1X": "1X", "X2": "X2", "12": "12",
"1-X": "1X", "X-2": "X2", "1-2": "12"},
"OU15": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
"OU25": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
"OU35": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
"HT_OU05": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
"HT_OU15": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
"BTTS": {"KG Var": "yes", "KG Yok": "no", "Var": "yes", "Yok": "no",
"Yes": "yes", "No": "no"},
"OE": {"Tek": "odd", "Çift": "even", "Odd": "odd", "Even": "even"},
}
def _set_board(
self,
market_board: Dict[str, Any],
market: str,
probs: Dict[str, float],
) -> None:
"""Overwrite one board entry's probs with calibrated values and refresh
its confidence to the EXISTING pick's now-calibrated probability.
We recalibrate the NUMBERS, not the pick selection — showing the engine's
pick alongside its honest probability. Falls back to the most-likely
outcome only when the pick can't be mapped."""
entry = market_board.get(market)
if not isinstance(entry, dict):
return
rounded = {k: round(float(v), 4) for k, v in probs.items()}
if not rounded:
return
entry["probs"] = rounded
pick = str(entry.get("pick") or "")
key = self._ANCHOR_PICK_KEY.get(market, {}).get(pick)
if key is None or key not in rounded:
key = max(rounded, key=rounded.get)
entry["confidence"] = round(rounded[key] * 100.0, 1)
entry["calibration_source"] = "market_anchor_v35"
def _apply_market_anchor(
self,
market_board: Dict[str, Any],
data: MatchData,
) -> Dict[str, Any]:
"""Anchor DISPLAYED per-market probabilities to the de-vigged market
price (+ proven home-favourite correction for MS, and DC derived from
it for internal consistency).
Only markets with REAL odds are rewritten — `devig` returns None for any
missing/placeholder leg, so no-odds markets are left untouched (and are
already dropped upstream per the product rule: never show fabricated
numbers for a match without odds). Toggle off with env MARKET_ANCHOR_CAL=0.
"""
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
return market_board
if not isinstance(market_board, dict) or not market_board:
return market_board
odds = getattr(data, "odds_data", None) or {}
def real(key: str) -> Optional[float]:
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 = devig([real("ms_h"), real("ms_d"), real("ms_a")])
if ms is not None:
p1, px, p2 = apply_home_correction(*ms)
if "MS" in market_board:
self._set_board(market_board, "MS", {"1": p1, "X": px, "2": p2})
if "DC" in market_board:
self._set_board(
market_board, "DC",
{"1X": p1 + px, "X2": px + p2, "12": p1 + p2},
)
# HT (3-way)
ht = devig([real("ht_h"), real("ht_d"), real("ht_a")])
if ht is not None and "HT" in market_board:
self._set_board(market_board, "HT", {"1": ht[0], "X": ht[1], "2": ht[2]})
# 2-way markets
for mk, ko, ku, lo, lu in (
("OU15", "ou15_o", "ou15_u", "over", "under"),
("OU25", "ou25_o", "ou25_u", "over", "under"),
("OU35", "ou35_o", "ou35_u", "over", "under"),
("BTTS", "btts_y", "btts_n", "yes", "no"),
("OE", "oe_odd", "oe_even", "odd", "even"),
("HT_OU05", "ht_ou05_o", "ht_ou05_u", "over", "under"),
("HT_OU15", "ht_ou15_o", "ht_ou15_u", "over", "under"),
):
if mk not in market_board:
continue
pair = devig([real(ko), real(ku)])
if pair is not None:
self._set_board(market_board, mk, {lo: pair[0], lu: pair[1]})
return market_board
def _anchored_prob_for(
self,
market_board: Dict[str, Any],
market: str,
pick: Any,
) -> Optional[float]:
"""Look up a pick's calibrated probability from the anchored board.
Returns None unless the market was actually anchored (real odds) and the
pick maps to a known outcome — so no-odds picks are never touched."""
entry = market_board.get(str(market or ""))
if not isinstance(entry, dict):
return None
if entry.get("calibration_source") != "market_anchor_v35":
return None
probs = entry.get("probs") or {}
key = self._ANCHOR_PICK_KEY.get(str(market or ""), {}).get(str(pick or ""))
if key is None or key not in probs:
return None
try:
return float(probs[key])
except (TypeError, ValueError):
return None
def _recalibrate_pick_display(
self,
obj: Optional[Dict[str, Any]],
market_board: Dict[str, Any],
) -> None:
"""Rewrite ONE pick object's displayed confidence/edge fields so they are
consistent with the calibrated (de-vigged market) probability.
Fixes Güven Skoru (`calibrated_confidence`/`unified_score`), Güven Aralığı
(`confidence_interval` recentred on the calibrated confidence), and the
value card's Model%/Teorik-avantaj (`model_probability`/`ev_edge`/`edge`,
recomputed honestly against the real price → the vig shows as it truly is,
no fabricated positive edge). Selection/gates/stake are left untouched."""
if not isinstance(obj, dict):
return
p = self._anchored_prob_for(market_board, obj.get("market"), obj.get("pick"))
if p is None:
return
try:
odds = float(obj.get("odds") or 0.0)
except (TypeError, ValueError):
odds = 0.0
implied = (1.0 / odds) if odds > 1.0 else 0.0
conf = round(p * 100.0, 1)
ev = round(p * odds - 1.0, 4) if odds > 1.0 else 0.0
obj["calibrated_probability"] = round(p, 4)
obj["model_probability"] = round(p, 4)
obj["calibrated_confidence"] = conf
obj["unified_score"] = conf
obj["implied_prob"] = round(implied, 4)
obj["model_edge"] = round(p - implied, 4) if implied > 0.0 else 0.0
obj["ev_edge"] = ev
obj["edge"] = ev
# Recentre the confidence interval on the calibrated confidence, keeping a
# sensible width (preserve the engine's width when present).
width = 16.0
ci = obj.get("confidence_interval")
if isinstance(ci, dict) and ci.get("lower") is not None and ci.get("upper") is not None:
try:
width = max(6.0, float(ci["upper"]) - float(ci["lower"]))
except (TypeError, ValueError):
width = 16.0
half = width / 2.0
lower = round(max(0.0, conf - half), 1)
upper = round(min(100.0, conf + half), 1)
band = "HIGH" if conf >= 60.0 else "MEDIUM" if conf >= 42.0 else "LOW"
obj["confidence_interval"] = {
"band": band,
"lower": lower,
"upper": upper,
"width": round(upper - lower, 1),
"threshold_met": conf >= 50.0,
}
obj["confidence_band"] = band
obj["calibration_source"] = "market_anchor_v35"
def _apply_anchor_to_picks(
self,
market_board: Dict[str, Any],
main_pick: Optional[Dict[str, Any]],
value_pick: Optional[Dict[str, Any]],
aggressive_pick: Optional[Dict[str, Any]],
supporting: Optional[List[Dict[str, Any]]],
bet_summary: Optional[List[Dict[str, Any]]],
) -> None:
"""Make every DISPLAYED pick object consistent with the anchored board.
Toggle off with env MARKET_ANCHOR_CAL=0."""
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
return
for obj in (main_pick, value_pick, aggressive_pick):
self._recalibrate_pick_display(obj, market_board)
for obj in list(supporting or []):
self._recalibrate_pick_display(obj, market_board)
for obj in list(bet_summary or []):
self._recalibrate_pick_display(obj, market_board)
def _build_market_rows(
self,
data: MatchData,