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