@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user