gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m6s

This commit is contained in:
2026-05-29 11:59:51 +03:00
parent 659110c806
commit b5cb412236
4 changed files with 736 additions and 64 deletions
+313
View File
@@ -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 +%515.
>
> 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 (—) | — | markete ö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.07.5 (V31c'deki 6.050.0 değil):** edge dar bir banda yoğunlaşmış.
`6.07.0 +%35` · `7.08.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.08.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.07.5** bandında. 8.0 üstü longshot'lar kaybeder
(eski 6.050.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.06.0 / 3.05.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 <csv>` (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 <csv>` — V31c tier dökümü (premium/strong/standard ROI).
- `/tmp/best_bet_values.py <csv>` — grid-search liderlik tablosu + portföy + kombine testi.
- `/tmp/leakage_split.py <csv>` — 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@<host>:/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
```
+337 -59
View File
@@ -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
@@ -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)),
@@ -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;