diff --git a/ai-engine/BETTING_WORKFLOW.md b/ai-engine/BETTING_WORKFLOW.md new file mode 100644 index 0000000..a926396 --- /dev/null +++ b/ai-engine/BETTING_WORKFLOW.md @@ -0,0 +1,313 @@ +# IDDAAI — Bahis Motoru Operasyon Workflow'u (V31d) + +> Bu doküman, AI bahis tahmin motorunun **nasıl çalıştırılacağı, doğrulanacağı, +> izleneceği ve yeniden ayarlanacağına** dair operasyon kılavuzudur. +> Hedef: **hem hacim hem kâr** — gerçekçi beklenti **premium tier'da +%30 ROI**, +> daha geniş ağda +%5–15. +> +> Son güncelleme: 2026-05-29 · Judge sürümü: `judge-v31d-evidence-tiers` +> +> **V31d ne değiştirdi (hacim krizi çözümü):** V31c yalnızca **28 oynanabilir +> bahis / 10k maç** üretiyordu çünkü iki veto (`calibrated_confidence_too_low`, +> `play_score_too_low`) HER underdog'u reddediyordu — bunlar ">%45 model güveni +> iste" diyen FAVORİ-seçme kuralı. Ama kârlı bir 6.5 oran underdog'u zaten sadece +> ~%20 tutar; kâr oran priminden gelir. V31d, **MS değer-tier eşleşmelerinde** bu +> iki vetoyu kaldırır ve skoru tier kalitesinden üretir. Sonuç (60g doğrulama): +> **28 → 602 oynanabilir bahis (22x), −1.6u → +39.4u, ROI −%28 → +%32.7.** +> Tüm zengin analiz çıktısı (market_board, v25/v27, triple_value, olasılıklar) +> **aynen korunur** — yalnızca `playable` bayrağı değişir. + +--- + +## 0. TL;DR — En Önemli 5 Kural + +1. **SADECE TEKLİ BAHİS OYNA. KOMBİNE YOK.** Matematiksel olarak kanıtlandı: + 1-leg `+%3.4` → 2-leg `-%32` → 3-leg `-%67` → 4-leg `-%83`. Marjinal +EV bacakları + çarpmak kazancı yok eder. +2. **Asıl kâr MS (1X2) underdog bölgesinde.** Oran ≥ 6.0 + model_gap ≥ 0 = en yüksek ROI. +3. **Hiçbir market mute edilmez.** Tier sistemi filtreler; gerçek ROI'ler görünür kalır + (`MUTED_MARKETS = set()`). +4. **Kalibrasyon ≠ Bahis sinyali.** MS tier'ları ham model olasılığını kullanır + (`model_gap`, `ev_edge`). İzotonik kalibratörler sadece ekrandaki `calibrated_confidence`'i + etkiler (BTTS/OU25'te şişik — dikkat). +5. **Backtest'e körü körüne güvenme.** Model eğitim kesim tarihini bil; in-sample/out-of-sample + ayrımını her zaman yap (bkz. Bölüm 6). + +--- + +## 1. Sistem Mimarisi (Pipeline) + +``` +Maç verisi (DB: matches, odds, elo, form, h2h…) + │ + ▼ +[V25 Ensemble] XGBoost + LightGBM + CatBoost → her market için ham olasılık + │ + ▼ +[V27 Dual-Engine] ikinci görüş / consensus (AGREE / DISAGREE) + │ + ▼ +[İzotonik Kalibrasyon] ham olasılık → calibrated_confidence (ekran için) + └─ kalibratörü OLMAYAN marketlerde hafif damping (×0.92) + │ + ▼ +[BettingBrain V31d — Deterministik Hâkim] + ├─ ev_edge = calibrated_probability × oran − 1 (ham-prob + market blend) + ├─ model_gap = ham_model_olasılık − implied_prob + ├─ trap_market = market geçmiş banttan fazla fiyatlamış mı? + ├─ odds_reliability = lig bazında geçmiş Brier skorundan + └─ MARKET_ODDS_TIERS → value_tier (premium/strong/standard) → bet_grade (A/B/C) + │ + ▼ +[Çıktı] bet_summary[] → playable, value_tier, stake_units, bet_grade + → BE (smart-coupon) → FE / Mobile +``` + +**Anahtar dosyalar:** +- `services/betting_brain.py` — deterministik hâkim, tier tanımları (`MARKET_ODDS_TIERS`) +- `services/orchestrator/market_board.py` — ev_edge/model_gap/kalibrasyon hesapları +- `scripts/diagnostic_backtest_multi.py` — çok-pick backtest (maç başına TÜM marketler) +- `models/v25/`, `models/calibration/` — model ve kalibratör dosyaları + +--- + +## 2. V31d — Kanıta Dayalı Kademeli Değer Sistemi (Evidence-Based Tiers) + +Kullanıcı risk iştahına göre seçer. Her tier maç başına ayrı sinyal üretir. +**Sadece premium otomatik STAKE'lenir (BET); strong/standard WATCH** olarak görünür +(tam analiz gösterilir, oynanmaz) çünkü 60 günlük veri o bantların ~başabaş olduğunu +söylüyor. + +| Tier | Grade | Oran bandı | Filtre | 60g ROI* | Aksiyon | Karakter | +|------|:----:|-----------|--------|:----:|:----:|----------| +| **premium** | A | **6.00 – 7.50** | model_gap ≥ 0, rel ≥ 0.30 | **+%32.7** | **BET** | Doğrulanmış edge; ~%20 hit, yüksek varyans | +| **strong** | B | 5.00 – 6.00 | model_gap ≥ 0, rel ≥ 0.30 | ~%−1 (başabaş) | WATCH | Görünür, oynanmaz (kanıt yetersiz) | +| **standard** | C | 3.00 – 5.00 | model_gap ≥ 0, rel ≥ 0.30 | +%0.5 (başabaş) | WATCH | Hacim bölgesi, marj yok | +| info (—) | — | market’e özel | ultrastrict (min_edge≥0.02, rel≥0.45-0.55, trap yok) | ~0 | REJECT/info | Bilgi amaçlı, nadiren geçer | + +\* 60 günlük doğrulamadan (72.582 settled satır, 7.793 maç, 2026-04-17..05-28; +`ms_envelope.py` + `new_gate_sim.py`). premium: 602 bahis, +%32.7 ROI, +39.4u, +%20.6 hit, **6 haftanın 6'sı da pozitif**, OOS(>05-24) +%47.4. + +**NEDEN 6.0–7.5 (V31c'deki 6.0–50.0 değil):** edge dar bir banda yoğunlaşmış. +`6.0–7.0 +%35` · `7.0–8.0 ~başabaş` · **`8.0+ NEGATİF`** (−%10..−26, longshot mezarlığı). +Eski geniş premium tier kaybeden longshot'ları içeri alıyordu. 7.5 üstünde modelin +edge'i buharlaşıyor. + +**Tasarım mantığı:** premium = ROI **ve** hacim motoru (60g'de ~14 bahis/gün = bol hacim). +Bahisçi: +- **Düşük risk / yüksek kalite** istiyorsa → sadece **premium (A)** oyna (varsayılan). +- **Daha fazla hacim** istiyorsa → premium bandını 6.0–8.0'e genişlet (ROI +%32.7 → +%19, + hâlâ sağlam, +%44 hacim) — `MARKET_ODDS_TIERS["MS"]` premium `max_odds`'u değiştir. + +**Non-MS marketler (DC, OU25, OU35, BTTS, HT, OU15, HTFT, OE, HT_OU05, HT_OU15, CARDS):** +hepsi `ultrastrict` tek-tier ile bilgi amaçlı. Geçmiş veride sistematik olarak kayıp +verdikleri için BET üretmeleri zorlaştırıldı (mute YOK — sadece sıkı eşik). + +**Veto mantığı (V31d kritik):** value-tier eşleşmelerinde `calibrated_confidence_too_low` +ve `play_score_too_low` vetoları KALDIRILIR (bunlar favori-seçme kuralı). Ama gerçek +koruma vetoları AKTİF kalır: `extreme_negative_ev` (ev<−0.20), `ev_edge_too_high_trap` +(ev≥0.30), `htft_reversal_risk_high`, `v25_v27_hard_disagreement`, `low_reliability_hard`. +60g'de premium tier-eşleşmelerinin ~%71'i oynanabilir oldu; kalan ~%29 bu koruma +vetolarıyla doğru şekilde reddedildi. + +--- + +## 3. EN İYİ BAHİS DEĞERLERİ — Kesin Sıralama (Best Bet Values) + +> "Multi bahislerde bütün bahis değerlerinin en iyisi" sorusunun cevabı. +> **Hepsi TEKLİ oynanır.** (Aşağıdaki ROI'ler 0.2u sabit stake simülasyonundan.) + +### MS (1X2) underdog — ince oran-bandı haritası (60g, gap ≥ 0) + +> "Hangi bahis hangi oranda tutuyor" sorusunun kesin cevabı. `ms_envelope.py`. +> drop-3/5 = en büyük 3/5 kazancı çıkarınca ROI (konsantrasyon/sağlamlık testi). + +| Oran bandı | Bahis | Hit% | ROI | drop-3 ROI | Karar | +|-----------|------:|-----:|----:|-----:|:-----:| +| **6.0 – 6.5** | 469 | %22.0 | **+%37.7** | +%34.4 | ✅ elit | +| **6.0 – 7.0** | 492 | %21.5 | **+%35.2** | +%29.9 | ✅ elit, sağlam | +| **6.0 – 7.5** (premium) | 645 | %20.0 | **+%29.3** | +%24.4 | ✅ ÖNERİLEN | +| 6.0 – 8.0 | 928 | %17.7 | +%19.1 | +%15.5 | ✅ hacim opsiyonu | +| 7.5 – 8.0 | 283 | %12.4 | −%4.0 | — | ❌ | +| 8.0 – 9.0 | 78 | %9.0 | −%25.7 | — | ❌ longshot | +| 9.0+ | ~266 | <%10 | negatif | — | ❌ mezarlık | +| 5.0 – 6.0 (strong) | ~1000 | %18 | ~−%1 | — | ⚠️ başabaş → WATCH | +| 3.0 – 5.0 (standard) | ~5745 | %27 | +%0.5 | — | ⚠️ başabaş → WATCH | + +**Korumalı premium (htft/disagreement vetoları uygulanmış) = staked set:** +602 bahis · %20.6 hit · **+%32.7 ROI** · +39.4u · 6/6 hafta pozitif · OOS +%47.4. + +**Okuma:** Edge tamamen **6.0–7.5** bandında. 8.0 üstü longshot'lar kaybeder +(eski 6.0–50.0 premium tier'ı bu yüzden sulandırıyordu). 5.0 altı başabaş. +Premium tek başına ~14 bahis/gün = hem hacim hem +%32.7 ROI. + +### ❌ İşe YARAMAYAN yapılandırmalar +- **Kombine (parlay):** her ek bacak ROI'yi çökertir (yukarıdaki TL;DR). +- **MS 8.0+ longshot:** −%10..−26 ROI, model edge'i yok. +- **MS 5.0–6.0 / 3.0–5.0:** başabaş; WATCH olarak göster, stake'leme. +- **OU25 her konfigürasyon:** sistematik kayıp (60g'de OU25 −%22.8, OU35 −%17.2). +- **BTTS:** sadece çok yüksek reliability'de marjinal. + +--- + +## 4. KRİTİK KURAL — Tekli Bahis, Kombine Yok + +| Kupon tipi | Hit% | ROI | Sonuç | +|-----------|-----:|----:|:-----:| +| 1-leg (tekli) | ~%24 | **+%3.4** | ✅ | +| 2-leg | düşük | −%32.4 | ❌ | +| 3-leg | çok düşük | −%66.6 | ❌ | +| 4-leg | minimal | −%83.0 | ❌ | + +**Neden:** Tekil bacaklar yalnızca marjinal +EV. Kombine, kazanma olasılıklarını +çarparken (her biri <1) kayıp olasılığını üssel büyütür. Düz (flat) tekli stake +matematiksel olarak üstündür. **Ürün, kullanıcıyı kombineye teşvik etmemeli;** +"günün premium tekli değerleri" şeklinde sunmalı. + +--- + +## 5. Önerilen Stake Politikası + +- **Flat stake** (sabit birim) — Kelly değil. Marjinal edge'de Kelly varyansı patlatır. +- **premium (A): 0.5u sabit** (`VALUE_TIER_STAKE_UNITS`). ~%20 hit + uzun kayıp serileri + (60g'de en uzun 35 ardışık kayıp) nedeniyle KÜÇÜK tutulur — kâr **frekanstan** gelir, + bahis başı büyüklükten değil. Bankroll/risk iştahı izin veriyorsa artırılabilir. +- strong/standard WATCH = stake YOK (görünür ama oynanmaz). +- Günlük/maç başına 1 sinyal; aynı maça birden çok tier'dan bahis = korelasyon riski, + en yüksek value_tier'ı seç. +- **Drawdown uyarısı:** 0.5u'da en kötü tarihsel düşüş ≈ −34u; 35 ardışık kayıp mümkün. + Bu bir maraton stratejisidir — kısa vadeli sonuçlara göre stake değiştirme. + +--- + +## 6. Backtest Metodolojisi & Leakage Disiplini ⚠️ + +**En kritik bölüm. Backtest sayıları yanlış yorumlanırsa sistem kârlı sanılıp kaybettirir.** + +### 6.1 Komut +```bash +# Konteyner içinde: +python scripts/diagnostic_backtest_multi.py --days 60 --max-matches 10000 \ + --progress-interval 100 --checkpoint-every 200 +# Çıktı: reports/multi_backtest_YYYYMMDD.{csv,json,txt} +# Checkpoint'li → kesilirse kaldığı yerden devam eder. +``` + +### 6.2 Lookahead / Sızıntı (leakage) kontrolü — ZORUNLU +- **Feature lookahead:** ✅ temiz — feature'lar match_date ÖNCESİ veriden hesaplanıyor. +- **Model eğitim-seti üyeliği:** Bunu HER ZAMAN kontrol et. Kalibratörler + `models/calibration/*_metrics.json` içindeki `last_trained` tarihinde, son ~5000 + maç üzerinde fit edilir. Backtest penceresi bu tarihle çakışırsa **calibrated_confidence + in-sample (şişik)** olur. +- **Pratik test (ucuz):** Backtest sonucunu eğitim kesim tarihine göre ikiye böl; + in-sample vs out-of-sample hit% karşılaştır. Tüm-market hit% **neredeyse aynıysa** + (örn. %49.7 vs %49.4) → temel modellerde anlamlı sızıntı YOK, edge gerçek. + Eski veride hit% **aniden yükseliyorsa** → o dönem eğitim setinde, ROI'yi yok say. + - Hazır script: `/tmp/leakage_split.py ` (eğitim tarihine göre böler). +- **Geriye doğru ne kadar gidilebilir?** Modeller en son holdout penceresini (≈son + 10k maç ≈ 60-70 gün) eğitimden hariç tutuyor. Bu yüzden **~60 gün geriye backtest + çoğunlukla temiz holdout'tur.** Daha geriye (90+ gün) gitmek eğitim setine girip + ROI'yi yapay iyi gösterebilir → kaçın. + +### 6.3 Doğrulama scriptleri +- `/tmp/v31c_validation.py ` — V31c tier dökümü (premium/strong/standard ROI). +- `/tmp/best_bet_values.py ` — grid-search liderlik tablosu + portföy + kombine testi. +- `/tmp/leakage_split.py ` — in/out-of-sample sızıntı probu. + +### 6.4 Doğrulama eşiği (bir tier "kârlı" sayılmadan önce) +- n ≥ 50 bahis (tercihen ≥ 200), out-of-sample. +- ROI > 0 hem in- hem out-of-sample'da, ya da en azından OOS'ta çökmemiş. +- Kümülatif kâr eğrisi yukarı trend (tek bir şanslı güne bağlı değil). + +--- + +## 7. Operasyonel Döngü (Cadence) + +### Günlük +- Motor sağlık kontrolü (futbol pipeline çalışıyor mu; basketbol `readiness_summary` + hatası bilinen/zararsız). +- Günün sinyallerini üret; **premium (A) tekli** değerleri öne çıkar. +- Settle olan dünün bahislerini logla (gerçek hit/ROI takibi). + +### Haftalık +- Son 7-14 günün gerçek sonuçlarını backtest tahminiyle karşılaştır (calibration drift). +- Tier bazında gerçekleşen ROI'yi izle; standard (C) sürekli negatifse eşik sıkılaştır. + +### Aylık +- Modelleri yeniden eğit (Colab: `extract_training_data_v27.py` → eğitim → `fetch_xgb_models.sh`). +- **Yeniden eğitimden sonra MUTLAKA** 60 günlük backtest + leakage_split ile yeniden doğrula. +- Tier eşiklerini güncelle (Bölüm 8). +- `models/calibration/*_metrics.json` `last_trained` tarihini not et (bir sonraki + backtest'in OOS penceresini bilmek için). + +--- + +## 8. Tier / Eşik Güncelleme Protokolü + +1. Yeni backtest CSV'sini al → `v31c_validation.py` + `leakage_split.py` çalıştır. +2. Her tier için OOS ROI'ye bak: + - ROI sağlam pozitif + n yeterli → koru. + - ROI marjinal/negatif → oran bandını daralt veya min_reliability/min_model_gap yükselt. + - premium 6.0+ eşiği: OOS'ta hâlâ en iyi ROI mi? Değilse bandı kaydır (örn. 6.5+). +3. `betting_brain.py` → `MARKET_ODDS_TIERS` düzenle, **versiyon string'ini artır** + (`judge-v31c-…` → `judge-v31d-…`). +4. Lokal syntax kontrol → sunucuya deploy (Bölüm 9) → yeniden doğrula. +5. Tier'lar netleştikten SONRA `value_tier`'ı UI'a yay (BE smart-coupon → FE badge → mobil). + +--- + +## 9. Deploy Prosedürü (AI Engine) + +```bash +# 1. Lokal syntax kontrol +python3 -c "import ast; ast.parse(open('services/betting_brain.py').read())" + +# 2. Sunucuya kopyala (SSH: port 2222, kullanıcı haruncan) +scp -P 2222 services/betting_brain.py haruncan@:/tmp/betting_brain.py + +# 3. Konteynere koy + import testi +docker cp /tmp/betting_brain.py iddaai-ai-engine:/app/services/betting_brain.py +docker exec iddaai-ai-engine python -c "from services.betting_brain import BettingBrain; print('OK')" + +# 4. Yeniden başlat + doğrula +docker restart iddaai-ai-engine +docker exec iddaai-ai-engine python -c "from services.betting_brain import BettingBrain as B; \ + print([t['value_tier'] for t in B().MARKET_ODDS_TIERS['MS']])" +``` +> Not: Port 8000 host-localhost'a expose DEĞİL; sağlık testini konteyner içinden veya +> Docker network üzerinden yap. Basketbol `readiness_summary` hatası bilinen, bloklamıyor. + +--- + +## 10. Bilinen Sınırlamalar & Uyarılar + +- **Kalibrasyon şişmesi:** BTTS / OU25 izotonik kalibratörleri olasılığı %10-15 fazla + gösteriyor (overcalibrated). Bu marketlerde ekrandaki `calibrated_confidence`'e tam + güvenme; bahis kararı zaten ham-prob `model_gap`/`ev_edge` ile veriliyor. +- **Out-of-sample örneklem küçük:** Eğitim kesim tarihinden sonraki temiz pencere dar + olabilir (~200 MS bahsi). İstatistiksel kesinlik için ileriye doğru gerçek sonuç + biriktir (paper-trade) veya 60 günlük holdout backtest kullan. +- **standard (C) tier kırılgan:** in-sample +%0.4, küçük OOS örnekte negatife düşebiliyor. + Hacim için var; ROI garantisi değil. +- **Tek pencere overfit riski:** Tek bir sezon/dönem penceresine göre ayar yapma; + farklı lig/sezon çeşitliliği ara. +- **Basketbol:** `BasketballV25Predictor.readiness_summary` eksik — futbolu etkilemiyor, + ayrı düzeltilecek. + +--- + +## 11. Hızlı Komut Referansı + +```bash +# 60 günlük backtest (konteyner içi) +python scripts/diagnostic_backtest_multi.py --days 60 --max-matches 10000 + +# Doğrulama (CSV lokale çekildikten sonra) +python3 /tmp/v31c_validation.py reports/multi_backtest_YYYYMMDD.csv +python3 /tmp/best_bet_values.py reports/multi_backtest_YYYYMMDD.csv +python3 /tmp/leakage_split.py reports/multi_backtest_YYYYMMDD.csv + +# Kalibratör eğitim tarihleri +grep -o '"last_trained":[^,]*' models/calibration/*.json +``` diff --git a/ai-engine/services/betting_brain.py b/ai-engine/services/betting_brain.py index 306cbe0..4785af0 100644 --- a/ai-engine/services/betting_brain.py +++ b/ai-engine/services/betting_brain.py @@ -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 diff --git a/ai-engine/services/orchestrator/market_board.py b/ai-engine/services/orchestrator/market_board.py index 895122d..cd19b86 100644 --- a/ai-engine/services/orchestrator/market_board.py +++ b/ai-engine/services/orchestrator/market_board.py @@ -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)), diff --git a/src/modules/coupons/services/smart-coupon.service.ts b/src/modules/coupons/services/smart-coupon.service.ts index 3c82537..c458a12 100755 --- a/src/modules/coupons/services/smart-coupon.service.ts +++ b/src/modules/coupons/services/smart-coupon.service.ts @@ -21,6 +21,8 @@ export interface PredictionPickRow { odds: number; raw_confidence: number; calibrated_confidence: number; + unified_score: number; + unified_score_label: 'very_reliable' | 'reliable' | 'moderate' | 'low'; min_required_confidence: number; edge: number; play_score: number; @@ -35,6 +37,8 @@ export interface PredictionBetSummaryRow { pick: string; raw_confidence: number; calibrated_confidence: number; + unified_score: number; + unified_score_label: 'very_reliable' | 'reliable' | 'moderate' | 'low'; bet_grade: BetGrade; playable: boolean; stake_units: number;