@@ -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 <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
|
||||||
|
```
|
||||||
@@ -12,14 +12,33 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
|
|
||||||
class BettingBrain:
|
class BettingBrain:
|
||||||
MIN_ODDS = 1.30
|
MIN_ODDS = 1.30
|
||||||
MIN_BET_SCORE = 72.0
|
MIN_BET_SCORE = 62.0
|
||||||
MIN_WATCH_SCORE = 62.0
|
MIN_WATCH_SCORE = 52.0
|
||||||
MIN_BAND_SAMPLE = 8
|
MIN_BAND_SAMPLE = 8
|
||||||
HARD_DIVERGENCE = 0.22
|
HARD_DIVERGENCE = 0.22
|
||||||
SOFT_DIVERGENCE = 0.14
|
SOFT_DIVERGENCE = 0.14
|
||||||
EXTREME_MODEL_PROB = 0.85
|
EXTREME_MODEL_PROB = 0.85
|
||||||
EXTREME_GAP = 0.30
|
EXTREME_GAP = 0.30
|
||||||
SNIPER_BYPASSABLE_VETOES = {"play_score_too_low"}
|
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
|
TRAP_MARKET_GAP = 0.10
|
||||||
|
|
||||||
MARKET_MIN_CONFIDENCE = {
|
MARKET_MIN_CONFIDENCE = {
|
||||||
@@ -39,31 +58,181 @@ class BettingBrain:
|
|||||||
|
|
||||||
SNIPER_BLOCKED_MARKETS = {"HT", "HTFT", "OE", "CARDS", "HT_OU05", "HT_OU15"}
|
SNIPER_BLOCKED_MARKETS = {"HT", "HTFT", "OE", "CARDS", "HT_OU05", "HT_OU15"}
|
||||||
|
|
||||||
# Markets that lose money under every filter combination per the
|
# V30: NO markets muted — backtest tüm marketlerin gerçek ROI'sini görmeli.
|
||||||
# diagnostic backtest (1000 matches). Until calibration is rebuilt for
|
# Tier sistemi zaten filtreleme yapıyor; mute etmek veri kaybına yol açar.
|
||||||
# these specifically, force NO_BET. Re-evaluate after each backtest run.
|
MUTED_MARKETS = set()
|
||||||
MUTED_MARKETS = {"BTTS"}
|
|
||||||
|
|
||||||
# Per-market optimal filter envelopes derived from the diagnostic
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# backtest grid search (reports/filter_optimization_patch.json). Any
|
# V31d: KANITA DAYALI KADEMELİ DEĞER SİSTEMİ (Evidence-Based Tiers)
|
||||||
# pick falling OUTSIDE this envelope is vetoed. Tightens the playable
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# band to the ROI-positive zone identified empirically.
|
# 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,
|
# Validated on 60-day, 72,582-settled-row multi-pick backtest
|
||||||
# min_reliability, require_v27_agree}
|
# (ms_envelope.py + new_gate_sim.py, span 2026-04-17..05-28):
|
||||||
MARKET_OPTIMAL_FILTERS = {
|
# PREMIUM (6.0-7.5, gap≥0, protective vetoes kept):
|
||||||
"MS": {
|
# 602 bets, +32.7% ROI, +39.4u, 20.6% hit, avgOdd 6.50
|
||||||
"min_edge": -0.05, "max_edge": 0.15,
|
# ALL 6 weeks positive (+13.7%..+52.9%); OOS(>05-24) +47.4%;
|
||||||
"min_odds": 1.20, "max_odds": 10.0,
|
# survives dropping top-5 wins (+24%). 14.3 bets/day.
|
||||||
"min_reliability": 0.0, "require_v27_agree": True,
|
# 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.
|
||||||
"OU25": {
|
#
|
||||||
"min_edge": -1.0, "max_edge": 0.15,
|
# WHY 6.0-7.5 (not 6.0-50.0 as in V31c): the edge is concentrated.
|
||||||
"min_odds": 1.80, "max_odds": 10.0,
|
# odds 6.0-7.0 +35% | 7.0-8.0 ~breakeven | 8.0+ NEGATIVE (longshot
|
||||||
"min_reliability": 0.0, "require_v27_agree": False,
|
# 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 = {
|
MARKET_PRIORS = {
|
||||||
"DC": 4.0,
|
"DC": 4.0,
|
||||||
"OU15": 3.0,
|
"OU15": 3.0,
|
||||||
@@ -197,7 +366,7 @@ class BettingBrain:
|
|||||||
|
|
||||||
rejected = [d for d in decisions if d.get("action") == "REJECT"]
|
rejected = [d for d in decisions if d.get("action") == "REJECT"]
|
||||||
guarded["betting_brain"] = {
|
guarded["betting_brain"] = {
|
||||||
"version": "judge-v2-score-coherent",
|
"version": "judge-v31d-evidence-tiers",
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
"reason": decision_reason,
|
"reason": decision_reason,
|
||||||
"main_pick_key": main_key or None,
|
"main_pick_key": main_key or None,
|
||||||
@@ -240,6 +409,21 @@ class BettingBrain:
|
|||||||
triple_is_value = bool((triple or {}).get("is_value"))
|
triple_is_value = bool((triple or {}).get("is_value"))
|
||||||
consensus = str((package.get("v27_engine") or {}).get("consensus") or "").upper()
|
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] = []
|
positives: List[str] = []
|
||||||
issues: List[str] = []
|
issues: List[str] = []
|
||||||
vetoes: List[str] = []
|
vetoes: List[str] = []
|
||||||
@@ -256,7 +440,7 @@ class BettingBrain:
|
|||||||
if market in self.SNIPER_BLOCKED_MARKETS:
|
if market in self.SNIPER_BLOCKED_MARKETS:
|
||||||
is_value_sniper = False
|
is_value_sniper = False
|
||||||
if is_value_sniper:
|
if is_value_sniper:
|
||||||
score += 20.0
|
score += 8.0 # V29b: reduced from 20, tiers do the real filtering
|
||||||
positives.append("value_sniper_override")
|
positives.append("value_sniper_override")
|
||||||
|
|
||||||
score += max(0.0, min(20.0, calibrated_conf * 0.22))
|
score += max(0.0, min(20.0, calibrated_conf * 0.22))
|
||||||
@@ -276,7 +460,7 @@ class BettingBrain:
|
|||||||
if odds_rel < 0.30:
|
if odds_rel < 0.30:
|
||||||
score -= 22.0
|
score -= 22.0
|
||||||
issues.append("very_low_reliability_league")
|
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")
|
vetoes.append("low_reliability_league_hard_block")
|
||||||
elif odds_rel < 0.45:
|
elif odds_rel < 0.45:
|
||||||
score -= 12.0
|
score -= 12.0
|
||||||
@@ -305,38 +489,79 @@ class BettingBrain:
|
|||||||
# ev_edge < 0 = "model market'in altında olasılık veriyor" = vig'i
|
# ev_edge < 0 = "model market'in altında olasılık veriyor" = vig'i
|
||||||
# yiyemeyeceğimiz negative-EV bahis. Hard veto: oynama.
|
# yiyemeyeceğimiz negative-EV bahis. Hard veto: oynama.
|
||||||
# Sniper override hâlâ geçer (yüksek convicted alternatif pick'ler).
|
# Sniper override hâlâ geçer (yüksek convicted alternatif pick'ler).
|
||||||
if ev_edge < 0.0 and not is_value_sniper:
|
# V29b: negative_ev_edge hard veto REMOVED — tier system handles
|
||||||
vetoes.append("negative_ev_edge")
|
# edge bounds per-market via min_edge. MS underdog tier allows
|
||||||
issues.append(f"ev_edge={ev_edge:.3f}_below_zero")
|
# 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
|
# Trap edge: bizim diagnostic backtest'te ev_edge >= 0.20 olan tüm
|
||||||
# bahisler kaybediyordu (n=10, hepsi -%25+ ROI). Model market'i bu
|
# bahisler kaybediyordu (n=10, hepsi -%25+ ROI). Model market'i bu
|
||||||
# kadar yanlış buluyorsa muhtemelen modelin kendisinin yanlış olduğu
|
# kadar yanlış buluyorsa muhtemelen modelin kendisinin yanlış olduğu
|
||||||
# bir senaryo (eksik info, tuhaf maç, vs.) — oynama.
|
# 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")
|
vetoes.append("ev_edge_too_high_trap")
|
||||||
issues.append(f"ev_edge={ev_edge:.3f}_trap_range")
|
issues.append(f"ev_edge={ev_edge:.3f}_trap_range")
|
||||||
|
|
||||||
# ── MUTED MARKETS (grid search showed no profitable config) ──
|
# ── 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")
|
vetoes.append("market_muted_by_backtest")
|
||||||
issues.append(f"market_{market}_muted")
|
issues.append(f"market_{market}_muted")
|
||||||
|
|
||||||
# ── PER-MARKET OPTIMAL ENVELOPE (from grid search) ──
|
# ── V30: ODDS-TIERED ENVELOPE (from 7K backtest grid search) ──
|
||||||
envelope = self.MARKET_OPTIMAL_FILTERS.get(market)
|
# Each market has multiple odds zones with different filters.
|
||||||
if envelope and not is_value_sniper:
|
# If a bet doesn't fit ANY tier, it gets vetoed.
|
||||||
if ev_edge < envelope["min_edge"]:
|
# 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")
|
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")
|
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")
|
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")
|
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")
|
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")
|
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 is not None:
|
||||||
if divergence >= self.HARD_DIVERGENCE and not is_value_sniper:
|
if divergence >= self.HARD_DIVERGENCE and not is_value_sniper:
|
||||||
score -= 42.0
|
score -= 42.0
|
||||||
@@ -348,20 +573,8 @@ class BettingBrain:
|
|||||||
score += 11.0
|
score += 11.0
|
||||||
positives.append("v25_v27_aligned")
|
positives.append("v25_v27_aligned")
|
||||||
|
|
||||||
# Trap market detection: market overpriced vs historical band hit rate
|
# Trap market score penalty (flag computed above, before tier check)
|
||||||
trap_market_flag = False
|
if trap_market_flag:
|
||||||
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
|
score -= 14.0
|
||||||
issues.append("trap_market_market_overpriced")
|
issues.append("trap_market_market_overpriced")
|
||||||
|
|
||||||
@@ -465,6 +678,41 @@ class BettingBrain:
|
|||||||
if sniper_bypassed:
|
if sniper_bypassed:
|
||||||
positives.append("sniper_bypassed_soft_vetoes")
|
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))
|
score = max(0.0, min(100.0, score))
|
||||||
action = "BET"
|
action = "BET"
|
||||||
if vetoes:
|
if vetoes:
|
||||||
@@ -487,8 +735,12 @@ class BettingBrain:
|
|||||||
"issues": issues[:6],
|
"issues": issues[:6],
|
||||||
"vetoes": vetoes[:6],
|
"vetoes": vetoes[:6],
|
||||||
"sniper_bypassed": sniper_bypassed,
|
"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_flag": trap_market_flag,
|
||||||
"trap_market_gap": trap_market_gap,
|
"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,
|
"model_prob": round(model_prob, 4) if model_prob is not None else None,
|
||||||
"implied_prob": round(implied, 4),
|
"implied_prob": round(implied, 4),
|
||||||
"model_market_gap": round(model_gap, 4) if model_gap is not None else None,
|
"model_market_gap": round(model_gap, 4) if model_gap is not None else None,
|
||||||
@@ -501,10 +753,15 @@ class BettingBrain:
|
|||||||
if action != "BET":
|
if action != "BET":
|
||||||
self._force_no_bet(row, f"betting_brain_{action.lower()}")
|
self._force_no_bet(row, f"betting_brain_{action.lower()}")
|
||||||
else:
|
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["pick_reason"] = "betting_brain_approved"
|
||||||
row["stake_units"] = self._brain_stake(row, score)
|
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
|
row["playable"] = True
|
||||||
|
|
||||||
self._append_reason(row, f"betting_brain_{action.lower()}_{round(score)}")
|
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]:
|
def _summary_item(self, row: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
reasons = list(row.get("decision_reasons") or row.get("reasons") or [])
|
reasons = list(row.get("decision_reasons") or row.get("reasons") or [])
|
||||||
|
brain = row.get("betting_brain") or {}
|
||||||
return {
|
return {
|
||||||
"market": row.get("market"),
|
"market": row.get("market"),
|
||||||
"pick": row.get("pick"),
|
"pick": row.get("pick"),
|
||||||
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
|
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
|
||||||
"calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
|
"calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
|
||||||
"bet_grade": row.get("bet_grade", "PASS"),
|
"bet_grade": row.get("bet_grade", "PASS"),
|
||||||
|
"value_tier": row.get("value_tier") or brain.get("value_tier"), # V31c
|
||||||
"playable": bool(row.get("playable")),
|
"playable": bool(row.get("playable")),
|
||||||
"stake_units": float(row.get("stake_units", 0.0) or 0.0),
|
"stake_units": float(row.get("stake_units", 0.0) or 0.0),
|
||||||
"play_score": row.get("play_score", 0.0),
|
"play_score": row.get("play_score", 0.0),
|
||||||
@@ -615,9 +874,22 @@ class BettingBrain:
|
|||||||
"odds": row.get("odds", 0.0),
|
"odds": row.get("odds", 0.0),
|
||||||
"reasons": reasons[:6],
|
"reasons": reasons[:6],
|
||||||
"is_underdog_reference": bool(row.get("is_underdog_reference")),
|
"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
|
@staticmethod
|
||||||
def _candidate_sort_key(row: Dict[str, Any]) -> Tuple[float, float, float]:
|
def _candidate_sort_key(row: Dict[str, Any]) -> Tuple[float, float, float]:
|
||||||
brain = row.get("betting_brain") or {}
|
brain = row.get("betting_brain") or {}
|
||||||
@@ -657,6 +929,12 @@ class BettingBrain:
|
|||||||
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
||||||
if odds <= 1.0:
|
if odds <= 1.0:
|
||||||
return 0.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
|
cap = 2.0 if score >= 82.0 else 1.2
|
||||||
if score < 78.0:
|
if score < 78.0:
|
||||||
cap = 0.8
|
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 config.config_loader import build_threshold_dict, get_threshold_default
|
||||||
from models.calibration import get_calibrator
|
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:
|
class MarketBoardMixin:
|
||||||
def _build_prediction_package(
|
def _build_prediction_package(
|
||||||
@@ -1114,10 +1140,19 @@ class MarketBoardMixin:
|
|||||||
if cal_key and cal_key in calibrator.calibrators:
|
if cal_key and cal_key in calibrator.calibrators:
|
||||||
cal_input = max(0.001, min(0.999, raw_conf / 100.0))
|
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)
|
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))
|
calibrated_conf = max(1.0, min(99.0, cal_prob * 100.0))
|
||||||
else:
|
else:
|
||||||
multiplier = self.market_calibration.get(market, 0.85)
|
# V31b: Fallback for markets WITHOUT isotonic calibrator.
|
||||||
calibrated_conf = max(1.0, min(99.0, raw_conf * multiplier))
|
# 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)
|
min_conf = self.market_min_conf.get(market, 55.0)
|
||||||
|
|
||||||
implied_prob = (1.0 / odd) if odd > 1.0 else 0.0
|
implied_prob = (1.0 / odd) if odd > 1.0 else 0.0
|
||||||
@@ -1178,9 +1213,11 @@ class MarketBoardMixin:
|
|||||||
reasons: List[str] = []
|
reasons: List[str] = []
|
||||||
playable = True
|
playable = True
|
||||||
|
|
||||||
# V34: Broadened value_sniper bypass — odds-aware model rarely shows 3% EV edge
|
# V29b: Permissive upstream — let betting_brain's tiered system do the real filtering.
|
||||||
# Allow high-confidence predictions OR modest positive EV to bypass secondary gates
|
# Old threshold (ev>=0.008 OR conf>=55) let everything through AND bypassed brain vetoes.
|
||||||
is_value_sniper = ev_edge >= 0.008 or calibrated_conf >= 55.0
|
# 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 calibrated_conf < min_conf:
|
||||||
if not is_value_sniper:
|
if not is_value_sniper:
|
||||||
@@ -1283,11 +1320,49 @@ class MarketBoardMixin:
|
|||||||
stake_units = 0.25 # minimum stake (conservative)
|
stake_units = 0.25 # minimum stake (conservative)
|
||||||
reasons.append("no_ev_edge_minimum_stake")
|
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 = dict(row)
|
||||||
out.update(
|
out.update(
|
||||||
{
|
{
|
||||||
"raw_confidence": round(raw_conf, 1),
|
"raw_confidence": round(raw_conf, 1),
|
||||||
"calibrated_confidence": round(calibrated_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_confidence": round(min_conf, 1),
|
||||||
"min_required_play_score": round(min_play_score, 1),
|
"min_required_play_score": round(min_play_score, 1),
|
||||||
"min_required_edge": round(min_edge, 4),
|
"min_required_edge": round(min_edge, 4),
|
||||||
@@ -1347,6 +1422,8 @@ class MarketBoardMixin:
|
|||||||
"pick": row.get("pick"),
|
"pick": row.get("pick"),
|
||||||
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
|
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
|
||||||
"calibrated_confidence": row.get("calibrated_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"),
|
"bet_grade": row.get("bet_grade", "PASS"),
|
||||||
"playable": bool(row.get("playable")),
|
"playable": bool(row.get("playable")),
|
||||||
"stake_units": float(row.get("stake_units", 0.0)),
|
"stake_units": float(row.get("stake_units", 0.0)),
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export interface PredictionPickRow {
|
|||||||
odds: number;
|
odds: number;
|
||||||
raw_confidence: number;
|
raw_confidence: number;
|
||||||
calibrated_confidence: number;
|
calibrated_confidence: number;
|
||||||
|
unified_score: number;
|
||||||
|
unified_score_label: 'very_reliable' | 'reliable' | 'moderate' | 'low';
|
||||||
min_required_confidence: number;
|
min_required_confidence: number;
|
||||||
edge: number;
|
edge: number;
|
||||||
play_score: number;
|
play_score: number;
|
||||||
@@ -35,6 +37,8 @@ export interface PredictionBetSummaryRow {
|
|||||||
pick: string;
|
pick: string;
|
||||||
raw_confidence: number;
|
raw_confidence: number;
|
||||||
calibrated_confidence: number;
|
calibrated_confidence: number;
|
||||||
|
unified_score: number;
|
||||||
|
unified_score_label: 'very_reliable' | 'reliable' | 'moderate' | 'low';
|
||||||
bet_grade: BetGrade;
|
bet_grade: BetGrade;
|
||||||
playable: boolean;
|
playable: boolean;
|
||||||
stake_units: number;
|
stake_units: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user