@@ -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:
|
||||
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)),
|
||||
|
||||
Reference in New Issue
Block a user