vv
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m7s

This commit is contained in:
2026-06-02 03:37:00 +03:00
parent 671979b07d
commit 4e563e996e
10 changed files with 708 additions and 0 deletions
+134
View File
@@ -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) ──
+60
View File
@@ -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 {}