This commit is contained in:
@@ -41,6 +41,23 @@ class BettingBrain:
|
||||
VALUE_TIER_STAKE_UNITS = 0.5
|
||||
TRAP_MARKET_GAP = 0.10
|
||||
|
||||
# ── V31f: NATIONAL-TEAM REGIME ───────────────────────────────────────
|
||||
# National matches behave nothing like clubs (2300-match backtest):
|
||||
# * Only MS carries edge — OU/BTTS/HT/DC/OE all -12%..-21% → hard mute.
|
||||
# * MS edge lives in the 4.0–7.0 odds band for HAZIRLIK/ELEME fixtures
|
||||
# (+17% ROI, stable across older/newer halves: +22%/+24%).
|
||||
# * Favorites (odds<3) lose (-10..-18%); TURNUVA inverts the pattern
|
||||
# (4-7 band is -9% there) → tournaments get NO bet (analysis only).
|
||||
# Calibration is fine; this is a *bet-selection* gate, applied only when
|
||||
# match_info.is_national is True. Clubs are completely unaffected.
|
||||
# See mds/national-team-strategy.md.
|
||||
NATIONAL_BET_MARKET = "MS"
|
||||
NATIONAL_MIN_ODDS = 4.0
|
||||
NATIONAL_MAX_ODDS = 7.0
|
||||
NATIONAL_ALLOWED_COMPETITIONS = {"HAZIRLIK", "ELEME"}
|
||||
NATIONAL_BASE_SCORE = 66.0 # clears MIN_BET_SCORE(62) when gate passes
|
||||
NATIONAL_STAKE_UNITS = 0.5 # flat, high-variance band (~24% hit)
|
||||
|
||||
MARKET_MIN_CONFIDENCE = {
|
||||
"MS": 45.0,
|
||||
"DC": 55.0,
|
||||
@@ -366,7 +383,7 @@ class BettingBrain:
|
||||
|
||||
rejected = [d for d in decisions if d.get("action") == "REJECT"]
|
||||
guarded["betting_brain"] = {
|
||||
"version": "judge-v31d-evidence-tiers",
|
||||
"version": "judge-v31f-national-regime",
|
||||
"decision": decision,
|
||||
"reason": decision_reason,
|
||||
"main_pick_key": main_key or None,
|
||||
@@ -396,6 +413,10 @@ class BettingBrain:
|
||||
pick = str(row.get("pick") or "")
|
||||
model_prob = self._market_probability(row, package)
|
||||
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
||||
# V31f: national-team match flags (set by orchestrator in match_info).
|
||||
_mi = package.get("match_info") or {}
|
||||
is_national = bool(_mi.get("is_national"))
|
||||
competition_type = str(_mi.get("competition_type") or "")
|
||||
implied = (1.0 / odds) if odds > 1.0 else 0.0
|
||||
model_gap = (model_prob - implied) if model_prob is not None and implied > 0 else None
|
||||
calibrated_conf = self._safe_float(row.get("calibrated_confidence", row.get("confidence")), 0.0) or 0.0
|
||||
@@ -713,6 +734,85 @@ class BettingBrain:
|
||||
score = value_score
|
||||
positives.append(f"value_tier_{tier_value}")
|
||||
|
||||
# ── V31f: NATIONAL-TEAM REGIME (overrides club logic) ─────────────
|
||||
# For national matches the validated strategy is a narrow, mechanical
|
||||
# value gate (MS / odds 4-7 / Hazırlık+Eleme). We REPLACE the club
|
||||
# verdict so club-tuned vetoes/scores don't distort it. All the rich
|
||||
# analysis (probs, model_gap, divergence, triple) above is preserved
|
||||
# in the payload below — only action/score/stake are decided here.
|
||||
national_gate_passed = False
|
||||
if is_national:
|
||||
in_band = self.NATIONAL_MIN_ODDS <= odds <= self.NATIONAL_MAX_ODDS
|
||||
is_bet_market = market == self.NATIONAL_BET_MARKET
|
||||
comp_ok = competition_type in self.NATIONAL_ALLOWED_COMPETITIONS
|
||||
# Genuine safety vetoes still kill the bet even for national matches.
|
||||
hard_unsafe = {
|
||||
"low_reliability_league_hard_block",
|
||||
"v25_v27_hard_disagreement",
|
||||
"extreme_negative_ev",
|
||||
"htft_reversal_risk_high",
|
||||
}
|
||||
has_hard_unsafe = any(v in hard_unsafe for v in vetoes)
|
||||
|
||||
national_vetoes: List[str] = []
|
||||
if not is_bet_market:
|
||||
national_vetoes.append("national_non_ms_market_muted")
|
||||
if not comp_ok:
|
||||
national_vetoes.append("national_tournament_no_bet")
|
||||
if not in_band:
|
||||
national_vetoes.append("national_odds_outside_value_band")
|
||||
if has_hard_unsafe:
|
||||
national_vetoes.append("national_hard_safety_veto")
|
||||
|
||||
if national_vetoes:
|
||||
vetoes = national_vetoes
|
||||
action = "REJECT"
|
||||
score = min(score, 40.0)
|
||||
issues.append(f"national_gate:{competition_type or 'unknown'}")
|
||||
else:
|
||||
vetoes = []
|
||||
national_gate_passed = True
|
||||
score = self.NATIONAL_BASE_SCORE + max(-4.0, min(8.0, ev_edge * 30.0))
|
||||
score = max(0.0, min(100.0, score))
|
||||
action = "BET"
|
||||
positives.append("national_value_gate_passed")
|
||||
issues.append(f"national_gate:{competition_type}")
|
||||
# skip the club action logic below
|
||||
row["betting_brain"] = {
|
||||
"action": action,
|
||||
"score": round(score, 1),
|
||||
"summary": self._summary(action, market, pick, positives, issues, vetoes),
|
||||
"positives": positives[:5],
|
||||
"issues": issues[:6],
|
||||
"vetoes": vetoes[:6],
|
||||
"sniper_bypassed": sniper_bypassed,
|
||||
"value_tier_bypassed": [],
|
||||
"is_value_tier": False,
|
||||
"is_national": True, # V31f
|
||||
"competition_type": competition_type,
|
||||
"trap_market_flag": trap_market_flag,
|
||||
"trap_market_gap": trap_market_gap,
|
||||
"tier_label": "national_value" if national_gate_passed else None,
|
||||
"value_tier": None,
|
||||
"model_prob": round(model_prob, 4) if model_prob is not None else None,
|
||||
"implied_prob": round(implied, 4),
|
||||
"model_market_gap": round(model_gap, 4) if model_gap is not None else None,
|
||||
"v27_prob": round(v27_prob, 4) if v27_prob is not None else None,
|
||||
"divergence": round(divergence, 4) if divergence is not None else None,
|
||||
"triple_key": triple_key,
|
||||
"triple_value": triple,
|
||||
}
|
||||
if national_gate_passed:
|
||||
row["is_guaranteed"] = False # high-variance band, never "guaranteed"
|
||||
row["pick_reason"] = "national_value_gate"
|
||||
row["stake_units"] = self.NATIONAL_STAKE_UNITS
|
||||
row["bet_grade"] = "B"
|
||||
row["playable"] = True
|
||||
else:
|
||||
self._force_no_bet(row, f"betting_brain_{action.lower()}")
|
||||
self._append_reason(row, f"betting_brain_national_{action.lower()}_{round(score)}")
|
||||
return row
|
||||
|
||||
score = max(0.0, min(100.0, score))
|
||||
action = "BET"
|
||||
if vetoes:
|
||||
|
||||
Reference in New Issue
Block a user