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

This commit is contained in:
2026-05-29 11:59:51 +03:00
parent 659110c806
commit b5cb412236
4 changed files with 736 additions and 64 deletions
@@ -58,6 +58,32 @@ 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
# ── V30: Post-calibration trust factors ─────────────────────────────
# Controls how much to trust isotonic calibrator vs raw model output.
# trust=1.0 → use calibrator fully; trust=0.0 → bypass, use raw model.
# Derived from calibrator_metrics.json analysis (mean_predicted vs mean_actual):
# MS calibrators: gap < 0.5% → excellent, full trust
# BTTS: gap = +14.4% → calibrator broken, bypass
# OU25: gap = +5.3% → over-inflates, mostly bypass
# OU35: gap = +3.6% → moderate inflation, dampen
# OU15: gap = +1.5% → slight, mostly trust
# HT: mixed → moderate trust
# DC/HT_FT: < 30 samples → unreliable, bypass
POST_CAL_TRUST: Dict[str, float] = {
"ms_home": 1.0,
"ms_draw": 1.0,
"ms_away": 1.0,
"btts": 0.0,
"ou25": 0.15,
"ou35": 0.30,
"ou15": 0.70,
"ht_home": 0.50,
"ht_draw": 0.30,
"ht_away": 0.50,
"dc": 0.0,
"ht_ft": 0.0,
}
class MarketBoardMixin:
def _build_prediction_package(
@@ -1114,10 +1140,19 @@ class MarketBoardMixin:
if cal_key and cal_key in calibrator.calibrators:
cal_input = max(0.001, min(0.999, raw_conf / 100.0))
cal_prob = calibrator.calibrate(cal_key, cal_input, odds_val=odd if odd > 1.0 else None)
# V30: Trust-based blending — some calibrators inflate probabilities.
# Blend isotonic output with raw model based on calibrator accuracy.
trust = POST_CAL_TRUST.get(cal_key, 0.5)
cal_prob = trust * cal_prob + (1.0 - trust) * cal_input
calibrated_conf = max(1.0, min(99.0, cal_prob * 100.0))
else:
multiplier = self.market_calibration.get(market, 0.85)
calibrated_conf = max(1.0, min(99.0, raw_conf * multiplier))
# V31b: Fallback for markets WITHOUT isotonic calibrator.
# Old approach used aggressive multipliers (0.58-0.85) causing
# massive deflation: HT_OU15 -40.5%, HT_OU05 -25.2%, OE -18.3%.
# New approach: mild damping (0.92) acknowledges slight model
# overconfidence without destroying probability signal.
# The tier system (V31b) is the real profitability gatekeeper.
calibrated_conf = max(1.0, min(99.0, raw_conf * 0.92))
min_conf = self.market_min_conf.get(market, 55.0)
implied_prob = (1.0 / odd) if odd > 1.0 else 0.0
@@ -1178,9 +1213,11 @@ class MarketBoardMixin:
reasons: List[str] = []
playable = True
# V34: Broadened value_sniper bypass — odds-aware model rarely shows 3% EV edge
# Allow high-confidence predictions OR modest positive EV to bypass secondary gates
is_value_sniper = ev_edge >= 0.008 or calibrated_conf >= 55.0
# V29b: Permissive upstream — let betting_brain's tiered system do the real filtering.
# Old threshold (ev>=0.008 OR conf>=55) let everything through AND bypassed brain vetoes.
# New approach: let most picks through market_board, but brain's MARKET_ODDS_TIERS
# + hard vetoes (neg EV, muted, low reliability) handle the intelligent filtering.
is_value_sniper = calibrated_conf >= 45.0
if calibrated_conf < min_conf:
if not is_value_sniper:
@@ -1283,11 +1320,49 @@ class MarketBoardMixin:
stake_units = 0.25 # minimum stake (conservative)
reasons.append("no_ev_edge_minimum_stake")
# ── V30: Birleşik Güven Skoru (BGS) ────────────────────────────
# A single, honest metric for users: quality-adjusted win probability.
# Combines calibrated probability with data quality signals.
# Correlation analysis: model_gap r=-0.12, trap negative, reliability weak positive.
bgs = calibrated_conf # POST_CAL_TRUST corrected base
model_gap = prob - implied_prob if implied_prob > 0 else 0.0
# Penalty when model overestimates vs market (r=-0.12 correlation)
if model_gap > 0.05:
bgs -= 8.0
elif model_gap > 0.0:
bgs -= 3.0
# Trap market detection: implied prob significantly above historical band rate
is_trap_signal = False
if band_available and band_prob > 0 and implied_prob > 0:
is_trap_signal = (implied_prob - band_prob) > 0.10
if is_trap_signal:
bgs -= 7.0
# League reliability adjustment (±2)
bgs += (odds_rel - 0.50) * 4.0
# Band alignment
if band_available:
if bool(band_verdict.get("aligned")):
bgs += 2.0
else:
bgs -= 3.0
# BGS label for frontend
bgs = max(1.0, min(99.0, bgs))
if bgs >= 70:
bgs_label = "very_reliable"
elif bgs >= 55:
bgs_label = "reliable"
elif bgs >= 40:
bgs_label = "moderate"
else:
bgs_label = "low"
out = dict(row)
out.update(
{
"raw_confidence": round(raw_conf, 1),
"calibrated_confidence": round(calibrated_conf, 1),
"unified_score": round(bgs, 1),
"unified_score_label": bgs_label,
"min_required_confidence": round(min_conf, 1),
"min_required_play_score": round(min_play_score, 1),
"min_required_edge": round(min_edge, 4),
@@ -1347,6 +1422,8 @@ class MarketBoardMixin:
"pick": row.get("pick"),
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
"calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
"unified_score": row.get("unified_score", row.get("calibrated_confidence", 0.0)),
"unified_score_label": row.get("unified_score_label", "moderate"),
"bet_grade": row.get("bet_grade", "PASS"),
"playable": bool(row.get("playable")),
"stake_units": float(row.get("stake_units", 0.0)),