@@ -12,14 +12,33 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
class BettingBrain:
|
||||
MIN_ODDS = 1.30
|
||||
MIN_BET_SCORE = 72.0
|
||||
MIN_WATCH_SCORE = 62.0
|
||||
MIN_BET_SCORE = 62.0
|
||||
MIN_WATCH_SCORE = 52.0
|
||||
MIN_BAND_SAMPLE = 8
|
||||
HARD_DIVERGENCE = 0.22
|
||||
SOFT_DIVERGENCE = 0.14
|
||||
EXTREME_MODEL_PROB = 0.85
|
||||
EXTREME_GAP = 0.30
|
||||
SNIPER_BYPASSABLE_VETOES = {"play_score_too_low"}
|
||||
# V31d: value-tier underdogs are bet on the odds-premium edge, NOT on
|
||||
# high win-probability. These two vetoes encode a favorite-picking rule
|
||||
# (demand >45% confidence) that structurally excludes every profitable
|
||||
# underdog, so we waive them when a row matches an MS value tier.
|
||||
# Genuine safety vetoes (extreme_neg_ev, ev_too_high_trap, htft_reversal
|
||||
# _risk_high, v25_v27_hard_disagreement, low_reliability_hard_block) are
|
||||
# NOT in this set and still reject.
|
||||
VALUE_TIER_BYPASSABLE_VETOES = {"calibrated_confidence_too_low", "play_score_too_low"}
|
||||
VALUE_TIER_NAMES = {"premium", "strong", "standard"}
|
||||
# V31d: value-regime score floors (replaces favorite-confidence scoring
|
||||
# for value-tier matches). premium clears MIN_BET_SCORE(62) → BET;
|
||||
# strong/standard are capped below it → WATCH (visible, not staked)
|
||||
# because the 60-day data shows those bands break even.
|
||||
VALUE_TIER_BASE_SCORE = {"premium": 70.0, "strong": 56.0, "standard": 54.0}
|
||||
# V31d: flat, small stake for value-tier underdogs. Hit rate ~20% with
|
||||
# long losing streaks (60d: up to 35 in a row) — the edge is in FREQUENCY,
|
||||
# not per-bet size. Keep stake small to survive variance. Tunable: raise
|
||||
# only if the bettor's bankroll/risk appetite allows deeper drawdowns.
|
||||
VALUE_TIER_STAKE_UNITS = 0.5
|
||||
TRAP_MARKET_GAP = 0.10
|
||||
|
||||
MARKET_MIN_CONFIDENCE = {
|
||||
@@ -39,31 +58,181 @@ class BettingBrain:
|
||||
|
||||
SNIPER_BLOCKED_MARKETS = {"HT", "HTFT", "OE", "CARDS", "HT_OU05", "HT_OU15"}
|
||||
|
||||
# Markets that lose money under every filter combination per the
|
||||
# diagnostic backtest (1000 matches). Until calibration is rebuilt for
|
||||
# these specifically, force NO_BET. Re-evaluate after each backtest run.
|
||||
MUTED_MARKETS = {"BTTS"}
|
||||
# V30: NO markets muted — backtest tüm marketlerin gerçek ROI'sini görmeli.
|
||||
# Tier sistemi zaten filtreleme yapıyor; mute etmek veri kaybına yol açar.
|
||||
MUTED_MARKETS = set()
|
||||
|
||||
# Per-market optimal filter envelopes derived from the diagnostic
|
||||
# backtest grid search (reports/filter_optimization_patch.json). Any
|
||||
# pick falling OUTSIDE this envelope is vetoed. Tightens the playable
|
||||
# band to the ROI-positive zone identified empirically.
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# V31d: KANITA DAYALI KADEMELİ DEĞER SİSTEMİ (Evidence-Based Tiers)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# User directive: show 3 quality levels so the bettor picks by risk
|
||||
# appetite ("hangi bahis hangi oranda tutuyor"). Each MS underdog tier
|
||||
# carries a `value_tier` label that propagates to the UI. Only the
|
||||
# PREMIUM band is auto-staked (BET); strong/standard surface as WATCH
|
||||
# (full analysis shown, not staked) because the data says they break
|
||||
# even — see new_gate_sim.py.
|
||||
#
|
||||
# Each entry: {min_conf, min_edge, max_edge, min_odds, max_odds,
|
||||
# min_reliability, require_v27_agree}
|
||||
MARKET_OPTIMAL_FILTERS = {
|
||||
"MS": {
|
||||
"min_edge": -0.05, "max_edge": 0.15,
|
||||
"min_odds": 1.20, "max_odds": 10.0,
|
||||
"min_reliability": 0.0, "require_v27_agree": True,
|
||||
},
|
||||
"OU25": {
|
||||
"min_edge": -1.0, "max_edge": 0.15,
|
||||
"min_odds": 1.80, "max_odds": 10.0,
|
||||
"min_reliability": 0.0, "require_v27_agree": False,
|
||||
},
|
||||
# Validated on 60-day, 72,582-settled-row multi-pick backtest
|
||||
# (ms_envelope.py + new_gate_sim.py, span 2026-04-17..05-28):
|
||||
# PREMIUM (6.0-7.5, gap≥0, protective vetoes kept):
|
||||
# 602 bets, +32.7% ROI, +39.4u, 20.6% hit, avgOdd 6.50
|
||||
# ALL 6 weeks positive (+13.7%..+52.9%); OOS(>05-24) +47.4%;
|
||||
# survives dropping top-5 wins (+24%). 14.3 bets/day.
|
||||
# STRONG (5.0-6.0, gap≥0): ~breakeven (-1%) → WATCH, not staked.
|
||||
# STANDARD (3.0-5.0, gap≥0): +0.5% breakeven → WATCH, volume zone.
|
||||
#
|
||||
# WHY 6.0-7.5 (not 6.0-50.0 as in V31c): the edge is concentrated.
|
||||
# odds 6.0-7.0 +35% | 7.0-8.0 ~breakeven | 8.0+ NEGATIVE (longshot
|
||||
# graveyard, -10..-26% ROI). The old wide premium tier let losing
|
||||
# longshots in. Above 7.5 the model's edge evaporates.
|
||||
#
|
||||
# ROOT-CAUSE FIX (the volume crisis): underdogs were structurally
|
||||
# un-bettable. Two vetoes (calibrated_confidence_too_low,
|
||||
# play_score_too_low) auto-REJECTED every dog because they demand
|
||||
# >45% model confidence — a FAVORITE-picking rule. A 6.5 dog wins
|
||||
# ~20% of the time; that IS the edge (odds premium), not a defect.
|
||||
# For value-tier matches we bypass those two vetoes and score from
|
||||
# the validated tier quality instead of win-probability. Genuine
|
||||
# protections stay: extreme_neg_ev, ev_too_high_trap, htft_reversal
|
||||
# _risk_high, v25_v27_hard_disagreement. Result: 28 → 602 staked
|
||||
# bets (22x volume), -1.6u → +39.4u profit. ALL rich analysis data
|
||||
# (market_board, v25/v27, triple_value, probs) is untouched — only
|
||||
# the `playable` flag changes.
|
||||
#
|
||||
# MULTI-LEG VERDICT (definitive): parlays DESTROY edge.
|
||||
# 1-leg +3.4% → 2-leg -32% → 3-leg -67% → 4-leg -83%.
|
||||
# System must bet SINGLES only. No combo recommendations.
|
||||
#
|
||||
# Non-MS markets: ultrastrict tiers (rarely pass BET) → info-only.
|
||||
# All non-MS configurations showed negative ROI in backtest.
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
MARKET_ODDS_TIERS = {
|
||||
# ── MS (Match Score / 1X2) — the ONLY profitable market ────────
|
||||
"MS": [
|
||||
# PREMIUM — the validated edge. 6.0-7.5 odds, model >= market.
|
||||
# 60d: 602 bets, +32.7% ROI, all weeks positive. AUTO-STAKED.
|
||||
# Low hit (~20%) → high variance; stake stays small (see _brain_stake).
|
||||
{"min_odds": 6.00, "max_odds": 7.50, "min_edge": -0.20,
|
||||
"max_edge": 0.25, "min_reliability": 0.30,
|
||||
"min_model_gap": 0.0,
|
||||
"require_v27_agree": False, "require_no_trap": False,
|
||||
"value_tier": "premium",
|
||||
"label": "ms_underdog_premium"},
|
||||
|
||||
# STRONG — 5.0-6.0. Breakeven (-1%) → WATCH (visible, not staked).
|
||||
{"min_odds": 5.00, "max_odds": 6.00, "min_edge": -0.20,
|
||||
"max_edge": 0.25, "min_reliability": 0.30,
|
||||
"min_model_gap": 0.0,
|
||||
"require_v27_agree": False, "require_no_trap": False,
|
||||
"value_tier": "strong",
|
||||
"label": "ms_underdog_strong"},
|
||||
|
||||
# STANDARD — 3.0-5.0 volume zone. +0.5% breakeven → WATCH.
|
||||
{"min_odds": 3.00, "max_odds": 5.00, "min_edge": -0.18,
|
||||
"max_edge": 0.25, "min_reliability": 0.30,
|
||||
"min_model_gap": 0.0,
|
||||
"require_v27_agree": False, "require_no_trap": False,
|
||||
"value_tier": "standard",
|
||||
"label": "ms_underdog_standard"},
|
||||
],
|
||||
|
||||
# ── Non-MS markets: visible but NOT playable ───────────────────
|
||||
# All non-MS markets showed negative ROI in 50K-row backtest.
|
||||
# Tiers exist so the model's read is surfaced (bet_summary),
|
||||
# but criteria are strict enough that almost nothing passes BET.
|
||||
# The user sees info; the system doesn't lose money on them.
|
||||
|
||||
"DC": [
|
||||
{"min_odds": 1.15, "max_odds": 1.60, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "dc_ultrastrict"},
|
||||
],
|
||||
|
||||
"OU25": [
|
||||
{"min_odds": 1.60, "max_odds": 2.20, "min_edge": 0.02,
|
||||
"max_edge": 0.10, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.03,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ou25_ultrastrict"},
|
||||
],
|
||||
|
||||
"OU35": [
|
||||
{"min_odds": 1.50, "max_odds": 2.50, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ou35_ultrastrict"},
|
||||
],
|
||||
|
||||
"BTTS": [
|
||||
{"min_odds": 1.60, "max_odds": 2.10, "min_edge": 0.02,
|
||||
"max_edge": 0.10, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.03,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "btts_ultrastrict"},
|
||||
],
|
||||
|
||||
"HT": [
|
||||
{"min_odds": 2.00, "max_odds": 3.50, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ht_ultrastrict"},
|
||||
],
|
||||
|
||||
"OU15": [
|
||||
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ou15_ultrastrict"},
|
||||
],
|
||||
|
||||
"HTFT": [
|
||||
{"min_odds": 4.00, "max_odds": 15.00, "min_edge": 0.03,
|
||||
"max_edge": 0.15, "min_reliability": 0.45,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "htft_ultrastrict"},
|
||||
],
|
||||
|
||||
"OE": [
|
||||
{"min_odds": 1.80, "max_odds": 2.10, "min_edge": 0.02,
|
||||
"max_edge": 0.08, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.03,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "oe_ultrastrict"},
|
||||
],
|
||||
|
||||
"HT_OU05": [
|
||||
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ht_ou05_ultrastrict"},
|
||||
],
|
||||
|
||||
"HT_OU15": [
|
||||
{"min_odds": 1.60, "max_odds": 3.00, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ht_ou15_ultrastrict"},
|
||||
],
|
||||
|
||||
"CARDS": [
|
||||
{"min_odds": 1.60, "max_odds": 2.50, "min_edge": 0.02,
|
||||
"max_edge": 0.10, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "cards_ultrastrict"},
|
||||
],
|
||||
}
|
||||
|
||||
# Legacy flat envelope (backward compat for markets not in tiered system)
|
||||
MARKET_OPTIMAL_FILTERS = {}
|
||||
|
||||
MARKET_PRIORS = {
|
||||
"DC": 4.0,
|
||||
"OU15": 3.0,
|
||||
@@ -197,7 +366,7 @@ class BettingBrain:
|
||||
|
||||
rejected = [d for d in decisions if d.get("action") == "REJECT"]
|
||||
guarded["betting_brain"] = {
|
||||
"version": "judge-v2-score-coherent",
|
||||
"version": "judge-v31d-evidence-tiers",
|
||||
"decision": decision,
|
||||
"reason": decision_reason,
|
||||
"main_pick_key": main_key or None,
|
||||
@@ -240,6 +409,21 @@ class BettingBrain:
|
||||
triple_is_value = bool((triple or {}).get("is_value"))
|
||||
consensus = str((package.get("v27_engine") or {}).get("consensus") or "").upper()
|
||||
|
||||
# V29c: Compute trap_market_flag early (needed by tier require_no_trap)
|
||||
trap_market_flag = False
|
||||
trap_market_gap = None
|
||||
if isinstance(triple, dict):
|
||||
_band_rate = self._safe_float(triple.get("band_rate"))
|
||||
_implied = self._safe_float(triple.get("implied_prob"))
|
||||
if (
|
||||
_band_rate is not None
|
||||
and _implied is not None
|
||||
and band_sample >= self.MIN_BAND_SAMPLE
|
||||
and (_implied - _band_rate) > self.TRAP_MARKET_GAP
|
||||
):
|
||||
trap_market_flag = True
|
||||
trap_market_gap = round(_implied - _band_rate, 4)
|
||||
|
||||
positives: List[str] = []
|
||||
issues: List[str] = []
|
||||
vetoes: List[str] = []
|
||||
@@ -256,7 +440,7 @@ class BettingBrain:
|
||||
if market in self.SNIPER_BLOCKED_MARKETS:
|
||||
is_value_sniper = False
|
||||
if is_value_sniper:
|
||||
score += 20.0
|
||||
score += 8.0 # V29b: reduced from 20, tiers do the real filtering
|
||||
positives.append("value_sniper_override")
|
||||
|
||||
score += max(0.0, min(20.0, calibrated_conf * 0.22))
|
||||
@@ -276,7 +460,7 @@ class BettingBrain:
|
||||
if odds_rel < 0.30:
|
||||
score -= 22.0
|
||||
issues.append("very_low_reliability_league")
|
||||
if market in {"MS", "DC", "OU25", "BTTS"} and not is_value_sniper:
|
||||
if market in {"MS", "DC", "OU25", "BTTS"}: # V29: hard veto, no sniper bypass
|
||||
vetoes.append("low_reliability_league_hard_block")
|
||||
elif odds_rel < 0.45:
|
||||
score -= 12.0
|
||||
@@ -305,38 +489,79 @@ class BettingBrain:
|
||||
# ev_edge < 0 = "model market'in altında olasılık veriyor" = vig'i
|
||||
# yiyemeyeceğimiz negative-EV bahis. Hard veto: oynama.
|
||||
# Sniper override hâlâ geçer (yüksek convicted alternatif pick'ler).
|
||||
if ev_edge < 0.0 and not is_value_sniper:
|
||||
vetoes.append("negative_ev_edge")
|
||||
issues.append(f"ev_edge={ev_edge:.3f}_below_zero")
|
||||
# V29b: negative_ev_edge hard veto REMOVED — tier system handles
|
||||
# edge bounds per-market via min_edge. MS underdog tier allows
|
||||
# ev >= -0.15, so a universal ev<0 veto would kill profitable bets.
|
||||
if ev_edge < -0.20: # Only veto truly extreme negative edge
|
||||
vetoes.append("extreme_negative_ev_edge")
|
||||
issues.append(f"ev_edge={ev_edge:.3f}_extreme_negative")
|
||||
# Trap edge: bizim diagnostic backtest'te ev_edge >= 0.20 olan tüm
|
||||
# bahisler kaybediyordu (n=10, hepsi -%25+ ROI). Model market'i bu
|
||||
# kadar yanlış buluyorsa muhtemelen modelin kendisinin yanlış olduğu
|
||||
# bir senaryo (eksik info, tuhaf maç, vs.) — oynama.
|
||||
if ev_edge >= 0.20 and not is_value_sniper:
|
||||
if ev_edge >= 0.30: # V29b: raised from 0.20, tiers cap at 0.25
|
||||
vetoes.append("ev_edge_too_high_trap")
|
||||
issues.append(f"ev_edge={ev_edge:.3f}_trap_range")
|
||||
|
||||
# ── MUTED MARKETS (grid search showed no profitable config) ──
|
||||
if market in self.MUTED_MARKETS and not is_value_sniper:
|
||||
if market in self.MUTED_MARKETS: # V29: hard veto, no sniper bypass
|
||||
vetoes.append("market_muted_by_backtest")
|
||||
issues.append(f"market_{market}_muted")
|
||||
|
||||
# ── PER-MARKET OPTIMAL ENVELOPE (from grid search) ──
|
||||
envelope = self.MARKET_OPTIMAL_FILTERS.get(market)
|
||||
if envelope and not is_value_sniper:
|
||||
if ev_edge < envelope["min_edge"]:
|
||||
# ── V30: ODDS-TIERED ENVELOPE (from 7K backtest grid search) ──
|
||||
# Each market has multiple odds zones with different filters.
|
||||
# If a bet doesn't fit ANY tier, it gets vetoed.
|
||||
# V30: added model_gap filtering — data shows model>market is
|
||||
# inversely correlated with winning for BTTS/OU25.
|
||||
tiers = self.MARKET_ODDS_TIERS.get(market, [])
|
||||
# Also check legacy flat envelope for backward compat
|
||||
legacy_env = self.MARKET_OPTIMAL_FILTERS.get(market)
|
||||
tier_matched = False
|
||||
tier_label = None
|
||||
tier_value = None # V31c: quality tier (premium/strong/standard)
|
||||
if tiers:
|
||||
for tier in tiers:
|
||||
if not (tier["min_odds"] <= odds <= tier["max_odds"]):
|
||||
continue
|
||||
if ev_edge < tier["min_edge"] or ev_edge > tier["max_edge"]:
|
||||
continue
|
||||
if odds_rel < tier["min_reliability"]:
|
||||
continue
|
||||
if tier.get("require_v27_agree") and consensus != "AGREE":
|
||||
continue
|
||||
if tier.get("require_no_trap") and trap_market_flag:
|
||||
continue
|
||||
# V30: model-market gap filter
|
||||
if model_gap is not None:
|
||||
if "min_model_gap" in tier and model_gap < tier["min_model_gap"]:
|
||||
continue
|
||||
if "max_model_gap" in tier and model_gap > tier["max_model_gap"]:
|
||||
continue
|
||||
tier_matched = True
|
||||
tier_label = tier.get("label")
|
||||
tier_value = tier.get("value_tier") # V31c
|
||||
break
|
||||
if not tier_matched:
|
||||
vetoes.append("outside_all_odds_tiers")
|
||||
issues.append(f"no_profitable_tier_for_{market}_at_odds_{odds:.2f}")
|
||||
elif legacy_env:
|
||||
if ev_edge < legacy_env["min_edge"]:
|
||||
vetoes.append("outside_envelope_edge_low")
|
||||
if ev_edge > envelope["max_edge"]:
|
||||
if ev_edge > legacy_env["max_edge"]:
|
||||
vetoes.append("outside_envelope_edge_high")
|
||||
if odds and odds < envelope["min_odds"]:
|
||||
if odds and odds < legacy_env["min_odds"]:
|
||||
vetoes.append("outside_envelope_odds_low")
|
||||
if odds and odds > envelope["max_odds"]:
|
||||
if odds and odds > legacy_env["max_odds"]:
|
||||
vetoes.append("outside_envelope_odds_high")
|
||||
if odds_rel < envelope["min_reliability"]:
|
||||
if odds_rel < legacy_env["min_reliability"]:
|
||||
vetoes.append("outside_envelope_reliability_low")
|
||||
if envelope["require_v27_agree"] and consensus != "AGREE":
|
||||
if legacy_env.get("require_v27_agree") and consensus != "AGREE":
|
||||
vetoes.append("outside_envelope_v27_must_agree")
|
||||
|
||||
# V31d: a matched value tier is the validated profitable signal.
|
||||
# It unlocks the value-betting regime (veto bypass + score floor).
|
||||
is_value_tier = tier_value in self.VALUE_TIER_NAMES
|
||||
|
||||
if divergence is not None:
|
||||
if divergence >= self.HARD_DIVERGENCE and not is_value_sniper:
|
||||
score -= 42.0
|
||||
@@ -348,22 +573,10 @@ class BettingBrain:
|
||||
score += 11.0
|
||||
positives.append("v25_v27_aligned")
|
||||
|
||||
# Trap market detection: market overpriced vs historical band hit rate
|
||||
trap_market_flag = False
|
||||
trap_market_gap = None
|
||||
if isinstance(triple, dict):
|
||||
band_rate_val = self._safe_float(triple.get("band_rate"))
|
||||
implied_val = self._safe_float(triple.get("implied_prob"))
|
||||
if (
|
||||
band_rate_val is not None
|
||||
and implied_val is not None
|
||||
and band_sample >= self.MIN_BAND_SAMPLE
|
||||
and (implied_val - band_rate_val) > self.TRAP_MARKET_GAP
|
||||
):
|
||||
trap_market_flag = True
|
||||
trap_market_gap = round(implied_val - band_rate_val, 4)
|
||||
score -= 14.0
|
||||
issues.append("trap_market_market_overpriced")
|
||||
# Trap market score penalty (flag computed above, before tier check)
|
||||
if trap_market_flag:
|
||||
score -= 14.0
|
||||
issues.append("trap_market_market_overpriced")
|
||||
|
||||
if isinstance(triple, dict):
|
||||
if triple_is_value:
|
||||
@@ -465,6 +678,41 @@ class BettingBrain:
|
||||
if sniper_bypassed:
|
||||
positives.append("sniper_bypassed_soft_vetoes")
|
||||
|
||||
# ── V31d: VALUE-TIER REGIME ──────────────────────────────────────
|
||||
# A matched MS value tier is the validated profitable signal (60d:
|
||||
# premium 6.0-7.5 → +32.7% ROI). Underdogs are bet on the odds
|
||||
# premium, not on win-probability, so:
|
||||
# (1) waive the two favorite-confidence vetoes (genuine safety
|
||||
# vetoes — extreme_neg_ev, ev_too_high_trap, htft_reversal
|
||||
# _risk_high, v25_v27_hard_disagreement, low_reliability_hard
|
||||
# — are NOT waived and still reject);
|
||||
# (2) replace the favorite-confidence SCORE with a value floor so
|
||||
# premium can clear MIN_BET_SCORE while strong/standard stay
|
||||
# WATCH-level. All rich analysis output is untouched.
|
||||
value_tier_bypassed: List[str] = []
|
||||
if is_value_tier:
|
||||
if vetoes:
|
||||
remaining = []
|
||||
for v in vetoes:
|
||||
if v in self.VALUE_TIER_BYPASSABLE_VETOES:
|
||||
value_tier_bypassed.append(v)
|
||||
else:
|
||||
remaining.append(v)
|
||||
vetoes = remaining
|
||||
if value_tier_bypassed:
|
||||
positives.append("value_tier_bypassed_favorite_vetoes")
|
||||
# Value-regime score: floor by tier quality + small +EV nudge.
|
||||
value_score = self.VALUE_TIER_BASE_SCORE.get(tier_value, 50.0)
|
||||
value_score += max(-5.0, min(10.0, ev_edge * 35.0))
|
||||
if odds_rel >= 0.45:
|
||||
value_score += 3.0
|
||||
# Only premium is auto-staked; cap the rest below MIN_BET_SCORE
|
||||
# so they surface as WATCH (visible analysis, not a staked bet).
|
||||
if tier_value != "premium":
|
||||
value_score = min(value_score, 60.0)
|
||||
score = value_score
|
||||
positives.append(f"value_tier_{tier_value}")
|
||||
|
||||
score = max(0.0, min(100.0, score))
|
||||
action = "BET"
|
||||
if vetoes:
|
||||
@@ -487,8 +735,12 @@ class BettingBrain:
|
||||
"issues": issues[:6],
|
||||
"vetoes": vetoes[:6],
|
||||
"sniper_bypassed": sniper_bypassed,
|
||||
"value_tier_bypassed": value_tier_bypassed, # V31d
|
||||
"is_value_tier": is_value_tier, # V31d
|
||||
"trap_market_flag": trap_market_flag,
|
||||
"trap_market_gap": trap_market_gap,
|
||||
"tier_label": tier_label,
|
||||
"value_tier": tier_value, # V31c: premium/strong/standard
|
||||
"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,
|
||||
@@ -501,10 +753,15 @@ class BettingBrain:
|
||||
if action != "BET":
|
||||
self._force_no_bet(row, f"betting_brain_{action.lower()}")
|
||||
else:
|
||||
row["is_guaranteed"] = bool(score >= 82.0)
|
||||
# V31d: value-tier underdogs are high-variance (~20% hit) — never
|
||||
# label them "guaranteed" no matter how high the value score is.
|
||||
row["is_guaranteed"] = bool(score >= 82.0) and not is_value_tier
|
||||
row["pick_reason"] = "betting_brain_approved"
|
||||
row["stake_units"] = self._brain_stake(row, score)
|
||||
row["bet_grade"] = "A" if score >= 82.0 else "B"
|
||||
# V31c: bet_grade now reflects value_tier so the UI can show
|
||||
# the bettor which quality band a pick belongs to.
|
||||
row["bet_grade"] = self._grade_from_tier(tier_value, score)
|
||||
row["value_tier"] = tier_value
|
||||
row["playable"] = True
|
||||
|
||||
self._append_reason(row, f"betting_brain_{action.lower()}_{round(score)}")
|
||||
@@ -600,12 +857,14 @@ class BettingBrain:
|
||||
|
||||
def _summary_item(self, row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
reasons = list(row.get("decision_reasons") or row.get("reasons") or [])
|
||||
brain = row.get("betting_brain") or {}
|
||||
return {
|
||||
"market": row.get("market"),
|
||||
"pick": row.get("pick"),
|
||||
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
|
||||
"calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
|
||||
"bet_grade": row.get("bet_grade", "PASS"),
|
||||
"value_tier": row.get("value_tier") or brain.get("value_tier"), # V31c
|
||||
"playable": bool(row.get("playable")),
|
||||
"stake_units": float(row.get("stake_units", 0.0) or 0.0),
|
||||
"play_score": row.get("play_score", 0.0),
|
||||
@@ -615,9 +874,22 @@ class BettingBrain:
|
||||
"odds": row.get("odds", 0.0),
|
||||
"reasons": reasons[:6],
|
||||
"is_underdog_reference": bool(row.get("is_underdog_reference")),
|
||||
"betting_brain": row.get("betting_brain"),
|
||||
"betting_brain": brain,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _grade_from_tier(value_tier: Optional[str], score: float) -> str:
|
||||
"""V31c: Map value_tier → bet grade so the UI surfaces the
|
||||
quality band. Falls back to score-based grade for untiered picks.
|
||||
premium → A (deep underdog, highest ROI, high variance)
|
||||
strong → B (strong underdog)
|
||||
standard → C (volume zone, thin edge)
|
||||
"""
|
||||
mapping = {"premium": "A", "strong": "B", "standard": "C"}
|
||||
if value_tier in mapping:
|
||||
return mapping[value_tier]
|
||||
return "A" if score >= 82.0 else "B"
|
||||
|
||||
@staticmethod
|
||||
def _candidate_sort_key(row: Dict[str, Any]) -> Tuple[float, float, float]:
|
||||
brain = row.get("betting_brain") or {}
|
||||
@@ -657,6 +929,12 @@ class BettingBrain:
|
||||
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
||||
if odds <= 1.0:
|
||||
return 0.0
|
||||
# V31d: value-tier underdogs use a small FLAT stake (high variance),
|
||||
# not the score-scaled favorite stake. score is high (70+) by design
|
||||
# but that reflects validated tier EV, not win-probability.
|
||||
brain = row.get("betting_brain") or {}
|
||||
if brain.get("is_value_tier") or brain.get("value_tier") in self.VALUE_TIER_NAMES:
|
||||
return self.VALUE_TIER_STAKE_UNITS
|
||||
cap = 2.0 if score >= 82.0 else 1.2
|
||||
if score < 78.0:
|
||||
cap = 0.8
|
||||
|
||||
Reference in New Issue
Block a user