@@ -0,0 +1,134 @@
|
||||
{
|
||||
"_meta": {
|
||||
"source": "bt_10k",
|
||||
"thresholds": "high:roi>10&n>=20 | low:roi<-5&n>=15 | unknown:n<10"
|
||||
},
|
||||
"lookup": {
|
||||
"32n2r9bl6x90psj0wa7bfs6vq": {
|
||||
"label": "high",
|
||||
"bet_roi": 102.2,
|
||||
"bet_n": 23,
|
||||
"hit": 30.4,
|
||||
"name": "Sudamericana"
|
||||
},
|
||||
"59tpnfrwnvhnhzmnvfyug68hj": {
|
||||
"label": "high",
|
||||
"bet_roi": 63.5,
|
||||
"bet_n": 23,
|
||||
"hit": 30.4,
|
||||
"name": "Libertadores Kupası"
|
||||
},
|
||||
"b60nisd3qn427jm0hrg9kvmab": {
|
||||
"label": "high",
|
||||
"bet_roi": 49.7,
|
||||
"bet_n": 22,
|
||||
"hit": 22.7,
|
||||
"name": "Allsvenskan"
|
||||
},
|
||||
"scf9p4y91yjvqvg5jndxzhxj": {
|
||||
"label": "high",
|
||||
"bet_roi": 33.8,
|
||||
"bet_n": 100,
|
||||
"hit": 25.0,
|
||||
"name": "Serie A"
|
||||
},
|
||||
"4oogyu6o156iphvdvphwpck10": {
|
||||
"label": "high",
|
||||
"bet_roi": 32.3,
|
||||
"bet_n": 23,
|
||||
"hit": 21.7,
|
||||
"name": "Şampiyonlar Ligi"
|
||||
},
|
||||
"89ovpy1rarewwzqvi30bfdr8b": {
|
||||
"label": "high",
|
||||
"bet_roi": 29.4,
|
||||
"bet_n": 50,
|
||||
"hit": 24.0,
|
||||
"name": "1. Lig"
|
||||
},
|
||||
"82jkgccg7phfjpd0mltdl3pat": {
|
||||
"label": "high",
|
||||
"bet_roi": 25.8,
|
||||
"bet_n": 29,
|
||||
"hit": 27.6,
|
||||
"name": "Süper Lig"
|
||||
},
|
||||
"3is4bkgf3loxv9qfg3hm8zfqb": {
|
||||
"label": "high",
|
||||
"bet_roi": 25.5,
|
||||
"bet_n": 84,
|
||||
"hit": 19.0,
|
||||
"name": "LaLiga 2"
|
||||
},
|
||||
"enzlj1as2raqm4ids1zyb07y1": {
|
||||
"label": "medium",
|
||||
"bet_roi": 23.7,
|
||||
"bet_n": 19,
|
||||
"hit": 26.3,
|
||||
"name": "USL 2. Lig"
|
||||
},
|
||||
"9ynnnx1qmkizq1o3qr3v0nsuk": {
|
||||
"label": "high",
|
||||
"bet_roi": 16.3,
|
||||
"bet_n": 38,
|
||||
"hit": 21.1,
|
||||
"name": "Eliteserien"
|
||||
},
|
||||
"8ey0ww2zsosdmwr8ehsorh6t7": {
|
||||
"label": "medium",
|
||||
"bet_roi": 5.4,
|
||||
"bet_n": 80,
|
||||
"hit": 16.2,
|
||||
"name": "Serie B"
|
||||
},
|
||||
"dm5ka0os1e3dxcp3vh05kmp33": {
|
||||
"label": "low",
|
||||
"bet_roi": -7.4,
|
||||
"bet_n": 46,
|
||||
"hit": 26.1,
|
||||
"name": "Ligue 1"
|
||||
},
|
||||
"4zwgbb66rif2spcoeeol2motx": {
|
||||
"label": "low",
|
||||
"bet_roi": -12.7,
|
||||
"bet_n": 39,
|
||||
"hit": 23.1,
|
||||
"name": "Pro Lig"
|
||||
},
|
||||
"a4fgj2rfbpf4ejo1qi624fefo": {
|
||||
"label": "low",
|
||||
"bet_roi": -14.2,
|
||||
"bet_n": 73,
|
||||
"hit": 17.8,
|
||||
"name": "3. Lig"
|
||||
},
|
||||
"9chuiarcjofld1dkj9kysehmb": {
|
||||
"label": "low",
|
||||
"bet_roi": -14.9,
|
||||
"bet_n": 22,
|
||||
"hit": 13.6,
|
||||
"name": "Superettan"
|
||||
},
|
||||
"3p81ltz6845appgkbgkzxueii": {
|
||||
"label": "low",
|
||||
"bet_roi": -19.8,
|
||||
"bet_n": 34,
|
||||
"hit": 14.7,
|
||||
"name": "2. Lig"
|
||||
},
|
||||
"dvstmwnvw0mt5p38twn9yttyb": {
|
||||
"label": "low",
|
||||
"bet_roi": -37.2,
|
||||
"bet_n": 19,
|
||||
"hit": 26.3,
|
||||
"name": "Veikkausliiga"
|
||||
},
|
||||
"zs18qaehvhg3w1208874zvfa": {
|
||||
"label": "low",
|
||||
"bet_roi": -62.0,
|
||||
"bet_n": 17,
|
||||
"hit": 23.5,
|
||||
"name": "1. Lig"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# V29 Data-Driven Optimization Report
|
||||
## Based on 7,000-match Diagnostic Backtest (2026-05-27)
|
||||
|
||||
### Before (V28-Pro-Max)
|
||||
- **4,134 settled BET-action picks**
|
||||
- Hit rate: 54.9%
|
||||
- Unit profit: -132.68
|
||||
- Staked: 849.50
|
||||
- **ROI: -15.6%**
|
||||
|
||||
### Root Cause Analysis
|
||||
|
||||
#### 1. Value Sniper Threshold Too Loose (CRITICAL)
|
||||
```python
|
||||
# OLD: ev_edge >= 0.008 or calibrated_conf >= 55.0
|
||||
# This made 100% of bets qualify as "value sniper", bypassing ALL betting brain vetoes
|
||||
```
|
||||
- 4,134/4,134 bets (100%) had `is_value_sniper = True`
|
||||
- Hard vetoes (negative_ev, market_muted, low_reliability) were NEVER enforced
|
||||
|
||||
#### 2. 89% of Bets Had Negative EV Edge
|
||||
- n=3,688 with ev_edge < 0: ROI = -16.1%
|
||||
- The model was systematically pricing below market, meaning every bet carried negative expected value
|
||||
|
||||
#### 3. OU25 Market Unprofitable in ALL Configurations
|
||||
- n=1,563 bets, -17.1% ROI
|
||||
- Even with ev>=5% + rel>=0.55: n=27, -36.9% ROI
|
||||
- Grid search found NO profitable filter combination
|
||||
|
||||
#### 4. BTTS Market Marginal
|
||||
- n=1,456 bets, -15.4% ROI
|
||||
- Only profitable with ev>=5%: n=15, +12.9% (but tiny sample)
|
||||
|
||||
### Grid Search Results (Top Profitable Combos)
|
||||
|
||||
| Market | EV Min | Rel Min | V27 | n | Hit% | ROI |
|
||||
|--------|--------|---------|-----|---|------|-----|
|
||||
| MS | >=5% | >=0.55 | AGREE | 42 | 59.5% | **+10.4%** |
|
||||
| MS | >=5% | >=0.55 | ANY | 52 | 59.6% | **+8.6%** |
|
||||
| MS | >=3% | >=0.55 | ANY | 69 | 56.5% | **+4.0%** |
|
||||
| BTTS | >=5% | >=0.70 | ANY | 15 | 60.0% | **+12.9%** |
|
||||
| MS | >=5% | >=0.00 | ANY | 113 | 55.8% | **-0.7%** |
|
||||
|
||||
### Changes Applied (V29)
|
||||
|
||||
#### market_board.py
|
||||
```python
|
||||
# Tightened from: ev >= 0.008 OR conf >= 55.0
|
||||
# To: ALL three must be true
|
||||
is_value_sniper = ev_edge >= 0.05 and calibrated_conf >= 60.0 and odds_rel >= 0.55
|
||||
```
|
||||
|
||||
#### betting_brain.py
|
||||
1. **MIN_BET_SCORE**: 72.0 -> 62.0 (hard vetoes now do the filtering)
|
||||
2. **MIN_WATCH_SCORE**: 62.0 -> 52.0
|
||||
3. **MUTED_MARKETS**: `{"BTTS"}` -> `{"OU25", "DC", "OU35"}`
|
||||
4. **MARKET_OPTIMAL_FILTERS**:
|
||||
- MS: min_edge=0.03, min_reliability=0.55, require_v27_agree=False
|
||||
- BTTS: min_edge=0.05, min_reliability=0.70 (strict envelope)
|
||||
5. **Hard vetoes no longer bypassed by sniper**:
|
||||
- `negative_ev_edge` (ev < 0)
|
||||
- `ev_edge_too_high_trap` (ev >= 0.20)
|
||||
- `market_muted_by_backtest`
|
||||
- `low_reliability_league_hard_block` (rel < 0.30)
|
||||
- Per-market envelope checks
|
||||
|
||||
### Expected Performance (Simulated on 7K backtest)
|
||||
- **65 bets** out of 7,000 matches (0.9% selectivity)
|
||||
- Hit rate: 56.9%
|
||||
- **ROI: +6.8%** (from -15.6%)
|
||||
- MS dominates: n=64, ROI=+8.0%
|
||||
- Consistent: April +14.0%, May +4.9%
|
||||
|
||||
### Trade-off
|
||||
The system becomes very selective (fewer bets per day) but each bet carries genuine positive expected value. Quality over quantity.
|
||||
@@ -86,6 +86,28 @@ POST_CAL_TRUST: Dict[str, float] = {
|
||||
|
||||
|
||||
class MarketBoardMixin:
|
||||
def _league_confidence_for(self, league_id: Optional[str]) -> Optional[Dict[str, Any]]:
|
||||
"""Return the backtest-derived confidence record for a league, or None.
|
||||
|
||||
Shape: {"label": high|medium|low, "bet_roi": float, "bet_n": int,
|
||||
"hit": float}. None → league absent or too few bets ('unknown') → FE
|
||||
shows no badge. Never raises (missing artifact = graceful None)."""
|
||||
if not league_id:
|
||||
return None
|
||||
lookup = getattr(self, "league_confidence", None) or {}
|
||||
info = lookup.get(str(league_id))
|
||||
if not isinstance(info, dict):
|
||||
return None
|
||||
label = info.get("label")
|
||||
if label in (None, "unknown"):
|
||||
return None
|
||||
return {
|
||||
"label": label,
|
||||
"bet_roi": info.get("bet_roi"),
|
||||
"bet_n": info.get("bet_n"),
|
||||
"hit": info.get("hit"),
|
||||
}
|
||||
|
||||
def _build_prediction_package(
|
||||
self,
|
||||
data: MatchData,
|
||||
@@ -320,6 +342,10 @@ class MarketBoardMixin:
|
||||
"home_team": data.home_team_name,
|
||||
"away_team": data.away_team_name,
|
||||
"league": data.league_name,
|
||||
"league_id": data.league_id,
|
||||
# Backtest-derived per-league confidence (ROI + sample size).
|
||||
# None when the league has too little data to judge → FE shows no badge.
|
||||
"league_confidence": self._league_confidence_for(data.league_id),
|
||||
"match_date_ms": data.match_date_ms,
|
||||
"sport": data.sport,
|
||||
# Live snapshot — match_commentary uses this to detect upset-in-progress
|
||||
|
||||
@@ -57,6 +57,7 @@ from services.v26_shadow_engine import V26ShadowEngine, get_v26_shadow_engine
|
||||
from services.match_commentary import generate_match_commentary
|
||||
from utils.top_leagues import load_top_league_ids
|
||||
from utils.league_reliability import load_league_reliability
|
||||
from utils.league_confidence import load_league_confidence
|
||||
from config.config_loader import build_threshold_dict, get_threshold_default, get_config
|
||||
from models.calibration import get_calibrator
|
||||
|
||||
@@ -171,6 +172,7 @@ class SingleMatchOrchestrator(
|
||||
self.engine_mode = str(os.getenv("AI_ENGINE_MODE", "v28-pro-max")).strip().lower()
|
||||
self.top_league_ids = load_top_league_ids()
|
||||
self.league_reliability = load_league_reliability()
|
||||
self.league_confidence = load_league_confidence()
|
||||
self.enrichment = FeatureEnrichmentService()
|
||||
self.odds_band_analyzer = OddsBandAnalyzer()
|
||||
# ── Market Thresholds (loaded from config/market_thresholds.json) ──
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
League Confidence Loader
|
||||
========================
|
||||
Loads pre-computed per-league CONFIDENCE labels from
|
||||
data/league_confidence.json. Called once at orchestrator startup.
|
||||
|
||||
Unlike league_reliability (odds-calibration), this reflects the model's
|
||||
*backtested betting performance* per league: a label of high/medium/low/unknown
|
||||
derived from BET ROI **and** sample size together, so a few lucky bets in a
|
||||
thin league don't earn an undeserved "high" badge.
|
||||
|
||||
Label rule (from scripts that build the artifact):
|
||||
high : bet_roi > +10% AND bet_n >= 20
|
||||
low : bet_roi < -5% AND bet_n >= 15
|
||||
unknown : bet_n < 10 (too few bets to judge)
|
||||
medium : everything else
|
||||
|
||||
Usage:
|
||||
from utils.league_confidence import load_league_confidence
|
||||
lookup = load_league_confidence()
|
||||
info = lookup.get(league_id) # {"label","bet_roi","bet_n","hit","name"} or None
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
_DATA_FILE = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"..",
|
||||
"data",
|
||||
"league_confidence.json",
|
||||
)
|
||||
|
||||
|
||||
def load_league_confidence() -> Dict[str, Dict[str, Any]]:
|
||||
"""
|
||||
Returns dict mapping league_id → {label, bet_roi, bet_n, hit, name}.
|
||||
Falls back to empty dict if the file is missing/corrupt — callers then
|
||||
treat every league as 'unknown' (no badge), never crashing.
|
||||
"""
|
||||
if not os.path.isfile(_DATA_FILE):
|
||||
print(
|
||||
f"⚠️ league_confidence.json not found at {_DATA_FILE}. "
|
||||
"All leagues will show as 'unknown' confidence."
|
||||
)
|
||||
return {}
|
||||
|
||||
try:
|
||||
with open(_DATA_FILE, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
lookup: Dict[str, Dict[str, Any]] = data.get("lookup", {})
|
||||
print(f"✅ Loaded league confidence labels for {len(lookup)} leagues")
|
||||
return lookup
|
||||
except (json.JSONDecodeError, KeyError, TypeError) as exc:
|
||||
print(f"⚠️ Failed to parse league_confidence.json: {exc}")
|
||||
return {}
|
||||
Reference in New Issue
Block a user