Compare commits
21 Commits
b619c2454a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d40b55fb9 | |||
| 6269ede4ad | |||
| 4c137fbab6 | |||
| bb911176df | |||
| 950add373f | |||
| 9a8f9941b6 | |||
| e0fbde2fde | |||
| b62a4f2161 | |||
| c3e44ee697 | |||
| 42b6c7ce43 | |||
| 7b17aa1fee | |||
| c338aba1c0 | |||
| 1c03fa5e1c | |||
| 9e41407cb5 | |||
| b9700f9fda | |||
| 033a29c79c | |||
| 4e563e996e | |||
| 671979b07d | |||
| b5cb412236 | |||
| 659110c806 | |||
| 988ee2f50d |
@@ -46,6 +46,7 @@ jobs:
|
|||||||
-e AI_ENGINE_URL='http://iddaai-ai-engine:8000' \
|
-e AI_ENGINE_URL='http://iddaai-ai-engine:8000' \
|
||||||
-e JWT_SECRET='${{ secrets.JWT_SECRET }}' \
|
-e JWT_SECRET='${{ secrets.JWT_SECRET }}' \
|
||||||
-e JWT_ACCESS_EXPIRATION='1d' \
|
-e JWT_ACCESS_EXPIRATION='1d' \
|
||||||
|
-e IMAGE_BASE_URL='https://files.iddaai.com' \
|
||||||
iddaai-be:latest /bin/sh -c "npx prisma migrate deploy && node dist/src/main.js"
|
iddaai-be:latest /bin/sh -c "npx prisma migrate deploy && node dist/src/main.js"
|
||||||
|
|
||||||
- name: Saglik Kontrolu
|
- name: Saglik Kontrolu
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -1,874 +0,0 @@
|
|||||||
{
|
|
||||||
"meta":{"test_sets":["test"],"test_metrics":[{"best_value":"Min","name":"Logloss"}],"learn_metrics":[{"best_value":"Min","name":"Logloss"}],"launch_mode":"Train","parameters":"","iteration_count":2000,"learn_sets":["learn"],"name":"experiment"},
|
|
||||||
"iterations":[
|
|
||||||
{"learn":[0.692389481],"iteration":0,"passed_time":0.04679785798,"remaining_time":93.54891809,"test":[0.6924099937]},
|
|
||||||
{"learn":[0.6916338586],"iteration":1,"passed_time":0.08350330552,"remaining_time":83.41980222,"test":[0.6916660956]},
|
|
||||||
{"learn":[0.6910159214],"iteration":2,"passed_time":0.132821758,"remaining_time":88.41501689,"test":[0.691108145]},
|
|
||||||
{"learn":[0.6903417151],"iteration":3,"passed_time":0.162826233,"remaining_time":81.25029026,"test":[0.6904585078]},
|
|
||||||
{"learn":[0.6896961461],"iteration":4,"passed_time":0.1969265393,"remaining_time":78.57368918,"test":[0.689812816]},
|
|
||||||
{"learn":[0.6890979366],"iteration":5,"passed_time":0.2309352918,"remaining_time":76.74749531,"test":[0.689192261]},
|
|
||||||
{"learn":[0.6884946167],"iteration":6,"passed_time":0.2693987513,"remaining_time":76.70167304,"test":[0.6886032715]},
|
|
||||||
{"learn":[0.6879503686],"iteration":7,"passed_time":0.3199759681,"remaining_time":79.67401607,"test":[0.6880706742]},
|
|
||||||
{"learn":[0.6874528094],"iteration":8,"passed_time":0.3645802206,"remaining_time":80.65324659,"test":[0.6876192378]},
|
|
||||||
{"learn":[0.6869036785],"iteration":9,"passed_time":0.4116507506,"remaining_time":81.91849936,"test":[0.6870868859]},
|
|
||||||
{"learn":[0.6863761921],"iteration":10,"passed_time":0.4562469316,"remaining_time":82.49774064,"test":[0.6865493528]},
|
|
||||||
{"learn":[0.6859038678],"iteration":11,"passed_time":0.491541699,"remaining_time":81.43207481,"test":[0.686105086]},
|
|
||||||
{"learn":[0.685410175],"iteration":12,"passed_time":0.5221556769,"remaining_time":79.80948692,"test":[0.6856345086]},
|
|
||||||
{"learn":[0.6849483392],"iteration":13,"passed_time":0.5553110353,"remaining_time":78.77483686,"test":[0.6852027185]},
|
|
||||||
{"learn":[0.6845417792],"iteration":14,"passed_time":0.5952927147,"remaining_time":78.77706925,"test":[0.6848238481]},
|
|
||||||
{"learn":[0.6841038875],"iteration":15,"passed_time":0.6300274185,"remaining_time":78.12339989,"test":[0.6844045699]},
|
|
||||||
{"learn":[0.6836957422],"iteration":16,"passed_time":0.662600544,"remaining_time":77.29040464,"test":[0.6840077621]},
|
|
||||||
{"learn":[0.6832947461],"iteration":17,"passed_time":0.7004221698,"remaining_time":77.12426337,"test":[0.6836197496]},
|
|
||||||
{"learn":[0.6829014105],"iteration":18,"passed_time":0.7300844347,"remaining_time":76.12090869,"test":[0.6832475033]},
|
|
||||||
{"learn":[0.6825264546],"iteration":19,"passed_time":0.7641559459,"remaining_time":75.65143865,"test":[0.6829012069]},
|
|
||||||
{"learn":[0.6822106577],"iteration":20,"passed_time":0.8040792063,"remaining_time":75.77489282,"test":[0.6825880966]},
|
|
||||||
{"learn":[0.6818649349],"iteration":21,"passed_time":0.8356039756,"remaining_time":75.12839381,"test":[0.6822424968]},
|
|
||||||
{"learn":[0.6815467855],"iteration":22,"passed_time":0.8861440327,"remaining_time":76.16985881,"test":[0.6819180513]},
|
|
||||||
{"learn":[0.6812293319],"iteration":23,"passed_time":0.920219319,"remaining_time":75.76472393,"test":[0.6816384467]},
|
|
||||||
{"learn":[0.6808837443],"iteration":24,"passed_time":0.960164738,"remaining_time":75.8530143,"test":[0.6813262593]},
|
|
||||||
{"learn":[0.6805816494],"iteration":25,"passed_time":0.9895547925,"remaining_time":75.13004463,"test":[0.6810353411]},
|
|
||||||
{"learn":[0.6803209634],"iteration":26,"passed_time":1.025550161,"remaining_time":74.94112844,"test":[0.6808138172]},
|
|
||||||
{"learn":[0.6800350862],"iteration":27,"passed_time":1.060852064,"remaining_time":74.71429535,"test":[0.6805550049]},
|
|
||||||
{"learn":[0.6797703947],"iteration":28,"passed_time":1.10467538,"remaining_time":75.07983357,"test":[0.680347991]},
|
|
||||||
{"learn":[0.6794926675],"iteration":29,"passed_time":1.141766834,"remaining_time":74.97602208,"test":[0.680089679]},
|
|
||||||
{"learn":[0.6792251865],"iteration":30,"passed_time":1.180421588,"remaining_time":74.9758099,"test":[0.6798451919]},
|
|
||||||
{"learn":[0.6789670166],"iteration":31,"passed_time":1.213674604,"remaining_time":74.64098814,"test":[0.6796090443]},
|
|
||||||
{"learn":[0.678722402],"iteration":32,"passed_time":1.245848393,"remaining_time":74.26011482,"test":[0.6793890865]},
|
|
||||||
{"learn":[0.678476935],"iteration":33,"passed_time":1.287262512,"remaining_time":74.43406171,"test":[0.6791683772]},
|
|
||||||
{"learn":[0.6782297335],"iteration":34,"passed_time":1.327473991,"remaining_time":74.52818262,"test":[0.6789766369]},
|
|
||||||
{"learn":[0.6780226701],"iteration":35,"passed_time":1.3760549,"remaining_time":75.07143955,"test":[0.6787930242]},
|
|
||||||
{"learn":[0.6778291026],"iteration":36,"passed_time":1.427620019,"remaining_time":75.74102965,"test":[0.6786087714]},
|
|
||||||
{"learn":[0.6776045324],"iteration":37,"passed_time":1.468182407,"remaining_time":75.80457587,"test":[0.6784161299]},
|
|
||||||
{"learn":[0.6773969079],"iteration":38,"passed_time":1.508647379,"remaining_time":75.85788487,"test":[0.6782227897]},
|
|
||||||
{"learn":[0.6771819602],"iteration":39,"passed_time":1.549435187,"remaining_time":75.92232419,"test":[0.6780242369]},
|
|
||||||
{"learn":[0.6769816736],"iteration":40,"passed_time":1.586036608,"remaining_time":75.78160282,"test":[0.6778499631]},
|
|
||||||
{"learn":[0.6767984027],"iteration":41,"passed_time":1.621458864,"remaining_time":75.59086802,"test":[0.6776975784]},
|
|
||||||
{"learn":[0.6766201184],"iteration":42,"passed_time":1.663424818,"remaining_time":75.70517136,"test":[0.6775231674]},
|
|
||||||
{"learn":[0.6764394377],"iteration":43,"passed_time":1.70110089,"remaining_time":75.62166686,"test":[0.6773582124]},
|
|
||||||
{"learn":[0.6762698797],"iteration":44,"passed_time":1.739954496,"remaining_time":75.59135644,"test":[0.6772234666]},
|
|
||||||
{"learn":[0.6760974263],"iteration":45,"passed_time":1.776461223,"remaining_time":75.46098325,"test":[0.6770659843]},
|
|
||||||
{"learn":[0.6759245179],"iteration":46,"passed_time":1.819761638,"remaining_time":75.61690381,"test":[0.6769049529]},
|
|
||||||
{"learn":[0.6757673909],"iteration":47,"passed_time":1.869479807,"remaining_time":76.02551217,"test":[0.6767664194]},
|
|
||||||
{"learn":[0.6756172628],"iteration":48,"passed_time":1.916010121,"remaining_time":76.28848462,"test":[0.6766584917]},
|
|
||||||
{"learn":[0.675474531],"iteration":49,"passed_time":1.953635244,"remaining_time":76.19177452,"test":[0.6765507257]},
|
|
||||||
{"learn":[0.6753286933],"iteration":50,"passed_time":1.993876686,"remaining_time":76.19736591,"test":[0.6764489911]},
|
|
||||||
{"learn":[0.6751900513],"iteration":51,"passed_time":2.038943041,"remaining_time":76.38194316,"test":[0.6763947956]},
|
|
||||||
{"learn":[0.6750574835],"iteration":52,"passed_time":2.080276765,"remaining_time":76.42073325,"test":[0.6762778712]},
|
|
||||||
{"learn":[0.6749329567],"iteration":53,"passed_time":2.158576742,"remaining_time":77.78871001,"test":[0.6761865366]},
|
|
||||||
{"learn":[0.6748033265],"iteration":54,"passed_time":2.220619687,"remaining_time":78.52918711,"test":[0.6760679685]},
|
|
||||||
{"learn":[0.6746797823],"iteration":55,"passed_time":2.286959228,"remaining_time":79.39015604,"test":[0.6759774874]},
|
|
||||||
{"learn":[0.674535525],"iteration":56,"passed_time":2.328472096,"remaining_time":79.3723032,"test":[0.6758500622]},
|
|
||||||
{"learn":[0.6744256514],"iteration":57,"passed_time":2.367031568,"remaining_time":79.25474665,"test":[0.6757625065]},
|
|
||||||
{"learn":[0.674310819],"iteration":58,"passed_time":2.409161286,"remaining_time":79.25732298,"test":[0.6756876412]},
|
|
||||||
{"learn":[0.6741967947],"iteration":59,"passed_time":2.444825903,"remaining_time":79.04937087,"test":[0.6756151069]},
|
|
||||||
{"learn":[0.6740879654],"iteration":60,"passed_time":2.48484996,"remaining_time":78.98564055,"test":[0.6755303655]},
|
|
||||||
{"learn":[0.6739772476],"iteration":61,"passed_time":2.521603395,"remaining_time":78.8204416,"test":[0.6754565036]},
|
|
||||||
{"learn":[0.67388281],"iteration":62,"passed_time":2.554102332,"remaining_time":78.5285114,"test":[0.6753738983]},
|
|
||||||
{"learn":[0.6737789726],"iteration":63,"passed_time":2.593937938,"remaining_time":78.46662263,"test":[0.6752897299]},
|
|
||||||
{"learn":[0.6736812332],"iteration":64,"passed_time":2.623889155,"remaining_time":78.11116175,"test":[0.6752115539]},
|
|
||||||
{"learn":[0.6735930009],"iteration":65,"passed_time":2.660795108,"remaining_time":77.96935967,"test":[0.6751595431]},
|
|
||||||
{"learn":[0.6734947116],"iteration":66,"passed_time":2.695822592,"remaining_time":77.77649358,"test":[0.6750764658]},
|
|
||||||
{"learn":[0.6733961481],"iteration":67,"passed_time":2.725876686,"remaining_time":77.44696703,"test":[0.6750179194]},
|
|
||||||
{"learn":[0.6732990195],"iteration":68,"passed_time":2.761848366,"remaining_time":77.29172746,"test":[0.6749408803]},
|
|
||||||
{"learn":[0.6732133575],"iteration":69,"passed_time":2.791847449,"remaining_time":76.97522253,"test":[0.6748795802]},
|
|
||||||
{"learn":[0.673111539],"iteration":70,"passed_time":2.824541003,"remaining_time":76.73999429,"test":[0.674790372]},
|
|
||||||
{"learn":[0.6730080451],"iteration":71,"passed_time":2.861023716,"remaining_time":76.61185729,"test":[0.6747239773]},
|
|
||||||
{"learn":[0.6729157861],"iteration":72,"passed_time":2.897136588,"remaining_time":76.47646857,"test":[0.6746701254]},
|
|
||||||
{"learn":[0.6728347949],"iteration":73,"passed_time":2.935718661,"remaining_time":76.40802894,"test":[0.6746120937]},
|
|
||||||
{"learn":[0.6727640693],"iteration":74,"passed_time":3.040023476,"remaining_time":78.02726921,"test":[0.6745550085]},
|
|
||||||
{"learn":[0.6726808811],"iteration":75,"passed_time":3.097341794,"remaining_time":78.41165279,"test":[0.6744855074]},
|
|
||||||
{"learn":[0.6726029645],"iteration":76,"passed_time":3.152948955,"remaining_time":78.74182909,"test":[0.6744264172]},
|
|
||||||
{"learn":[0.6725356026],"iteration":77,"passed_time":3.216126808,"remaining_time":79.24866314,"test":[0.674381715]},
|
|
||||||
{"learn":[0.6724606887],"iteration":78,"passed_time":3.256861302,"remaining_time":79.19532355,"test":[0.6743331681]},
|
|
||||||
{"learn":[0.6723849561],"iteration":79,"passed_time":3.305679851,"remaining_time":79.33631641,"test":[0.67428564]},
|
|
||||||
{"learn":[0.6723050519],"iteration":80,"passed_time":3.348083566,"remaining_time":79.32064647,"test":[0.6742202413]},
|
|
||||||
{"learn":[0.6722508802],"iteration":81,"passed_time":3.38129387,"remaining_time":79.08928832,"test":[0.6741620971]},
|
|
||||||
{"learn":[0.6721773904],"iteration":82,"passed_time":3.41660066,"remaining_time":78.91112609,"test":[0.6741109453]},
|
|
||||||
{"learn":[0.6721007598],"iteration":83,"passed_time":3.48099347,"remaining_time":79.39980344,"test":[0.6740556003]},
|
|
||||||
{"learn":[0.6720353564],"iteration":84,"passed_time":3.535359896,"remaining_time":79.64957884,"test":[0.6740146772]},
|
|
||||||
{"learn":[0.6719790902],"iteration":85,"passed_time":3.581806996,"remaining_time":79.71603012,"test":[0.673983295]},
|
|
||||||
{"learn":[0.6719140024],"iteration":86,"passed_time":3.612293661,"remaining_time":79.42893993,"test":[0.6739595301]},
|
|
||||||
{"learn":[0.6718573633],"iteration":87,"passed_time":3.644530261,"remaining_time":79.18570293,"test":[0.6739336659]},
|
|
||||||
{"learn":[0.671795602],"iteration":88,"passed_time":3.67809653,"remaining_time":78.97575809,"test":[0.673890361]},
|
|
||||||
{"learn":[0.6717369134],"iteration":89,"passed_time":3.712417516,"remaining_time":78.78574951,"test":[0.673863586]},
|
|
||||||
{"learn":[0.6716711079],"iteration":90,"passed_time":3.743502971,"remaining_time":78.53128759,"test":[0.6738190616]},
|
|
||||||
{"learn":[0.6716070843],"iteration":91,"passed_time":3.775351679,"remaining_time":78.2975109,"test":[0.6737799295]},
|
|
||||||
{"learn":[0.6715517232],"iteration":92,"passed_time":3.806186247,"remaining_time":78.04728142,"test":[0.6737364374]},
|
|
||||||
{"learn":[0.6714957378],"iteration":93,"passed_time":3.83798807,"remaining_time":77.82133257,"test":[0.6737093719]},
|
|
||||||
{"learn":[0.6714364567],"iteration":94,"passed_time":3.871278973,"remaining_time":77.62933099,"test":[0.6736630475]},
|
|
||||||
{"learn":[0.6713881758],"iteration":95,"passed_time":3.913531039,"remaining_time":77.6183656,"test":[0.67364367]},
|
|
||||||
{"learn":[0.6713336502],"iteration":96,"passed_time":3.945433866,"remaining_time":77.40371802,"test":[0.6735998081]},
|
|
||||||
{"learn":[0.6712700267],"iteration":97,"passed_time":3.989716281,"remaining_time":77.43306496,"test":[0.6735526984]},
|
|
||||||
{"learn":[0.6712154424],"iteration":98,"passed_time":4.020621946,"remaining_time":77.20406384,"test":[0.6735012924]},
|
|
||||||
{"learn":[0.6711600413],"iteration":99,"passed_time":4.053732144,"remaining_time":77.02091074,"test":[0.6734818024]},
|
|
||||||
{"learn":[0.6711060533],"iteration":100,"passed_time":4.084124711,"remaining_time":76.78963194,"test":[0.6734379341]},
|
|
||||||
{"learn":[0.6710494943],"iteration":101,"passed_time":4.116434744,"remaining_time":76.59797199,"test":[0.6734059869]},
|
|
||||||
{"learn":[0.6709936897],"iteration":102,"passed_time":4.148330356,"remaining_time":76.40177365,"test":[0.6733740852]},
|
|
||||||
{"learn":[0.6709472183],"iteration":103,"passed_time":4.176511193,"remaining_time":76.14101176,"test":[0.6733330971]},
|
|
||||||
{"learn":[0.6708914508],"iteration":104,"passed_time":4.2025065,"remaining_time":75.84523636,"test":[0.6733060254]},
|
|
||||||
{"learn":[0.6708388195],"iteration":105,"passed_time":4.232975206,"remaining_time":75.63448151,"test":[0.6732755898]},
|
|
||||||
{"learn":[0.6707885854],"iteration":106,"passed_time":4.261364958,"remaining_time":75.39031649,"test":[0.6732294722]},
|
|
||||||
{"learn":[0.6707454167],"iteration":107,"passed_time":4.290824713,"remaining_time":75.1688922,"test":[0.6732035176]},
|
|
||||||
{"learn":[0.6706973013],"iteration":108,"passed_time":4.324192493,"remaining_time":75.01878903,"test":[0.673196437]},
|
|
||||||
{"learn":[0.6706577031],"iteration":109,"passed_time":4.351512102,"remaining_time":74.76688976,"test":[0.6731652709]},
|
|
||||||
{"learn":[0.67061108],"iteration":110,"passed_time":4.38641502,"remaining_time":74.64808984,"test":[0.673138808]},
|
|
||||||
{"learn":[0.6705625485],"iteration":111,"passed_time":4.424063991,"remaining_time":74.57707871,"test":[0.6731062725]},
|
|
||||||
{"learn":[0.6705146484],"iteration":112,"passed_time":4.45863849,"remaining_time":74.45531709,"test":[0.6730726625]},
|
|
||||||
{"learn":[0.6704704423],"iteration":113,"passed_time":4.497153675,"remaining_time":74.40027922,"test":[0.6730285927]},
|
|
||||||
{"learn":[0.6704155922],"iteration":114,"passed_time":4.533368584,"remaining_time":74.30782417,"test":[0.6729872702]},
|
|
||||||
{"learn":[0.6703687117],"iteration":115,"passed_time":4.564651269,"remaining_time":74.13623268,"test":[0.6729721425]},
|
|
||||||
{"learn":[0.6703324232],"iteration":116,"passed_time":4.596824343,"remaining_time":73.98136956,"test":[0.6729564624]},
|
|
||||||
{"learn":[0.6702884624],"iteration":117,"passed_time":4.628377967,"remaining_time":73.81870623,"test":[0.6729312424]},
|
|
||||||
{"learn":[0.670253478],"iteration":118,"passed_time":4.668052254,"remaining_time":73.78660748,"test":[0.6729354345]},
|
|
||||||
{"learn":[0.6702140804],"iteration":119,"passed_time":4.692108266,"remaining_time":73.50969617,"test":[0.6729085401]},
|
|
||||||
{"learn":[0.6701682529],"iteration":120,"passed_time":4.723741667,"remaining_time":73.354633,"test":[0.6728898322]},
|
|
||||||
{"learn":[0.6701320588],"iteration":121,"passed_time":4.756626425,"remaining_time":73.22085595,"test":[0.6728773638]},
|
|
||||||
{"learn":[0.6700939824],"iteration":122,"passed_time":4.788008428,"remaining_time":73.06578714,"test":[0.6728618874]},
|
|
||||||
{"learn":[0.6700655902],"iteration":123,"passed_time":4.815546648,"remaining_time":72.85456058,"test":[0.6728540413]},
|
|
||||||
{"learn":[0.6700190743],"iteration":124,"passed_time":4.843186806,"remaining_time":72.64780209,"test":[0.6728441291]},
|
|
||||||
{"learn":[0.6699792296],"iteration":125,"passed_time":4.875548614,"remaining_time":72.51411192,"test":[0.672815631]},
|
|
||||||
{"learn":[0.6699379404],"iteration":126,"passed_time":4.916953662,"remaining_time":72.51538748,"test":[0.6728082021]},
|
|
||||||
{"learn":[0.669895454],"iteration":127,"passed_time":4.952918369,"remaining_time":72.43643115,"test":[0.6727900064]},
|
|
||||||
{"learn":[0.6698563938],"iteration":128,"passed_time":4.991585558,"remaining_time":72.39733782,"test":[0.6727649552]},
|
|
||||||
{"learn":[0.6698215571],"iteration":129,"passed_time":5.028084166,"remaining_time":72.32705685,"test":[0.6727467657]},
|
|
||||||
{"learn":[0.6697857067],"iteration":130,"passed_time":5.059198996,"remaining_time":72.18048033,"test":[0.6727396032]},
|
|
||||||
{"learn":[0.6697449303],"iteration":131,"passed_time":5.096035515,"remaining_time":72.1166238,"test":[0.6727245271]},
|
|
||||||
{"learn":[0.6697052425],"iteration":132,"passed_time":5.125282589,"remaining_time":71.94663604,"test":[0.6726955143]},
|
|
||||||
{"learn":[0.6696695553],"iteration":133,"passed_time":5.156392608,"remaining_time":71.80469109,"test":[0.67269209]},
|
|
||||||
{"learn":[0.6696269265],"iteration":134,"passed_time":5.190402292,"remaining_time":71.70444647,"test":[0.672677932]},
|
|
||||||
{"learn":[0.6695969271],"iteration":135,"passed_time":5.221466142,"remaining_time":71.56480065,"test":[0.6726540285]},
|
|
||||||
{"learn":[0.6695489786],"iteration":136,"passed_time":5.251144663,"remaining_time":71.40790151,"test":[0.6726288583]},
|
|
||||||
{"learn":[0.6695173859],"iteration":137,"passed_time":5.274361693,"remaining_time":71.16566285,"test":[0.6725863431]},
|
|
||||||
{"learn":[0.6694811164],"iteration":138,"passed_time":5.309398952,"remaining_time":71.08483058,"test":[0.6725837967]},
|
|
||||||
{"learn":[0.6694477439],"iteration":139,"passed_time":5.344693175,"remaining_time":71.00806646,"test":[0.6725772977]},
|
|
||||||
{"learn":[0.6694082161],"iteration":140,"passed_time":5.377737126,"remaining_time":70.90222211,"test":[0.6725685594]},
|
|
||||||
{"learn":[0.6693679185],"iteration":141,"passed_time":5.416087925,"remaining_time":70.8668406,"test":[0.6725553829]},
|
|
||||||
{"learn":[0.6693341916],"iteration":142,"passed_time":5.452286939,"remaining_time":70.80347444,"test":[0.6725484347]},
|
|
||||||
{"learn":[0.6692933159],"iteration":143,"passed_time":5.490006789,"remaining_time":70.7600875,"test":[0.6725306172]},
|
|
||||||
{"learn":[0.6692619696],"iteration":144,"passed_time":5.521869859,"remaining_time":70.64185233,"test":[0.672543149]},
|
|
||||||
{"learn":[0.6692229289],"iteration":145,"passed_time":5.553520721,"remaining_time":70.5221056,"test":[0.6725196247]},
|
|
||||||
{"learn":[0.6691840164],"iteration":146,"passed_time":5.582178524,"remaining_time":70.3658286,"test":[0.6725226452]},
|
|
||||||
{"learn":[0.6691581406],"iteration":147,"passed_time":5.611368671,"remaining_time":70.21793769,"test":[0.6725056913]},
|
|
||||||
{"learn":[0.6691177196],"iteration":148,"passed_time":5.636941079,"remaining_time":70.02669757,"test":[0.6724771476]},
|
|
||||||
{"learn":[0.6690851126],"iteration":149,"passed_time":5.673704689,"remaining_time":69.97569117,"test":[0.6724439435]},
|
|
||||||
{"learn":[0.6690518144],"iteration":150,"passed_time":5.706346207,"remaining_time":69.87439826,"test":[0.672442532]},
|
|
||||||
{"learn":[0.6690149711],"iteration":151,"passed_time":5.738210991,"remaining_time":69.76456521,"test":[0.6724303064]},
|
|
||||||
{"learn":[0.668993877],"iteration":152,"passed_time":5.765951318,"remaining_time":69.60596133,"test":[0.6724235788]},
|
|
||||||
{"learn":[0.6689596579],"iteration":153,"passed_time":5.795573467,"remaining_time":69.47161442,"test":[0.6724294499]},
|
|
||||||
{"learn":[0.6689372651],"iteration":154,"passed_time":5.81744896,"remaining_time":69.24640858,"test":[0.6724285935]},
|
|
||||||
{"learn":[0.6689003045],"iteration":155,"passed_time":5.853529431,"remaining_time":69.19171968,"test":[0.6724172017]},
|
|
||||||
{"learn":[0.6688680182],"iteration":156,"passed_time":5.888380392,"remaining_time":69.12283479,"test":[0.6724130745]},
|
|
||||||
{"learn":[0.6688348164],"iteration":157,"passed_time":5.924601775,"remaining_time":69.07035741,"test":[0.6723860878]},
|
|
||||||
{"learn":[0.6687947046],"iteration":158,"passed_time":5.964531924,"remaining_time":69.06102687,"test":[0.6723707604]},
|
|
||||||
{"learn":[0.6687605251],"iteration":159,"passed_time":5.996805452,"remaining_time":68.9632627,"test":[0.6723566111]},
|
|
||||||
{"learn":[0.668726253],"iteration":160,"passed_time":6.022341459,"remaining_time":68.78935368,"test":[0.6723469906]},
|
|
||||||
{"learn":[0.6686862718],"iteration":161,"passed_time":6.05082584,"remaining_time":68.65072774,"test":[0.6723287161]},
|
|
||||||
{"learn":[0.668663478],"iteration":162,"passed_time":6.079027554,"remaining_time":68.51026759,"test":[0.6723155898]},
|
|
||||||
{"learn":[0.6686399521],"iteration":163,"passed_time":6.108511297,"remaining_time":68.38552891,"test":[0.6722970834]},
|
|
||||||
{"learn":[0.6686058279],"iteration":164,"passed_time":6.140719309,"remaining_time":68.29224202,"test":[0.6722872244]},
|
|
||||||
{"learn":[0.6685761282],"iteration":165,"passed_time":6.169540017,"remaining_time":68.16226742,"test":[0.6722800481]},
|
|
||||||
{"learn":[0.6685469327],"iteration":166,"passed_time":6.2020892,"remaining_time":68.07442817,"test":[0.6722550973]},
|
|
||||||
{"learn":[0.6685157003],"iteration":167,"passed_time":6.231576547,"remaining_time":67.95385854,"test":[0.6722394313]},
|
|
||||||
{"learn":[0.6684805143],"iteration":168,"passed_time":6.263261652,"remaining_time":67.85817802,"test":[0.6722204135]},
|
|
||||||
{"learn":[0.6684485765],"iteration":169,"passed_time":6.295102833,"remaining_time":67.7649305,"test":[0.6721982148]},
|
|
||||||
{"learn":[0.6684144429],"iteration":170,"passed_time":6.325415964,"remaining_time":67.65605729,"test":[0.6721971176]},
|
|
||||||
{"learn":[0.6683849752],"iteration":171,"passed_time":6.35697084,"remaining_time":67.56129474,"test":[0.6721880705]},
|
|
||||||
{"learn":[0.6683568537],"iteration":172,"passed_time":6.395913563,"remaining_time":67.5452837,"test":[0.672179176]},
|
|
||||||
{"learn":[0.6683266628],"iteration":173,"passed_time":6.437330522,"remaining_time":67.55497433,"test":[0.6721769709]},
|
|
||||||
{"learn":[0.6682937842],"iteration":174,"passed_time":6.472195712,"remaining_time":67.49575528,"test":[0.6721693215]},
|
|
||||||
{"learn":[0.6682657097],"iteration":175,"passed_time":6.503044842,"remaining_time":67.395192,"test":[0.6721581386]},
|
|
||||||
{"learn":[0.6682301443],"iteration":176,"passed_time":6.533528251,"remaining_time":67.29164972,"test":[0.6721638661]},
|
|
||||||
{"learn":[0.6681995916],"iteration":177,"passed_time":6.562589882,"remaining_time":67.17437509,"test":[0.6721598475]},
|
|
||||||
{"learn":[0.6681658267],"iteration":178,"passed_time":6.590816982,"remaining_time":67.04959623,"test":[0.6721433342]},
|
|
||||||
{"learn":[0.6681422687],"iteration":179,"passed_time":6.624646227,"remaining_time":66.98253407,"test":[0.6721335599]},
|
|
||||||
{"learn":[0.6681216601],"iteration":180,"passed_time":6.655147334,"remaining_time":66.88239227,"test":[0.6721300594]},
|
|
||||||
{"learn":[0.6680899019],"iteration":181,"passed_time":6.687788902,"remaining_time":66.80439684,"test":[0.6721153533]},
|
|
||||||
{"learn":[0.6680676394],"iteration":182,"passed_time":6.718057043,"remaining_time":66.7033314,"test":[0.6721076397]},
|
|
||||||
{"learn":[0.6680413672],"iteration":183,"passed_time":6.751300957,"remaining_time":66.6324051,"test":[0.6721009911]},
|
|
||||||
{"learn":[0.6680088406],"iteration":184,"passed_time":6.784288393,"remaining_time":66.55936991,"test":[0.6720999252]},
|
|
||||||
{"learn":[0.6679873982],"iteration":185,"passed_time":6.810905309,"remaining_time":66.42463565,"test":[0.6720953028]},
|
|
||||||
{"learn":[0.6679663544],"iteration":186,"passed_time":6.832974292,"remaining_time":66.24696466,"test":[0.6720942505]},
|
|
||||||
{"learn":[0.6679417375],"iteration":187,"passed_time":6.867184511,"remaining_time":66.18796986,"test":[0.6720856237]},
|
|
||||||
{"learn":[0.6679100197],"iteration":188,"passed_time":6.918652024,"remaining_time":66.29459691,"test":[0.6720876136]},
|
|
||||||
{"learn":[0.667881208],"iteration":189,"passed_time":6.96948149,"remaining_time":66.39348156,"test":[0.6720880182]},
|
|
||||||
{"learn":[0.6678475427],"iteration":190,"passed_time":7.018176318,"remaining_time":66.47058094,"test":[0.6720743856]},
|
|
||||||
{"learn":[0.6678310341],"iteration":191,"passed_time":7.074099623,"remaining_time":66.61443812,"test":[0.6720598415]},
|
|
||||||
{"learn":[0.6678060257],"iteration":192,"passed_time":7.117099742,"remaining_time":66.63522919,"test":[0.6720563492]},
|
|
||||||
{"learn":[0.6677789336],"iteration":193,"passed_time":7.191058554,"remaining_time":66.94356571,"test":[0.6720389527]},
|
|
||||||
{"learn":[0.6677478773],"iteration":194,"passed_time":7.2421897,"remaining_time":67.03667902,"test":[0.6720317324]},
|
|
||||||
{"learn":[0.6677212408],"iteration":195,"passed_time":7.282401129,"remaining_time":67.02781447,"test":[0.672000736]},
|
|
||||||
{"learn":[0.667704316],"iteration":196,"passed_time":7.317019235,"remaining_time":66.96744,"test":[0.6719895017]},
|
|
||||||
{"learn":[0.6676819639],"iteration":197,"passed_time":7.351194179,"remaining_time":66.90329248,"test":[0.6719725302]},
|
|
||||||
{"learn":[0.6676554448],"iteration":198,"passed_time":7.389840926,"remaining_time":66.87991712,"test":[0.6719770493]},
|
|
||||||
{"learn":[0.6676318346],"iteration":199,"passed_time":7.432994652,"remaining_time":66.89695187,"test":[0.6719667172]},
|
|
||||||
{"learn":[0.6676074705],"iteration":200,"passed_time":7.471295231,"remaining_time":66.86995085,"test":[0.6719511616]},
|
|
||||||
{"learn":[0.6675849784],"iteration":201,"passed_time":7.506377837,"remaining_time":66.8141948,"test":[0.6719427289]},
|
|
||||||
{"learn":[0.6675631744],"iteration":202,"passed_time":7.540821494,"remaining_time":66.75298633,"test":[0.6719299116]},
|
|
||||||
{"learn":[0.6675397619],"iteration":203,"passed_time":7.56808212,"remaining_time":66.62880141,"test":[0.6719106583]},
|
|
||||||
{"learn":[0.6675169086],"iteration":204,"passed_time":7.605676901,"remaining_time":66.59604896,"test":[0.6718967065]},
|
|
||||||
{"learn":[0.6674864762],"iteration":205,"passed_time":7.638300222,"remaining_time":66.51995436,"test":[0.671890967]},
|
|
||||||
{"learn":[0.6674670714],"iteration":206,"passed_time":7.665554951,"remaining_time":66.39777791,"test":[0.6718896293]},
|
|
||||||
{"learn":[0.6674375599],"iteration":207,"passed_time":7.700277678,"remaining_time":66.34085384,"test":[0.6718883534]},
|
|
||||||
{"learn":[0.6674148457],"iteration":208,"passed_time":7.734145802,"remaining_time":66.27681881,"test":[0.6718827289]},
|
|
||||||
{"learn":[0.6673974446],"iteration":209,"passed_time":7.766232144,"remaining_time":66.19788351,"test":[0.6718763224]},
|
|
||||||
{"learn":[0.6673812139],"iteration":210,"passed_time":7.796801222,"remaining_time":66.1065279,"test":[0.67187262]},
|
|
||||||
{"learn":[0.6673515687],"iteration":211,"passed_time":7.831891449,"remaining_time":66.05387693,"test":[0.6718590402]},
|
|
||||||
{"learn":[0.6673197956],"iteration":212,"passed_time":7.871259964,"remaining_time":66.0372843,"test":[0.6718455115]},
|
|
||||||
{"learn":[0.6672900754],"iteration":213,"passed_time":7.910110502,"remaining_time":66.01615587,"test":[0.6718253747]},
|
|
||||||
{"learn":[0.6672550009],"iteration":214,"passed_time":7.951342226,"remaining_time":66.01463197,"test":[0.671794877]},
|
|
||||||
{"learn":[0.6672271563],"iteration":215,"passed_time":7.989001461,"remaining_time":65.98323429,"test":[0.6717873786]},
|
|
||||||
{"learn":[0.667204521],"iteration":216,"passed_time":8.025973631,"remaining_time":65.94613357,"test":[0.6717765089]},
|
|
||||||
{"learn":[0.667181968],"iteration":217,"passed_time":8.058434478,"remaining_time":65.87215707,"test":[0.6717616726]},
|
|
||||||
{"learn":[0.6671640023],"iteration":218,"passed_time":8.087145957,"remaining_time":65.76806826,"test":[0.6717499215]},
|
|
||||||
{"learn":[0.66714351],"iteration":219,"passed_time":8.112590578,"remaining_time":65.63823286,"test":[0.6717326052]},
|
|
||||||
{"learn":[0.6671167156],"iteration":220,"passed_time":8.148644349,"remaining_time":65.59474342,"test":[0.6717161937]},
|
|
||||||
{"learn":[0.6670915937],"iteration":221,"passed_time":8.197662625,"remaining_time":65.65515382,"test":[0.6717056951]},
|
|
||||||
{"learn":[0.6670595279],"iteration":222,"passed_time":8.239228431,"remaining_time":65.65519696,"test":[0.6717021438]},
|
|
||||||
{"learn":[0.667033994],"iteration":223,"passed_time":8.268371203,"remaining_time":65.55637168,"test":[0.6716868488]},
|
|
||||||
{"learn":[0.6670008246],"iteration":224,"passed_time":8.298555216,"remaining_time":65.46638004,"test":[0.6716751909]},
|
|
||||||
{"learn":[0.6669858319],"iteration":225,"passed_time":8.327401394,"remaining_time":65.36641625,"test":[0.671670116]},
|
|
||||||
{"learn":[0.6669553964],"iteration":226,"passed_time":8.357648377,"remaining_time":65.27802014,"test":[0.6716558757]},
|
|
||||||
{"learn":[0.6669274683],"iteration":227,"passed_time":8.384989701,"remaining_time":65.16755154,"test":[0.6716559962]},
|
|
||||||
{"learn":[0.666896348],"iteration":228,"passed_time":8.418297538,"remaining_time":65.1039517,"test":[0.6716487875]},
|
|
||||||
{"learn":[0.6668698686],"iteration":229,"passed_time":8.453919972,"remaining_time":65.05842761,"test":[0.6716427451]},
|
|
||||||
{"learn":[0.6668513411],"iteration":230,"passed_time":8.49049033,"remaining_time":65.02024846,"test":[0.6716323255]},
|
|
||||||
{"learn":[0.6668309985],"iteration":231,"passed_time":8.523986676,"remaining_time":64.95865708,"test":[0.6716303547]},
|
|
||||||
{"learn":[0.6668058585],"iteration":232,"passed_time":8.550998228,"remaining_time":64.84812819,"test":[0.6716309509]},
|
|
||||||
{"learn":[0.6667845908],"iteration":233,"passed_time":8.575382398,"remaining_time":64.71848425,"test":[0.6716215401]},
|
|
||||||
{"learn":[0.6667582863],"iteration":234,"passed_time":8.607602961,"remaining_time":64.64859245,"test":[0.6716162103]},
|
|
||||||
{"learn":[0.6667332943],"iteration":235,"passed_time":8.6353786,"remaining_time":64.54579597,"test":[0.6716135097]},
|
|
||||||
{"learn":[0.6667070085],"iteration":236,"passed_time":8.66085309,"remaining_time":64.42651476,"test":[0.6716156696]},
|
|
||||||
{"learn":[0.6666907315],"iteration":237,"passed_time":8.691362456,"remaining_time":64.34529684,"test":[0.6716020054]},
|
|
||||||
{"learn":[0.6666633028],"iteration":238,"passed_time":8.719983169,"remaining_time":64.25058728,"test":[0.6715921704]},
|
|
||||||
{"learn":[0.6666406707],"iteration":239,"passed_time":8.746012652,"remaining_time":64.13742611,"test":[0.6715804466]},
|
|
||||||
{"learn":[0.6666134624],"iteration":240,"passed_time":8.773898765,"remaining_time":64.03853912,"test":[0.6715882966]},
|
|
||||||
{"learn":[0.6665850522],"iteration":241,"passed_time":8.803292064,"remaining_time":63.9511878,"test":[0.6715753942]},
|
|
||||||
{"learn":[0.6665631193],"iteration":242,"passed_time":8.833976809,"remaining_time":63.87365125,"test":[0.6715752261]},
|
|
||||||
{"learn":[0.6665412643],"iteration":243,"passed_time":8.862338006,"remaining_time":63.7797768,"test":[0.6715625509]},
|
|
||||||
{"learn":[0.6665168385],"iteration":244,"passed_time":8.892424073,"remaining_time":63.69879285,"test":[0.6715628214]},
|
|
||||||
{"learn":[0.6664904845],"iteration":245,"passed_time":8.932383667,"remaining_time":63.68862175,"test":[0.6715601629]},
|
|
||||||
{"learn":[0.6664678274],"iteration":246,"passed_time":8.962911123,"remaining_time":63.61126801,"test":[0.6715576255]},
|
|
||||||
{"learn":[0.6664539777],"iteration":247,"passed_time":8.991624872,"remaining_time":63.52147894,"test":[0.6715550274]},
|
|
||||||
{"learn":[0.6664334121],"iteration":248,"passed_time":9.021847081,"remaining_time":63.44278811,"test":[0.6715448645]},
|
|
||||||
{"learn":[0.6664121724],"iteration":249,"passed_time":9.05121341,"remaining_time":63.35849387,"test":[0.6715308166]},
|
|
||||||
{"learn":[0.666392034],"iteration":250,"passed_time":9.085113431,"remaining_time":63.30622865,"test":[0.671519334]},
|
|
||||||
{"learn":[0.666366899],"iteration":251,"passed_time":9.110250512,"remaining_time":63.19332498,"test":[0.6715184071]},
|
|
||||||
{"learn":[0.6663414098],"iteration":252,"passed_time":9.137253573,"remaining_time":63.09399997,"test":[0.6715163019]},
|
|
||||||
{"learn":[0.6663157816],"iteration":253,"passed_time":9.174559864,"remaining_time":63.06606899,"test":[0.6715096094]},
|
|
||||||
{"learn":[0.6662989799],"iteration":254,"passed_time":9.196898204,"remaining_time":62.93563673,"test":[0.6714992963]},
|
|
||||||
{"learn":[0.6662696102],"iteration":255,"passed_time":9.238149902,"remaining_time":62.9348962,"test":[0.6714917256]},
|
|
||||||
{"learn":[0.6662479711],"iteration":256,"passed_time":9.267818291,"remaining_time":62.85528125,"test":[0.671477406]},
|
|
||||||
{"learn":[0.6662231874],"iteration":257,"passed_time":9.297538986,"remaining_time":62.77640665,"test":[0.6714741542]},
|
|
||||||
{"learn":[0.6661947927],"iteration":258,"passed_time":9.324772701,"remaining_time":62.68119411,"test":[0.6714576155]},
|
|
||||||
{"learn":[0.6661669951],"iteration":259,"passed_time":9.357824574,"remaining_time":62.62544138,"test":[0.6714473645]},
|
|
||||||
{"learn":[0.6661426137],"iteration":260,"passed_time":9.388345461,"remaining_time":62.55299907,"test":[0.6714427232]},
|
|
||||||
{"learn":[0.6661216749],"iteration":261,"passed_time":9.427290804,"remaining_time":62.53676114,"test":[0.6714364275]},
|
|
||||||
{"learn":[0.6660983123],"iteration":262,"passed_time":9.461913185,"remaining_time":62.49179925,"test":[0.6714339587]},
|
|
||||||
{"learn":[0.6660803402],"iteration":263,"passed_time":9.496090562,"remaining_time":62.44398945,"test":[0.6714336287]},
|
|
||||||
{"learn":[0.6660617842],"iteration":264,"passed_time":9.524189317,"remaining_time":62.35648477,"test":[0.6714283568]},
|
|
||||||
{"learn":[0.6660443878],"iteration":265,"passed_time":9.55372419,"remaining_time":62.27878852,"test":[0.6714271895]},
|
|
||||||
{"learn":[0.6660176079],"iteration":266,"passed_time":9.590356068,"remaining_time":62.2475171,"test":[0.671413471]},
|
|
||||||
{"learn":[0.6659967546],"iteration":267,"passed_time":9.620235131,"remaining_time":62.17256436,"test":[0.6714072396]},
|
|
||||||
{"learn":[0.6659751467],"iteration":268,"passed_time":9.645948482,"remaining_time":62.0711406,"test":[0.6714002677]},
|
|
||||||
{"learn":[0.6659539329],"iteration":269,"passed_time":9.682675077,"remaining_time":62.04084401,"test":[0.6714001163]},
|
|
||||||
{"learn":[0.6659263951],"iteration":270,"passed_time":9.711914203,"remaining_time":61.96272936,"test":[0.6713933952]},
|
|
||||||
{"learn":[0.6659038921],"iteration":271,"passed_time":9.739142426,"remaining_time":61.87219894,"test":[0.6713926761]},
|
|
||||||
{"learn":[0.6658767418],"iteration":272,"passed_time":9.768751964,"remaining_time":61.79719649,"test":[0.6713836619]},
|
|
||||||
{"learn":[0.6658510507],"iteration":273,"passed_time":9.804576737,"remaining_time":61.76167682,"test":[0.6713772112]},
|
|
||||||
{"learn":[0.6658210119],"iteration":274,"passed_time":9.848653906,"remaining_time":61.77791996,"test":[0.6713603715]},
|
|
||||||
{"learn":[0.6657963011],"iteration":275,"passed_time":9.88663261,"remaining_time":61.75563268,"test":[0.6713560246]},
|
|
||||||
{"learn":[0.6657748552],"iteration":276,"passed_time":9.925808942,"remaining_time":61.74068161,"test":[0.6713837913]},
|
|
||||||
{"learn":[0.6657490013],"iteration":277,"passed_time":9.965409489,"remaining_time":61.72818396,"test":[0.6713684274]},
|
|
||||||
{"learn":[0.665732402],"iteration":278,"passed_time":9.99537326,"remaining_time":61.65604796,"test":[0.6713619356]},
|
|
||||||
{"learn":[0.6657118786],"iteration":279,"passed_time":10.02216777,"remaining_time":61.5647449,"test":[0.6713584836]},
|
|
||||||
{"learn":[0.665684467],"iteration":280,"passed_time":10.05593393,"remaining_time":61.51654955,"test":[0.6713673572]},
|
|
||||||
{"learn":[0.6656584634],"iteration":281,"passed_time":10.08025153,"remaining_time":61.41089406,"test":[0.6713625568]},
|
|
||||||
{"learn":[0.6656309991],"iteration":282,"passed_time":10.11102202,"remaining_time":61.34496401,"test":[0.6713542652]},
|
|
||||||
{"learn":[0.6656073482],"iteration":283,"passed_time":10.14714598,"remaining_time":61.31162855,"test":[0.6713512017]},
|
|
||||||
{"learn":[0.6655890957],"iteration":284,"passed_time":10.17528061,"remaining_time":61.23019734,"test":[0.671342038]},
|
|
||||||
{"learn":[0.6655665563],"iteration":285,"passed_time":10.2021403,"remaining_time":61.14149818,"test":[0.6713279798]},
|
|
||||||
{"learn":[0.6655452454],"iteration":286,"passed_time":10.23423432,"remaining_time":61.08447174,"test":[0.6713123285]},
|
|
||||||
{"learn":[0.6655255286],"iteration":287,"passed_time":10.26481698,"remaining_time":61.0186343,"test":[0.6713035326]},
|
|
||||||
{"learn":[0.6655053548],"iteration":288,"passed_time":10.29945844,"remaining_time":60.97707056,"test":[0.6713022203]},
|
|
||||||
{"learn":[0.6654893396],"iteration":289,"passed_time":10.32366496,"remaining_time":60.87402441,"test":[0.671296041]},
|
|
||||||
{"learn":[0.6654648912],"iteration":290,"passed_time":10.35344703,"remaining_time":60.80426453,"test":[0.6712829551]},
|
|
||||||
{"learn":[0.6654442759],"iteration":291,"passed_time":10.3949915,"remaining_time":60.8035804,"test":[0.6712769751]},
|
|
||||||
{"learn":[0.6654173127],"iteration":292,"passed_time":10.43148765,"remaining_time":60.77320621,"test":[0.6712702915]},
|
|
||||||
{"learn":[0.6653914518],"iteration":293,"passed_time":10.47162738,"remaining_time":60.76393303,"test":[0.6712379343]},
|
|
||||||
{"learn":[0.6653648946],"iteration":294,"passed_time":10.50360107,"remaining_time":60.70725362,"test":[0.6712192006]},
|
|
||||||
{"learn":[0.665344141],"iteration":295,"passed_time":10.53460819,"remaining_time":60.64517686,"test":[0.6712074061]},
|
|
||||||
{"learn":[0.6653140817],"iteration":296,"passed_time":10.57659448,"remaining_time":60.64626395,"test":[0.6711953324]},
|
|
||||||
{"learn":[0.665295365],"iteration":297,"passed_time":10.61260262,"remaining_time":60.61291829,"test":[0.6711891001]},
|
|
||||||
{"learn":[0.6652787488],"iteration":298,"passed_time":10.63910358,"remaining_time":60.52546889,"test":[0.6711870526]},
|
|
||||||
{"learn":[0.6652502991],"iteration":299,"passed_time":10.6681867,"remaining_time":60.45305797,"test":[0.6711812809]},
|
|
||||||
{"learn":[0.665231168],"iteration":300,"passed_time":10.70260503,"remaining_time":60.41104967,"test":[0.6711768946]},
|
|
||||||
{"learn":[0.6652136682],"iteration":301,"passed_time":10.72952096,"remaining_time":60.32690925,"test":[0.6711845012]},
|
|
||||||
{"learn":[0.6651903001],"iteration":302,"passed_time":10.76489952,"remaining_time":60.29054288,"test":[0.6711869636]},
|
|
||||||
{"learn":[0.6651697153],"iteration":303,"passed_time":10.80197155,"remaining_time":60.26363073,"test":[0.671186884]},
|
|
||||||
{"learn":[0.6651525958],"iteration":304,"passed_time":10.82922271,"remaining_time":60.18207375,"test":[0.6711890401]},
|
|
||||||
{"learn":[0.6651322685],"iteration":305,"passed_time":10.8578399,"remaining_time":60.10843394,"test":[0.6711868603]},
|
|
||||||
{"learn":[0.6651113828],"iteration":306,"passed_time":10.89228879,"remaining_time":60.06724727,"test":[0.6711900892]},
|
|
||||||
{"learn":[0.6650886807],"iteration":307,"passed_time":10.93056436,"remaining_time":60.04712628,"test":[0.6711884242]},
|
|
||||||
{"learn":[0.6650622251],"iteration":308,"passed_time":10.97231236,"remaining_time":60.04589061,"test":[0.6711837119]},
|
|
||||||
{"learn":[0.6650429987],"iteration":309,"passed_time":11.00296848,"remaining_time":59.98392494,"test":[0.6711766645]},
|
|
||||||
{"learn":[0.665015513],"iteration":310,"passed_time":11.03002276,"remaining_time":59.90259947,"test":[0.671172959]},
|
|
||||||
{"learn":[0.6650019022],"iteration":311,"passed_time":11.05828865,"remaining_time":59.82817707,"test":[0.6711740433]},
|
|
||||||
{"learn":[0.664979951],"iteration":312,"passed_time":11.09287745,"remaining_time":59.78812863,"test":[0.6711715069]},
|
|
||||||
{"learn":[0.6649549638],"iteration":313,"passed_time":11.1177757,"remaining_time":59.69608229,"test":[0.6711589843]},
|
|
||||||
{"learn":[0.6649340455],"iteration":314,"passed_time":11.14959087,"remaining_time":59.64146228,"test":[0.6711446402]},
|
|
||||||
{"learn":[0.6649162445],"iteration":315,"passed_time":11.18718772,"remaining_time":59.61779784,"test":[0.6711415366]},
|
|
||||||
{"learn":[0.6649048119],"iteration":316,"passed_time":11.21179073,"remaining_time":59.52505932,"test":[0.6711359351]},
|
|
||||||
{"learn":[0.6648796463],"iteration":317,"passed_time":11.24311165,"remaining_time":59.46828238,"test":[0.671143361]},
|
|
||||||
{"learn":[0.6648605481],"iteration":318,"passed_time":11.27486028,"remaining_time":59.41391889,"test":[0.6711353638]},
|
|
||||||
{"learn":[0.6648429084],"iteration":319,"passed_time":11.30400807,"remaining_time":59.34604237,"test":[0.6711444387]},
|
|
||||||
{"learn":[0.6648238121],"iteration":320,"passed_time":11.33488419,"remaining_time":59.28744721,"test":[0.6711487352]},
|
|
||||||
{"learn":[0.6647969527],"iteration":321,"passed_time":11.36208838,"remaining_time":59.20988915,"test":[0.67114436]},
|
|
||||||
{"learn":[0.6647854723],"iteration":322,"passed_time":11.39429642,"remaining_time":59.15862259,"test":[0.6711444722]},
|
|
||||||
{"learn":[0.6647589304],"iteration":323,"passed_time":11.4363998,"remaining_time":59.15866068,"test":[0.6711325635]},
|
|
||||||
{"learn":[0.6647429024],"iteration":324,"passed_time":11.47751019,"remaining_time":59.15332173,"test":[0.6711269403]},
|
|
||||||
{"learn":[0.6647237508],"iteration":325,"passed_time":11.5136833,"remaining_time":59.12241054,"test":[0.6711154078]},
|
|
||||||
{"learn":[0.6647059396],"iteration":326,"passed_time":11.54795566,"remaining_time":59.08174257,"test":[0.6711203043]},
|
|
||||||
{"learn":[0.664686288],"iteration":327,"passed_time":11.57245915,"remaining_time":58.99131613,"test":[0.6711241333]},
|
|
||||||
{"learn":[0.6646532527],"iteration":328,"passed_time":11.60790333,"remaining_time":58.95685857,"test":[0.6711213497]},
|
|
||||||
{"learn":[0.6646306438],"iteration":329,"passed_time":11.63787346,"remaining_time":58.89469298,"test":[0.6711231641]},
|
|
||||||
{"learn":[0.6646098516],"iteration":330,"passed_time":11.66805718,"remaining_time":58.83379887,"test":[0.6711049215]},
|
|
||||||
{"learn":[0.6645858284],"iteration":331,"passed_time":11.70070223,"remaining_time":58.78545579,"test":[0.6711031963]},
|
|
||||||
{"learn":[0.6645707188],"iteration":332,"passed_time":11.724753,"remaining_time":58.69418391,"test":[0.6710996314]},
|
|
||||||
{"learn":[0.6645485788],"iteration":333,"passed_time":11.75795297,"remaining_time":58.64895104,"test":[0.6710867309]},
|
|
||||||
{"learn":[0.6645305696],"iteration":334,"passed_time":11.78053066,"remaining_time":58.55099567,"test":[0.6710914578]},
|
|
||||||
{"learn":[0.6645108881],"iteration":335,"passed_time":11.81570271,"remaining_time":58.51586106,"test":[0.6710929585]},
|
|
||||||
{"learn":[0.6644923286],"iteration":336,"passed_time":11.8448851,"remaining_time":58.45116888,"test":[0.6710984779]},
|
|
||||||
{"learn":[0.6644805222],"iteration":337,"passed_time":11.86964023,"remaining_time":58.36491734,"test":[0.6710923199]},
|
|
||||||
{"learn":[0.6644572776],"iteration":338,"passed_time":11.90591446,"remaining_time":58.33546879,"test":[0.6710893917]},
|
|
||||||
{"learn":[0.6644320741],"iteration":339,"passed_time":11.94145444,"remaining_time":58.30239521,"test":[0.6710923306]},
|
|
||||||
{"learn":[0.6644115048],"iteration":340,"passed_time":11.98658051,"remaining_time":58.31594449,"test":[0.6710927901]},
|
|
||||||
{"learn":[0.6643949013],"iteration":341,"passed_time":12.02038848,"remaining_time":58.27428098,"test":[0.6711092802]},
|
|
||||||
{"learn":[0.6643619789],"iteration":342,"passed_time":12.06653941,"remaining_time":58.29229096,"test":[0.6711012995]},
|
|
||||||
{"learn":[0.6643389502],"iteration":343,"passed_time":12.12283646,"remaining_time":58.35877087,"test":[0.6711015305]},
|
|
||||||
{"learn":[0.6643088915],"iteration":344,"passed_time":12.17733618,"remaining_time":58.41591705,"test":[0.6710975574]},
|
|
||||||
{"learn":[0.664286972],"iteration":345,"passed_time":12.22133732,"remaining_time":58.42223099,"test":[0.6710899474]},
|
|
||||||
{"learn":[0.664274149],"iteration":346,"passed_time":12.2642467,"remaining_time":58.42305415,"test":[0.671085152]},
|
|
||||||
{"learn":[0.6642536926],"iteration":347,"passed_time":12.30091895,"remaining_time":58.39401755,"test":[0.6710814533]},
|
|
||||||
{"learn":[0.6642357634],"iteration":348,"passed_time":12.32484094,"remaining_time":58.30462002,"test":[0.6710701892]},
|
|
||||||
{"learn":[0.664207914],"iteration":349,"passed_time":12.35469303,"remaining_time":58.24355287,"test":[0.67105503]},
|
|
||||||
{"learn":[0.6641853097],"iteration":350,"passed_time":12.40148755,"remaining_time":58.26225919,"test":[0.6710527861]},
|
|
||||||
{"learn":[0.6641654917],"iteration":351,"passed_time":12.43803877,"remaining_time":58.23263605,"test":[0.6710508715]},
|
|
||||||
{"learn":[0.664143804],"iteration":352,"passed_time":12.47995438,"remaining_time":58.22800245,"test":[0.6710560803]},
|
|
||||||
{"learn":[0.6641290647],"iteration":353,"passed_time":12.51241326,"remaining_time":58.17918707,"test":[0.6710465693]},
|
|
||||||
{"learn":[0.6641117244],"iteration":354,"passed_time":12.5417829,"remaining_time":58.11614893,"test":[0.6710440741]},
|
|
||||||
{"learn":[0.6640880219],"iteration":355,"passed_time":12.5692936,"remaining_time":58.0447154,"test":[0.6710496913]},
|
|
||||||
{"learn":[0.6640669415],"iteration":356,"passed_time":12.5976392,"remaining_time":57.97737034,"test":[0.6710404659]},
|
|
||||||
{"learn":[0.6640462999],"iteration":357,"passed_time":12.62815847,"remaining_time":57.92021287,"test":[0.6710293986]},
|
|
||||||
{"learn":[0.664030296],"iteration":358,"passed_time":12.65342509,"remaining_time":57.8391938,"test":[0.6710353817]},
|
|
||||||
{"learn":[0.6640028542],"iteration":359,"passed_time":12.68233453,"remaining_time":57.77507954,"test":[0.6710271815]},
|
|
||||||
{"learn":[0.6639813347],"iteration":360,"passed_time":12.72037964,"remaining_time":57.75263774,"test":[0.6710288077]},
|
|
||||||
{"learn":[0.6639597941],"iteration":361,"passed_time":12.744473,"remaining_time":57.66698004,"test":[0.6710169894]},
|
|
||||||
{"learn":[0.6639429832],"iteration":362,"passed_time":12.77086568,"remaining_time":57.59203063,"test":[0.6710119848]},
|
|
||||||
{"learn":[0.6639222708],"iteration":363,"passed_time":12.81194554,"remaining_time":57.58335961,"test":[0.6710114775]},
|
|
||||||
{"learn":[0.6639065546],"iteration":364,"passed_time":12.84133287,"remaining_time":57.52213492,"test":[0.6710013614]},
|
|
||||||
{"learn":[0.6638823236],"iteration":365,"passed_time":12.87057337,"remaining_time":57.46042866,"test":[0.6709985657]},
|
|
||||||
{"learn":[0.6638648195],"iteration":366,"passed_time":12.8971183,"remaining_time":57.38690512,"test":[0.6709948954]},
|
|
||||||
{"learn":[0.6638436235],"iteration":367,"passed_time":12.93825161,"remaining_time":57.37833324,"test":[0.6709970591]},
|
|
||||||
{"learn":[0.6638208732],"iteration":368,"passed_time":12.97444296,"remaining_time":57.3477411,"test":[0.6709739289]},
|
|
||||||
{"learn":[0.6637956357],"iteration":369,"passed_time":13.00974924,"remaining_time":57.31321963,"test":[0.6709754911]},
|
|
||||||
{"learn":[0.6637718453],"iteration":370,"passed_time":13.03832239,"remaining_time":57.24912984,"test":[0.6709717066]},
|
|
||||||
{"learn":[0.663756918],"iteration":371,"passed_time":13.07843077,"remaining_time":57.23571316,"test":[0.67096845]},
|
|
||||||
{"learn":[0.6637353525],"iteration":372,"passed_time":13.11729124,"remaining_time":57.21671005,"test":[0.6709739445]},
|
|
||||||
{"learn":[0.6637143112],"iteration":373,"passed_time":13.14745329,"remaining_time":57.15978354,"test":[0.6709728881]},
|
|
||||||
{"learn":[0.6636956547],"iteration":374,"passed_time":13.18118022,"remaining_time":57.11844761,"test":[0.6709694284]},
|
|
||||||
{"learn":[0.663680995],"iteration":375,"passed_time":13.20539229,"remaining_time":57.03605604,"test":[0.6709604166]},
|
|
||||||
{"learn":[0.66366728],"iteration":376,"passed_time":13.23563977,"remaining_time":56.97995583,"test":[0.6709605025]},
|
|
||||||
{"learn":[0.6636487567],"iteration":377,"passed_time":13.27428255,"remaining_time":56.96001665,"test":[0.6709603727]},
|
|
||||||
{"learn":[0.6636266904],"iteration":378,"passed_time":13.30625754,"remaining_time":56.91146033,"test":[0.670944339]},
|
|
||||||
{"learn":[0.6636116064],"iteration":379,"passed_time":13.33327871,"remaining_time":56.84187241,"test":[0.6709447187]},
|
|
||||||
{"learn":[0.6635902746],"iteration":380,"passed_time":13.36632239,"remaining_time":56.79809961,"test":[0.6709538679]},
|
|
||||||
{"learn":[0.6635654896],"iteration":381,"passed_time":13.39639051,"remaining_time":56.74177969,"test":[0.6709640912]},
|
|
||||||
{"learn":[0.6635393029],"iteration":382,"passed_time":13.42189438,"remaining_time":56.66632694,"test":[0.6709534847]},
|
|
||||||
{"learn":[0.6635171734],"iteration":383,"passed_time":13.46730432,"remaining_time":56.6749057,"test":[0.6709471555]},
|
|
||||||
{"learn":[0.663500789],"iteration":384,"passed_time":13.50832777,"remaining_time":56.66480351,"test":[0.6709506783]},
|
|
||||||
{"learn":[0.663477743],"iteration":385,"passed_time":13.54029627,"remaining_time":56.61667921,"test":[0.6709546729]},
|
|
||||||
{"learn":[0.6634584806],"iteration":386,"passed_time":13.56996301,"remaining_time":56.5590448,"test":[0.670930774]},
|
|
||||||
{"learn":[0.6634337499],"iteration":387,"passed_time":13.59835745,"remaining_time":56.4962686,"test":[0.6709287322]},
|
|
||||||
{"learn":[0.6634135584],"iteration":388,"passed_time":13.6279617,"remaining_time":56.43867943,"test":[0.6709198643]},
|
|
||||||
{"learn":[0.6633868455],"iteration":389,"passed_time":13.65633448,"remaining_time":56.37615005,"test":[0.6709220389]},
|
|
||||||
{"learn":[0.6633755323],"iteration":390,"passed_time":13.68565529,"remaining_time":56.31769658,"test":[0.6709230923]},
|
|
||||||
{"learn":[0.663356103],"iteration":391,"passed_time":13.71789303,"remaining_time":56.27135714,"test":[0.670930414]},
|
|
||||||
{"learn":[0.6633337631],"iteration":392,"passed_time":13.75060752,"remaining_time":56.2270389,"test":[0.6709354296]},
|
|
||||||
{"learn":[0.663319422],"iteration":393,"passed_time":13.77167974,"remaining_time":56.13532403,"test":[0.6709351544]},
|
|
||||||
{"learn":[0.6632911566],"iteration":394,"passed_time":13.80416242,"remaining_time":56.09033084,"test":[0.6709414935]},
|
|
||||||
{"learn":[0.6632687875],"iteration":395,"passed_time":13.82525369,"remaining_time":55.9992599,"test":[0.6709445943]},
|
|
||||||
{"learn":[0.6632431997],"iteration":396,"passed_time":13.85836516,"remaining_time":55.95707646,"test":[0.6709475685]},
|
|
||||||
{"learn":[0.6632189331],"iteration":397,"passed_time":13.88898168,"remaining_time":55.90489613,"test":[0.6709533591]},
|
|
||||||
{"learn":[0.663201035],"iteration":398,"passed_time":13.91726355,"remaining_time":55.84345598,"test":[0.6709592222]},
|
|
||||||
{"learn":[0.6631898553],"iteration":399,"passed_time":13.95316828,"remaining_time":55.81267311,"test":[0.6709508704]},
|
|
||||||
{"learn":[0.6631712482],"iteration":400,"passed_time":13.99418497,"remaining_time":55.80224881,"test":[0.6709479912]},
|
|
||||||
{"learn":[0.663143025],"iteration":401,"passed_time":14.0253575,"remaining_time":55.75254052,"test":[0.6709417519]},
|
|
||||||
{"learn":[0.663121538],"iteration":402,"passed_time":14.04844239,"remaining_time":55.67087467,"test":[0.6709476082]},
|
|
||||||
{"learn":[0.6631087792],"iteration":403,"passed_time":14.0761289,"remaining_time":55.60767753,"test":[0.6709480979]},
|
|
||||||
{"learn":[0.6630859067],"iteration":404,"passed_time":14.10555105,"remaining_time":55.55149118,"test":[0.6709448724]},
|
|
||||||
{"learn":[0.663066483],"iteration":405,"passed_time":14.1427661,"remaining_time":55.52603242,"test":[0.6709421934]},
|
|
||||||
{"learn":[0.6630443652],"iteration":406,"passed_time":14.18285552,"remaining_time":55.51176619,"test":[0.6709386261]},
|
|
||||||
{"learn":[0.6630250376],"iteration":407,"passed_time":14.21458769,"remaining_time":55.46476372,"test":[0.6709461564]},
|
|
||||||
{"learn":[0.6630007822],"iteration":408,"passed_time":14.24035708,"remaining_time":55.39464088,"test":[0.670934384]},
|
|
||||||
{"learn":[0.6629768728],"iteration":409,"passed_time":14.26711915,"remaining_time":55.32858403,"test":[0.6709312987]},
|
|
||||||
{"learn":[0.6629528093],"iteration":410,"passed_time":14.29943785,"remaining_time":55.28420133,"test":[0.670931806]},
|
|
||||||
{"learn":[0.6629260936],"iteration":411,"passed_time":14.32489173,"remaining_time":55.21341763,"test":[0.6709286111]},
|
|
||||||
{"learn":[0.6629102182],"iteration":412,"passed_time":14.35119075,"remaining_time":55.14610101,"test":[0.6709224729]},
|
|
||||||
{"learn":[0.6628863488],"iteration":413,"passed_time":14.37946054,"remaining_time":55.08653242,"test":[0.6709236504]},
|
|
||||||
{"learn":[0.6628648972],"iteration":414,"passed_time":14.41005914,"remaining_time":55.03600899,"test":[0.6709245901]},
|
|
||||||
{"learn":[0.6628454339],"iteration":415,"passed_time":14.45103793,"remaining_time":55.02510598,"test":[0.6709463437]},
|
|
||||||
{"learn":[0.6628200274],"iteration":416,"passed_time":14.48428995,"remaining_time":54.98472661,"test":[0.6709567049]},
|
|
||||||
{"learn":[0.6627942591],"iteration":417,"passed_time":14.5135184,"remaining_time":54.92915339,"test":[0.670945606]},
|
|
||||||
{"learn":[0.6627744647],"iteration":418,"passed_time":14.53698524,"remaining_time":54.85196578,"test":[0.6709479298]},
|
|
||||||
{"learn":[0.662765485],"iteration":419,"passed_time":14.56542473,"remaining_time":54.79374067,"test":[0.6709464351]},
|
|
||||||
{"learn":[0.6627503257],"iteration":420,"passed_time":14.58728594,"remaining_time":54.71098455,"test":[0.6709414048]},
|
|
||||||
{"learn":[0.6627323029],"iteration":421,"passed_time":14.61501375,"remaining_time":54.65045425,"test":[0.6709414427]},
|
|
||||||
{"learn":[0.6627111509],"iteration":422,"passed_time":14.64231614,"remaining_time":54.58849302,"test":[0.6709296343]},
|
|
||||||
{"learn":[0.6626785863],"iteration":423,"passed_time":14.66665432,"remaining_time":54.51567739,"test":[0.670924721]},
|
|
||||||
{"learn":[0.6626576561],"iteration":424,"passed_time":14.69050441,"remaining_time":54.44128104,"test":[0.670906284]},
|
|
||||||
{"learn":[0.6626363113],"iteration":425,"passed_time":14.71910475,"remaining_time":54.38467341,"test":[0.6708996826]},
|
|
||||||
{"learn":[0.6626181065],"iteration":426,"passed_time":14.73941058,"remaining_time":54.2976413,"test":[0.6708987677]},
|
|
||||||
{"learn":[0.66259794],"iteration":427,"passed_time":14.77242451,"remaining_time":54.25759657,"test":[0.670909526]},
|
|
||||||
{"learn":[0.6625765658],"iteration":428,"passed_time":14.79088688,"remaining_time":54.1642967,"test":[0.6709033226]},
|
|
||||||
{"learn":[0.6625526572],"iteration":429,"passed_time":14.82430966,"remaining_time":54.12596783,"test":[0.6708750209]},
|
|
||||||
{"learn":[0.66253135],"iteration":430,"passed_time":14.84439175,"remaining_time":54.03909666,"test":[0.6708752079]},
|
|
||||||
{"learn":[0.6625035695],"iteration":431,"passed_time":14.8764415,"remaining_time":53.99597284,"test":[0.6708776566]},
|
|
||||||
{"learn":[0.662480212],"iteration":432,"passed_time":14.90666075,"remaining_time":53.94627573,"test":[0.6708736133]},
|
|
||||||
{"learn":[0.6624611632],"iteration":433,"passed_time":14.93845927,"remaining_time":53.90236684,"test":[0.6708754298]},
|
|
||||||
{"learn":[0.6624332625],"iteration":434,"passed_time":14.98024104,"remaining_time":53.89443041,"test":[0.6708751084]},
|
|
||||||
{"learn":[0.6624120584],"iteration":435,"passed_time":15.00605075,"remaining_time":53.82904442,"test":[0.6708642042]},
|
|
||||||
{"learn":[0.6623941719],"iteration":436,"passed_time":15.03384083,"remaining_time":53.77092268,"test":[0.6708610465]},
|
|
||||||
{"learn":[0.6623766304],"iteration":437,"passed_time":15.05972545,"remaining_time":53.70614417,"test":[0.6708574768]},
|
|
||||||
{"learn":[0.6623623329],"iteration":438,"passed_time":15.08505889,"remaining_time":53.63958297,"test":[0.6708557953]},
|
|
||||||
{"learn":[0.6623442925],"iteration":439,"passed_time":15.11080547,"remaining_time":53.57467393,"test":[0.670871378]},
|
|
||||||
{"learn":[0.6623212715],"iteration":440,"passed_time":15.13466304,"remaining_time":53.50326458,"test":[0.6708640187]},
|
|
||||||
{"learn":[0.6623025941],"iteration":441,"passed_time":15.16037021,"remaining_time":53.43859001,"test":[0.6708700565]},
|
|
||||||
{"learn":[0.6622749791],"iteration":442,"passed_time":15.18471062,"remaining_time":53.36928767,"test":[0.6708667534]},
|
|
||||||
{"learn":[0.6622534499],"iteration":443,"passed_time":15.21140556,"remaining_time":53.30843931,"test":[0.6708675383]},
|
|
||||||
{"learn":[0.6622305473],"iteration":444,"passed_time":15.23498219,"remaining_time":53.23684787,"test":[0.6708740175]},
|
|
||||||
{"learn":[0.6622059333],"iteration":445,"passed_time":15.26647355,"remaining_time":53.19304911,"test":[0.6708774523]},
|
|
||||||
{"learn":[0.6621871707],"iteration":446,"passed_time":15.28793136,"remaining_time":53.11444609,"test":[0.6708697231]},
|
|
||||||
{"learn":[0.6621638454],"iteration":447,"passed_time":15.31613827,"remaining_time":53.05947899,"test":[0.6708614971]},
|
|
||||||
{"learn":[0.6621511296],"iteration":448,"passed_time":15.33689091,"remaining_time":52.9788815,"test":[0.6708607946]},
|
|
||||||
{"learn":[0.6621349978],"iteration":449,"passed_time":15.36674634,"remaining_time":52.92990406,"test":[0.6708740865]},
|
|
||||||
{"learn":[0.6621120424],"iteration":450,"passed_time":15.393642,"remaining_time":52.87084582,"test":[0.6708729562]},
|
|
||||||
{"learn":[0.6620958271],"iteration":451,"passed_time":15.42984657,"remaining_time":52.84381082,"test":[0.6708674017]},
|
|
||||||
{"learn":[0.6620793528],"iteration":452,"passed_time":15.46956188,"remaining_time":52.82872456,"test":[0.6708693088]},
|
|
||||||
{"learn":[0.6620572713],"iteration":453,"passed_time":15.49032259,"remaining_time":52.74898396,"test":[0.6708712037]},
|
|
||||||
{"learn":[0.6620395025],"iteration":454,"passed_time":15.52379393,"remaining_time":52.71266289,"test":[0.6708703905]},
|
|
||||||
{"learn":[0.6620188044],"iteration":455,"passed_time":15.55053135,"remaining_time":52.65355352,"test":[0.6708577595]},
|
|
||||||
{"learn":[0.6620017347],"iteration":456,"passed_time":15.57735398,"remaining_time":52.59487352,"test":[0.6708493546]},
|
|
||||||
{"learn":[0.6619811454],"iteration":457,"passed_time":15.60434803,"remaining_time":52.53690973,"test":[0.6708523777]},
|
|
||||||
{"learn":[0.6619695569],"iteration":458,"passed_time":15.63056555,"remaining_time":52.47647387,"test":[0.6708454134]},
|
|
||||||
{"learn":[0.661952377],"iteration":459,"passed_time":15.656355,"remaining_time":52.41475368,"test":[0.6708404483]},
|
|
||||||
{"learn":[0.6619237442],"iteration":460,"passed_time":15.68232112,"remaining_time":52.35377918,"test":[0.6708274771]},
|
|
||||||
{"learn":[0.6619089407],"iteration":461,"passed_time":15.71164945,"remaining_time":52.30414904,"test":[0.6708244992]},
|
|
||||||
{"learn":[0.6618886168],"iteration":462,"passed_time":15.7361944,"remaining_time":52.23872743,"test":[0.6708344314]},
|
|
||||||
{"learn":[0.6618831383],"iteration":463,"passed_time":15.76527735,"remaining_time":52.18850433,"test":[0.6708279081]},
|
|
||||||
{"learn":[0.6618690774],"iteration":464,"passed_time":15.78652262,"remaining_time":52.11249942,"test":[0.6708258106]},
|
|
||||||
{"learn":[0.661845878],"iteration":465,"passed_time":15.81756836,"remaining_time":52.06899113,"test":[0.6708049714]},
|
|
||||||
{"learn":[0.6618290213],"iteration":466,"passed_time":15.83979966,"remaining_time":51.99660146,"test":[0.670810989]},
|
|
||||||
{"learn":[0.6618050064],"iteration":467,"passed_time":15.87342473,"remaining_time":51.9617237,"test":[0.6708212237]},
|
|
||||||
{"learn":[0.6617832833],"iteration":468,"passed_time":15.90381555,"remaining_time":51.9162934,"test":[0.6708221741]},
|
|
||||||
{"learn":[0.6617652311],"iteration":469,"passed_time":15.93502938,"remaining_time":51.87360627,"test":[0.6708259658]},
|
|
||||||
{"learn":[0.6617443144],"iteration":470,"passed_time":15.96919221,"remaining_time":51.84054117,"test":[0.6708159692]},
|
|
||||||
{"learn":[0.6617202619],"iteration":471,"passed_time":15.99477329,"remaining_time":51.77968981,"test":[0.6708136212]},
|
|
||||||
{"learn":[0.6617005831],"iteration":472,"passed_time":16.02279091,"remaining_time":51.72685354,"test":[0.6708224942]},
|
|
||||||
{"learn":[0.6616824419],"iteration":473,"passed_time":16.04763422,"remaining_time":51.66390258,"test":[0.6708363084]},
|
|
||||||
{"learn":[0.6616538226],"iteration":474,"passed_time":16.07374645,"remaining_time":51.60518598,"test":[0.670850875]},
|
|
||||||
{"learn":[0.6616314155],"iteration":475,"passed_time":16.09993591,"remaining_time":51.54685363,"test":[0.6708527236]},
|
|
||||||
{"learn":[0.6616127861],"iteration":476,"passed_time":16.12811357,"remaining_time":51.49500411,"test":[0.6708453401]},
|
|
||||||
{"learn":[0.6616029072],"iteration":477,"passed_time":16.15264086,"remaining_time":51.43163051,"test":[0.6708413844]},
|
|
||||||
{"learn":[0.6615843751],"iteration":478,"passed_time":16.17696751,"remaining_time":51.36778201,"test":[0.6708364569]},
|
|
||||||
{"learn":[0.661563216],"iteration":479,"passed_time":16.20551145,"remaining_time":51.31745293,"test":[0.6708251774]},
|
|
||||||
{"learn":[0.6615432257],"iteration":480,"passed_time":16.22860577,"remaining_time":51.2500045,"test":[0.6708154393]},
|
|
||||||
{"learn":[0.6615263324],"iteration":481,"passed_time":16.25544093,"remaining_time":51.19452144,"test":[0.6708111613]},
|
|
||||||
{"learn":[0.6615033259],"iteration":482,"passed_time":16.27729221,"remaining_time":51.12350369,"test":[0.6708102339]},
|
|
||||||
{"learn":[0.661484293],"iteration":483,"passed_time":16.30502335,"remaining_time":51.07110619,"test":[0.6707929623]},
|
|
||||||
{"learn":[0.6614678231],"iteration":484,"passed_time":16.32842702,"remaining_time":51.00529266,"test":[0.6707900226]},
|
|
||||||
{"learn":[0.6614463024],"iteration":485,"passed_time":16.36272839,"remaining_time":50.97360242,"test":[0.6707832384]},
|
|
||||||
{"learn":[0.6614155436],"iteration":486,"passed_time":16.39272506,"remaining_time":50.92852776,"test":[0.6707739118]},
|
|
||||||
{"learn":[0.6613958945],"iteration":487,"passed_time":16.42636604,"remaining_time":50.89480625,"test":[0.6707737538]},
|
|
||||||
{"learn":[0.661380611],"iteration":488,"passed_time":16.4597142,"remaining_time":50.86018027,"test":[0.6707730234]},
|
|
||||||
{"learn":[0.6613677802],"iteration":489,"passed_time":16.48056007,"remaining_time":50.78703206,"test":[0.6707796291]},
|
|
||||||
{"learn":[0.6613530086],"iteration":490,"passed_time":16.51091177,"remaining_time":50.74331132,"test":[0.670791408]},
|
|
||||||
{"learn":[0.6613248211],"iteration":491,"passed_time":16.53097438,"remaining_time":50.66810846,"test":[0.6707944906]},
|
|
||||||
{"learn":[0.6613059359],"iteration":492,"passed_time":16.56161402,"remaining_time":50.62546112,"test":[0.6707835635]},
|
|
||||||
{"learn":[0.6612729965],"iteration":493,"passed_time":16.5854633,"remaining_time":50.56216139,"test":[0.6707908928]},
|
|
||||||
{"learn":[0.6612624948],"iteration":494,"passed_time":16.61302735,"remaining_time":50.51031547,"test":[0.670796262]},
|
|
||||||
{"learn":[0.6612401679],"iteration":495,"passed_time":16.63896978,"remaining_time":50.45365029,"test":[0.6707877825]},
|
|
||||||
{"learn":[0.6612191637],"iteration":496,"passed_time":16.663707,"remaining_time":50.39346403,"test":[0.6707854132]},
|
|
||||||
{"learn":[0.6611912219],"iteration":497,"passed_time":16.69040179,"remaining_time":50.33932428,"test":[0.6707756206]},
|
|
||||||
{"learn":[0.6611773017],"iteration":498,"passed_time":16.71612789,"remaining_time":50.28238068,"test":[0.6707707899]},
|
|
||||||
{"learn":[0.6611638216],"iteration":499,"passed_time":16.74072553,"remaining_time":50.2221766,"test":[0.6707704386]},
|
|
||||||
{"learn":[0.6611450533],"iteration":500,"passed_time":16.77346538,"remaining_time":50.18647626,"test":[0.6707621465]},
|
|
||||||
{"learn":[0.6611179111],"iteration":501,"passed_time":16.80230735,"remaining_time":50.13915621,"test":[0.6707661931]},
|
|
||||||
{"learn":[0.6610959069],"iteration":502,"passed_time":16.83637769,"remaining_time":50.10747,"test":[0.6707651988]},
|
|
||||||
{"learn":[0.6610728788],"iteration":503,"passed_time":16.87382128,"remaining_time":50.08578697,"test":[0.6707607827]},
|
|
||||||
{"learn":[0.6610436668],"iteration":504,"passed_time":16.92151611,"remaining_time":50.09438927,"test":[0.670760242]},
|
|
||||||
{"learn":[0.6610188976],"iteration":505,"passed_time":16.9898618,"remaining_time":50.16374216,"test":[0.6707506008]},
|
|
||||||
{"learn":[0.6610030555],"iteration":506,"passed_time":17.03818668,"remaining_time":50.17359509,"test":[0.6707452886]},
|
|
||||||
{"learn":[0.6609831174],"iteration":507,"passed_time":17.06933058,"remaining_time":50.13275833,"test":[0.6707355189]},
|
|
||||||
{"learn":[0.6609586562],"iteration":508,"passed_time":17.1106164,"remaining_time":50.12166807,"test":[0.6707312551]},
|
|
||||||
{"learn":[0.660935882],"iteration":509,"passed_time":17.14537899,"remaining_time":50.09140137,"test":[0.6707199485]},
|
|
||||||
{"learn":[0.6609202024],"iteration":510,"passed_time":17.19066307,"remaining_time":50.09177556,"test":[0.6707131947]},
|
|
||||||
{"learn":[0.6609011137],"iteration":511,"passed_time":17.21958034,"remaining_time":50.04440537,"test":[0.6707154112]},
|
|
||||||
{"learn":[0.6608726737],"iteration":512,"passed_time":17.24756917,"remaining_time":49.99441591,"test":[0.6706982346]},
|
|
||||||
{"learn":[0.6608608849],"iteration":513,"passed_time":17.27150822,"remaining_time":49.93280391,"test":[0.6706988941]},
|
|
||||||
{"learn":[0.6608387256],"iteration":514,"passed_time":17.29800365,"remaining_time":49.87870957,"test":[0.6706989098]},
|
|
||||||
{"learn":[0.6608136063],"iteration":515,"passed_time":17.34332283,"remaining_time":49.87885868,"test":[0.670693306]},
|
|
||||||
{"learn":[0.6607946343],"iteration":516,"passed_time":17.37393636,"remaining_time":49.83664916,"test":[0.6706944515]},
|
|
||||||
{"learn":[0.6607703935],"iteration":517,"passed_time":17.4173655,"remaining_time":49.83114994,"test":[0.6706899688]},
|
|
||||||
{"learn":[0.6607509625],"iteration":518,"passed_time":17.46008645,"remaining_time":49.82348368,"test":[0.6706909374]},
|
|
||||||
{"learn":[0.6607238109],"iteration":519,"passed_time":17.4906988,"remaining_time":49.78121967,"test":[0.6706855074]},
|
|
||||||
{"learn":[0.6606999858],"iteration":520,"passed_time":17.5186435,"remaining_time":49.7314275,"test":[0.6706787779]},
|
|
||||||
{"learn":[0.6606813873],"iteration":521,"passed_time":17.54613056,"remaining_time":49.6804233,"test":[0.6706737082]},
|
|
||||||
{"learn":[0.6606610372],"iteration":522,"passed_time":17.57100039,"remaining_time":49.62211774,"test":[0.6706761225]},
|
|
||||||
{"learn":[0.660638456],"iteration":523,"passed_time":17.60084283,"remaining_time":49.5779466,"test":[0.670685455]},
|
|
||||||
{"learn":[0.6606156483],"iteration":524,"passed_time":17.62599925,"remaining_time":49.52066456,"test":[0.6706693855]},
|
|
||||||
{"learn":[0.6605968623],"iteration":525,"passed_time":17.65519625,"remaining_time":49.47482751,"test":[0.6706647216]},
|
|
||||||
{"learn":[0.6605735776],"iteration":526,"passed_time":17.67910836,"remaining_time":49.41428199,"test":[0.6706569188]},
|
|
||||||
{"learn":[0.6605517294],"iteration":527,"passed_time":17.70744827,"remaining_time":49.36621942,"test":[0.6706549134]},
|
|
||||||
{"learn":[0.6605309239],"iteration":528,"passed_time":17.72943083,"remaining_time":49.3005534,"test":[0.6706547978]},
|
|
||||||
{"learn":[0.6605086434],"iteration":529,"passed_time":17.75830336,"remaining_time":49.25416215,"test":[0.6706564214]},
|
|
||||||
{"learn":[0.6604803349],"iteration":530,"passed_time":17.78141858,"remaining_time":49.19190939,"test":[0.6706559196]},
|
|
||||||
{"learn":[0.6604566326],"iteration":531,"passed_time":17.80870208,"remaining_time":49.14130574,"test":[0.6706515072]},
|
|
||||||
{"learn":[0.6604430839],"iteration":532,"passed_time":17.82904188,"remaining_time":49.07167811,"test":[0.6706474616]},
|
|
||||||
{"learn":[0.6604273738],"iteration":533,"passed_time":17.86246645,"remaining_time":49.03815696,"test":[0.6706424204]},
|
|
||||||
{"learn":[0.6604048016],"iteration":534,"passed_time":17.90552779,"remaining_time":49.03102469,"test":[0.6706520008]},
|
|
||||||
{"learn":[0.6603845173],"iteration":535,"passed_time":18.02843143,"remaining_time":49.24183511,"test":[0.6706448306]},
|
|
||||||
{"learn":[0.6603669212],"iteration":536,"passed_time":18.07245966,"remaining_time":49.23651485,"test":[0.6706415789]},
|
|
||||||
{"learn":[0.6603488983],"iteration":537,"passed_time":18.10631942,"remaining_time":49.20341819,"test":[0.6706305359]},
|
|
||||||
{"learn":[0.6603176881],"iteration":538,"passed_time":18.13531438,"remaining_time":49.1571323,"test":[0.6706152774]},
|
|
||||||
{"learn":[0.6602953862],"iteration":539,"passed_time":18.16575265,"remaining_time":49.11481272,"test":[0.670616585]},
|
|
||||||
{"learn":[0.6602672025],"iteration":540,"passed_time":18.20025584,"remaining_time":49.08349958,"test":[0.6705963243]},
|
|
||||||
{"learn":[0.6602568636],"iteration":541,"passed_time":18.22381751,"remaining_time":49.02274158,"test":[0.6706027368]},
|
|
||||||
{"learn":[0.660235705],"iteration":542,"passed_time":18.25438575,"remaining_time":48.98092088,"test":[0.6706003522]},
|
|
||||||
{"learn":[0.6602152295],"iteration":543,"passed_time":18.28070524,"remaining_time":48.9277699,"test":[0.6706044301]},
|
|
||||||
{"learn":[0.6601897709],"iteration":544,"passed_time":18.30768805,"remaining_time":48.87648827,"test":[0.6706047241]},
|
|
||||||
{"learn":[0.6601683731],"iteration":545,"passed_time":18.33807201,"remaining_time":48.83435294,"test":[0.6706038235]},
|
|
||||||
{"learn":[0.6601472267],"iteration":546,"passed_time":18.36776304,"remaining_time":48.79041993,"test":[0.6706026913]},
|
|
||||||
{"learn":[0.6601262337],"iteration":547,"passed_time":18.41134623,"remaining_time":48.78334803,"test":[0.6705845786]},
|
|
||||||
{"learn":[0.6601119991],"iteration":548,"passed_time":18.44405381,"remaining_time":48.74739905,"test":[0.6705873967]},
|
|
||||||
{"learn":[0.6600869973],"iteration":549,"passed_time":18.47010718,"remaining_time":48.69391893,"test":[0.6705755426]},
|
|
||||||
{"learn":[0.6600667497],"iteration":550,"passed_time":18.5036553,"remaining_time":48.66024779,"test":[0.6705715731]},
|
|
||||||
{"learn":[0.6600397508],"iteration":551,"passed_time":18.53164471,"remaining_time":48.61199556,"test":[0.6705757153]},
|
|
||||||
{"learn":[0.660016863],"iteration":552,"passed_time":18.5577607,"remaining_time":48.55891452,"test":[0.6705516814]},
|
|
||||||
{"learn":[0.6599933158],"iteration":553,"passed_time":18.58492994,"remaining_time":48.50867995,"test":[0.6705530864]},
|
|
||||||
{"learn":[0.6599632649],"iteration":554,"passed_time":18.62562092,"remaining_time":48.49373376,"test":[0.6705552479]},
|
|
||||||
{"learn":[0.6599446007],"iteration":555,"passed_time":18.65010209,"remaining_time":48.43659608,"test":[0.6705563336]},
|
|
||||||
{"learn":[0.6599138126],"iteration":556,"passed_time":18.67796421,"remaining_time":48.38833458,"test":[0.6705718544]},
|
|
||||||
{"learn":[0.6598965504],"iteration":557,"passed_time":18.70319381,"remaining_time":48.33334314,"test":[0.6705688384]},
|
|
||||||
{"learn":[0.6598785723],"iteration":558,"passed_time":18.72995694,"remaining_time":48.28241136,"test":[0.6705641528]},
|
|
||||||
{"learn":[0.659860838],"iteration":559,"passed_time":18.75657945,"remaining_time":48.23120429,"test":[0.6705628467]},
|
|
||||||
{"learn":[0.6598408724],"iteration":560,"passed_time":18.78181322,"remaining_time":48.17652269,"test":[0.670558488]},
|
|
||||||
{"learn":[0.6598244857],"iteration":561,"passed_time":18.80867415,"remaining_time":48.12610931,"test":[0.6705544404]},
|
|
||||||
{"learn":[0.6598082469],"iteration":562,"passed_time":18.83488797,"remaining_time":48.0741279,"test":[0.6705617451]},
|
|
||||||
{"learn":[0.6597851673],"iteration":563,"passed_time":18.86939449,"remaining_time":48.04335193,"test":[0.6705631717]},
|
|
||||||
{"learn":[0.6597683521],"iteration":564,"passed_time":18.90235988,"remaining_time":48.00864854,"test":[0.6705636201]},
|
|
||||||
{"learn":[0.6597479006],"iteration":565,"passed_time":18.93001053,"remaining_time":47.96048604,"test":[0.6705537522]},
|
|
||||||
{"learn":[0.6597310938],"iteration":566,"passed_time":18.95858079,"remaining_time":47.91472006,"test":[0.670555083]},
|
|
||||||
{"learn":[0.6597096581],"iteration":567,"passed_time":18.9833487,"remaining_time":47.85942842,"test":[0.6705524541]},
|
|
||||||
{"learn":[0.6596862311],"iteration":568,"passed_time":19.0162481,"remaining_time":47.82469425,"test":[0.6705503132]},
|
|
||||||
{"learn":[0.6596574779],"iteration":569,"passed_time":19.03781666,"remaining_time":47.76154004,"test":[0.6705354602]},
|
|
||||||
{"learn":[0.6596385418],"iteration":570,"passed_time":19.0681355,"remaining_time":47.72043018,"test":[0.6705387012]},
|
|
||||||
{"learn":[0.6596189903],"iteration":571,"passed_time":19.09073714,"remaining_time":47.66009201,"test":[0.6705411923]},
|
|
||||||
{"learn":[0.65959275],"iteration":572,"passed_time":19.11146842,"remaining_time":47.59522765,"test":[0.6705390018]},
|
|
||||||
{"learn":[0.6595730662],"iteration":573,"passed_time":19.141368,"remaining_time":47.55329403,"test":[0.6705354939]},
|
|
||||||
{"learn":[0.6595566809],"iteration":574,"passed_time":19.16428373,"remaining_time":47.49409447,"test":[0.670531296]},
|
|
||||||
{"learn":[0.6595365076],"iteration":575,"passed_time":19.19652276,"remaining_time":47.45807015,"test":[0.6705377163]},
|
|
||||||
{"learn":[0.6595163446],"iteration":576,"passed_time":19.21727405,"remaining_time":47.39372785,"test":[0.6705248875]},
|
|
||||||
{"learn":[0.6594816637],"iteration":577,"passed_time":19.24969594,"remaining_time":47.35824848,"test":[0.6705252902]},
|
|
||||||
{"learn":[0.6594570142],"iteration":578,"passed_time":19.27445137,"remaining_time":47.30396442,"test":[0.6705181562]},
|
|
||||||
{"learn":[0.6594353055],"iteration":579,"passed_time":19.29822455,"remaining_time":47.24737734,"test":[0.6705123446]},
|
|
||||||
{"learn":[0.6594162362],"iteration":580,"passed_time":19.32403522,"remaining_time":47.19587948,"test":[0.6705128345]},
|
|
||||||
{"learn":[0.659395036],"iteration":581,"passed_time":19.35739555,"remaining_time":47.16286408,"test":[0.6705173712]},
|
|
||||||
{"learn":[0.6593798831],"iteration":582,"passed_time":19.39112791,"remaining_time":47.13075172,"test":[0.670541941]},
|
|
||||||
{"learn":[0.6593556719],"iteration":583,"passed_time":19.42704318,"remaining_time":47.1039266,"test":[0.6705463243]},
|
|
||||||
{"learn":[0.6593292627],"iteration":584,"passed_time":19.46022169,"remaining_time":47.07045077,"test":[0.6705513215]},
|
|
||||||
{"learn":[0.6592976737],"iteration":585,"passed_time":19.48332075,"remaining_time":47.01265452,"test":[0.6705455889]},
|
|
||||||
{"learn":[0.6592754841],"iteration":586,"passed_time":19.5115578,"remaining_time":46.9673444,"test":[0.6705408087]},
|
|
||||||
{"learn":[0.6592510441],"iteration":587,"passed_time":19.54275193,"remaining_time":46.92919341,"test":[0.6705510193]},
|
|
||||||
{"learn":[0.6592290326],"iteration":588,"passed_time":19.56411389,"remaining_time":46.86751222,"test":[0.6705456751]},
|
|
||||||
{"learn":[0.6592097404],"iteration":589,"passed_time":19.59700884,"remaining_time":46.8335296,"test":[0.6705402427]},
|
|
||||||
{"learn":[0.6591876204],"iteration":590,"passed_time":19.62169623,"remaining_time":46.77998306,"test":[0.6705443402]},
|
|
||||||
{"learn":[0.6591705995],"iteration":591,"passed_time":19.64747626,"remaining_time":46.72913272,"test":[0.67054441]},
|
|
||||||
{"learn":[0.6591456195],"iteration":592,"passed_time":19.67090184,"remaining_time":46.67278059,"test":[0.6705441955]},
|
|
||||||
{"learn":[0.6591107122],"iteration":593,"passed_time":19.69910949,"remaining_time":46.62785848,"test":[0.6705319356]},
|
|
||||||
{"learn":[0.6590819533],"iteration":594,"passed_time":19.72694709,"remaining_time":46.58211876,"test":[0.6705358843]},
|
|
||||||
{"learn":[0.6590551327],"iteration":595,"passed_time":19.7530808,"remaining_time":46.53242523,"test":[0.6705334396]},
|
|
||||||
{"learn":[0.6590373916],"iteration":596,"passed_time":19.77835609,"remaining_time":46.48079328,"test":[0.6705320462]},
|
|
||||||
{"learn":[0.6590177149],"iteration":597,"passed_time":19.80378809,"remaining_time":46.4296169,"test":[0.6705332043]},
|
|
||||||
{"learn":[0.6589946095],"iteration":598,"passed_time":19.83052585,"remaining_time":46.38158048,"test":[0.6705328363]},
|
|
||||||
{"learn":[0.6589697628],"iteration":599,"passed_time":19.8579153,"remaining_time":46.33513569,"test":[0.6705315638]},
|
|
||||||
{"learn":[0.6589442269],"iteration":600,"passed_time":19.89600309,"remaining_time":46.31365777,"test":[0.6705274435]},
|
|
||||||
{"learn":[0.6589182437],"iteration":601,"passed_time":19.92518872,"remaining_time":46.27145155,"test":[0.670509808]},
|
|
||||||
{"learn":[0.6588837179],"iteration":602,"passed_time":19.95754179,"remaining_time":46.23662666,"test":[0.6705077789]},
|
|
||||||
{"learn":[0.6588674101],"iteration":603,"passed_time":19.99116426,"remaining_time":46.20474388,"test":[0.6705212132]},
|
|
||||||
{"learn":[0.6588406916],"iteration":604,"passed_time":20.01900069,"remaining_time":46.15951398,"test":[0.6705098442]},
|
|
||||||
{"learn":[0.6588149945],"iteration":605,"passed_time":20.04735837,"remaining_time":46.11554053,"test":[0.6705061509]},
|
|
||||||
{"learn":[0.6587866031],"iteration":606,"passed_time":20.07232044,"remaining_time":46.06382599,"test":[0.6705003071]},
|
|
||||||
{"learn":[0.6587636648],"iteration":607,"passed_time":20.09871086,"remaining_time":46.01546959,"test":[0.6705045031]},
|
|
||||||
{"learn":[0.6587502469],"iteration":608,"passed_time":20.12348304,"remaining_time":45.96348917,"test":[0.6705083194]},
|
|
||||||
{"learn":[0.6587292784],"iteration":609,"passed_time":20.14920752,"remaining_time":45.91376797,"test":[0.6705329997]},
|
|
||||||
{"learn":[0.6587104112],"iteration":610,"passed_time":20.17662353,"remaining_time":45.86797068,"test":[0.6705269987]},
|
|
||||||
{"learn":[0.6586953782],"iteration":611,"passed_time":20.20202219,"remaining_time":45.81765818,"test":[0.6705315607]},
|
|
||||||
{"learn":[0.6586641191],"iteration":612,"passed_time":20.23050051,"remaining_time":45.77439512,"test":[0.6705142835]},
|
|
||||||
{"learn":[0.6586450136],"iteration":613,"passed_time":20.25381994,"remaining_time":45.71953492,"test":[0.6705165015]},
|
|
||||||
{"learn":[0.6586136263],"iteration":614,"passed_time":20.28518384,"remaining_time":45.68289369,"test":[0.6705001061]},
|
|
||||||
{"learn":[0.6585862768],"iteration":615,"passed_time":20.3078175,"remaining_time":45.62665489,"test":[0.6705013916]},
|
|
||||||
{"learn":[0.6585585235],"iteration":616,"passed_time":20.33878033,"remaining_time":45.5891948,"test":[0.6705037253]},
|
|
||||||
{"learn":[0.6585371631],"iteration":617,"passed_time":20.36122842,"remaining_time":45.53271469,"test":[0.67049647]},
|
|
||||||
{"learn":[0.6585092632],"iteration":618,"passed_time":20.3943397,"remaining_time":45.50013429,"test":[0.6705005632]},
|
|
||||||
{"learn":[0.6584914317],"iteration":619,"passed_time":20.42384285,"remaining_time":45.45952119,"test":[0.6704957943]},
|
|
||||||
{"learn":[0.6584662432],"iteration":620,"passed_time":20.45411533,"remaining_time":45.42065225,"test":[0.6704955333]},
|
|
||||||
{"learn":[0.6584454668],"iteration":621,"passed_time":20.488223,"remaining_time":45.39030754,"test":[0.6704961207]},
|
|
||||||
{"learn":[0.6584249408],"iteration":622,"passed_time":20.51043528,"remaining_time":45.33365872,"test":[0.6704921459]},
|
|
||||||
{"learn":[0.6583931228],"iteration":623,"passed_time":20.54384208,"remaining_time":45.30180561,"test":[0.6704751713]},
|
|
||||||
{"learn":[0.6583660767],"iteration":624,"passed_time":20.56912557,"remaining_time":45.25207624,"test":[0.6704753101]},
|
|
||||||
{"learn":[0.658354264],"iteration":625,"passed_time":20.59414123,"remaining_time":45.20183714,"test":[0.6704620888]},
|
|
||||||
{"learn":[0.6583253625],"iteration":626,"passed_time":20.61901142,"remaining_time":45.15135993,"test":[0.6704604282]},
|
|
||||||
{"learn":[0.6582968632],"iteration":627,"passed_time":20.6468542,"remaining_time":45.10745855,"test":[0.6704663192]},
|
|
||||||
{"learn":[0.6582687399],"iteration":628,"passed_time":20.67583093,"remaining_time":45.06607981,"test":[0.6704680085]},
|
|
||||||
{"learn":[0.658242535],"iteration":629,"passed_time":20.7010198,"remaining_time":45.01650336,"test":[0.670453228]},
|
|
||||||
{"learn":[0.6582199874],"iteration":630,"passed_time":20.72783977,"remaining_time":44.97054302,"test":[0.6704577785]},
|
|
||||||
{"learn":[0.6581918101],"iteration":631,"passed_time":20.75222724,"remaining_time":44.91937795,"test":[0.67046675]},
|
|
||||||
{"learn":[0.6581735218],"iteration":632,"passed_time":20.78264004,"remaining_time":44.88130954,"test":[0.6704731863]},
|
|
||||||
{"learn":[0.6581445869],"iteration":633,"passed_time":20.80459182,"remaining_time":44.82503538,"test":[0.6704811116]},
|
|
||||||
{"learn":[0.6581202427],"iteration":634,"passed_time":20.83717209,"remaining_time":44.79171637,"test":[0.6704839644]},
|
|
||||||
{"learn":[0.6580977862],"iteration":635,"passed_time":20.86231353,"remaining_time":44.74244599,"test":[0.6704854798]},
|
|
||||||
{"learn":[0.6580724179],"iteration":636,"passed_time":20.89269601,"remaining_time":44.70446572,"test":[0.6704835837]},
|
|
||||||
{"learn":[0.6580426322],"iteration":637,"passed_time":20.93117347,"remaining_time":44.68379039,"test":[0.6704736198]},
|
|
||||||
{"learn":[0.6580111256],"iteration":638,"passed_time":20.96066949,"remaining_time":44.64392985,"test":[0.6704640242]},
|
|
||||||
{"learn":[0.6579834747],"iteration":639,"passed_time":20.9941179,"remaining_time":44.61250055,"test":[0.670465663]},
|
|
||||||
{"learn":[0.6579541367],"iteration":640,"passed_time":21.0224519,"remaining_time":44.57022174,"test":[0.6704646829]},
|
|
||||||
{"learn":[0.6579254503],"iteration":641,"passed_time":21.0522529,"remaining_time":44.53108946,"test":[0.6704600961]},
|
|
||||||
{"learn":[0.657898555],"iteration":642,"passed_time":21.08260618,"remaining_time":44.49315178,"test":[0.6704643207]},
|
|
||||||
{"learn":[0.6578676875],"iteration":643,"passed_time":21.10716702,"remaining_time":44.44304112,"test":[0.6704600533]},
|
|
||||||
{"learn":[0.6578324163],"iteration":644,"passed_time":21.13594828,"remaining_time":44.40187584,"test":[0.6704614691]},
|
|
||||||
{"learn":[0.6578062223],"iteration":645,"passed_time":21.1601277,"remaining_time":44.35110357,"test":[0.6704728212]},
|
|
||||||
{"learn":[0.6577760631],"iteration":646,"passed_time":21.18552999,"remaining_time":44.30297075,"test":[0.6704758731]},
|
|
||||||
{"learn":[0.6577483474],"iteration":647,"passed_time":21.21048648,"remaining_time":44.25397797,"test":[0.6704833026]},
|
|
||||||
{"learn":[0.6577249642],"iteration":648,"passed_time":21.23686209,"remaining_time":44.20801337,"test":[0.6704767664]},
|
|
||||||
{"learn":[0.6576974966],"iteration":649,"passed_time":21.26287585,"remaining_time":44.16135753,"test":[0.6704702727]},
|
|
||||||
{"learn":[0.657675114],"iteration":650,"passed_time":21.28806218,"remaining_time":44.11305051,"test":[0.6704671372]},
|
|
||||||
{"learn":[0.6576447891],"iteration":651,"passed_time":21.31506267,"remaining_time":44.06856515,"test":[0.6704699936]},
|
|
||||||
{"learn":[0.6576102356],"iteration":652,"passed_time":21.3435081,"remaining_time":44.02711394,"test":[0.6704587989]},
|
|
||||||
{"learn":[0.6575793887],"iteration":653,"passed_time":21.37776713,"remaining_time":43.99766753,"test":[0.6704637668]},
|
|
||||||
{"learn":[0.6575543309],"iteration":654,"passed_time":21.40301154,"remaining_time":43.94969545,"test":[0.6704653717]},
|
|
||||||
{"learn":[0.6575340787],"iteration":655,"passed_time":21.44023109,"remaining_time":43.92632711,"test":[0.6704598273]},
|
|
||||||
{"learn":[0.6575061464],"iteration":656,"passed_time":21.4778965,"remaining_time":43.903828,"test":[0.6704522865]},
|
|
||||||
{"learn":[0.657476113],"iteration":657,"passed_time":21.50245582,"remaining_time":43.85455275,"test":[0.6704558586]},
|
|
||||||
{"learn":[0.6574447014],"iteration":658,"passed_time":21.53379663,"remaining_time":43.81915217,"test":[0.6704466331]},
|
|
||||||
{"learn":[0.6574247361],"iteration":659,"passed_time":21.55955041,"remaining_time":43.77242053,"test":[0.6704405886]},
|
|
||||||
{"learn":[0.6574034983],"iteration":660,"passed_time":21.58626671,"remaining_time":43.72770215,"test":[0.6704463767]},
|
|
||||||
{"learn":[0.6573783832],"iteration":661,"passed_time":21.61183918,"remaining_time":43.68072633,"test":[0.6704475216]},
|
|
||||||
{"learn":[0.657357694],"iteration":662,"passed_time":21.6373217,"remaining_time":43.63363366,"test":[0.6704572386]},
|
|
||||||
{"learn":[0.6573411592],"iteration":663,"passed_time":21.66283476,"remaining_time":43.58666753,"test":[0.6704658153]},
|
|
||||||
{"learn":[0.6573118559],"iteration":664,"passed_time":21.68841321,"remaining_time":43.5398972,"test":[0.6704600945]},
|
|
||||||
{"learn":[0.6572819076],"iteration":665,"passed_time":21.71420973,"remaining_time":43.4936273,"test":[0.6704561998]},
|
|
||||||
{"learn":[0.6572430097],"iteration":666,"passed_time":21.74213421,"remaining_time":43.45167151,"test":[0.6704535154]},
|
|
||||||
{"learn":[0.6572160391],"iteration":667,"passed_time":21.77174463,"remaining_time":43.41311953,"test":[0.6704413781]},
|
|
||||||
{"learn":[0.6571931413],"iteration":668,"passed_time":21.81895309,"remaining_time":43.40960622,"test":[0.6704450013]},
|
|
||||||
{"learn":[0.6571737099],"iteration":669,"passed_time":21.84627583,"remaining_time":43.36648784,"test":[0.6704422199]},
|
|
||||||
{"learn":[0.6571532872],"iteration":670,"passed_time":21.88834724,"remaining_time":43.35262814,"test":[0.67044342]},
|
|
||||||
{"learn":[0.6571208939],"iteration":671,"passed_time":21.93403139,"remaining_time":43.34582395,"test":[0.6704415341]},
|
|
||||||
{"learn":[0.6570887673],"iteration":672,"passed_time":21.9714274,"remaining_time":43.32256191,"test":[0.6704439539]},
|
|
||||||
{"learn":[0.6570633692],"iteration":673,"passed_time":22.01942449,"remaining_time":43.32011406,"test":[0.6704498197]},
|
|
||||||
{"learn":[0.6570454361],"iteration":674,"passed_time":22.05319867,"remaining_time":43.2896122,"test":[0.6704452194]},
|
|
||||||
{"learn":[0.6570231031],"iteration":675,"passed_time":22.09079747,"remaining_time":43.26659149,"test":[0.6704366524]},
|
|
||||||
{"learn":[0.6570052089],"iteration":676,"passed_time":22.14192346,"remaining_time":43.26996269,"test":[0.6704427124]},
|
|
||||||
{"learn":[0.6569855794],"iteration":677,"passed_time":22.17624471,"remaining_time":43.24040635,"test":[0.6704395579]},
|
|
||||||
{"learn":[0.6569579709],"iteration":678,"passed_time":22.213192,"remaining_time":43.21594497,"test":[0.6704401246]},
|
|
||||||
{"learn":[0.6569333354],"iteration":679,"passed_time":22.23966403,"remaining_time":43.17111253,"test":[0.6704415621]},
|
|
||||||
{"learn":[0.6569069617],"iteration":680,"passed_time":22.27051241,"remaining_time":43.13481039,"test":[0.6704341343]},
|
|
||||||
{"learn":[0.6568931857],"iteration":681,"passed_time":22.29625075,"remaining_time":43.08864881,"test":[0.6704369615]},
|
|
||||||
{"learn":[0.6568734532],"iteration":682,"passed_time":22.32160622,"remaining_time":43.04180877,"test":[0.6704357425]},
|
|
||||||
{"learn":[0.6568435196],"iteration":683,"passed_time":22.35059872,"remaining_time":43.00202911,"test":[0.6704294622]},
|
|
||||||
{"learn":[0.6568108038],"iteration":684,"passed_time":22.37956576,"remaining_time":42.96223208,"test":[0.6704289794]},
|
|
||||||
{"learn":[0.6567811374],"iteration":685,"passed_time":22.41993338,"remaining_time":42.94430389,"test":[0.6704272409]},
|
|
||||||
{"learn":[0.6567467284],"iteration":686,"passed_time":22.45285267,"remaining_time":42.91207504,"test":[0.6704101162]},
|
|
||||||
{"learn":[0.6567172734],"iteration":687,"passed_time":22.4848431,"remaining_time":42.8780729,"test":[0.6704069439]},
|
|
||||||
{"learn":[0.6566967606],"iteration":688,"passed_time":22.51193834,"remaining_time":42.83476221,"test":[0.6704100747]},
|
|
||||||
{"learn":[0.6566720128],"iteration":689,"passed_time":22.53798671,"remaining_time":42.78951101,"test":[0.6704122261]},
|
|
||||||
{"learn":[0.6566441608],"iteration":690,"passed_time":22.57108439,"remaining_time":42.75766928,"test":[0.6704137826]},
|
|
||||||
{"learn":[0.6566172287],"iteration":691,"passed_time":22.59836588,"remaining_time":42.7148303,"test":[0.6704207952]},
|
|
||||||
{"learn":[0.6565952549],"iteration":692,"passed_time":22.62447507,"remaining_time":42.66982528,"test":[0.6704154834]},
|
|
||||||
{"learn":[0.6565702687],"iteration":693,"passed_time":22.65349415,"remaining_time":42.63035067,"test":[0.6704253514]},
|
|
||||||
{"learn":[0.6565392213],"iteration":694,"passed_time":22.68028991,"remaining_time":42.58673141,"test":[0.6704155636]},
|
|
||||||
{"learn":[0.6565157938],"iteration":695,"passed_time":22.70844406,"remaining_time":42.54570555,"test":[0.6704141298]},
|
|
||||||
{"learn":[0.6564902789],"iteration":696,"passed_time":22.73944116,"remaining_time":42.51003133,"test":[0.6704207635]},
|
|
||||||
{"learn":[0.6564644734],"iteration":697,"passed_time":22.7613976,"remaining_time":42.45750671,"test":[0.6704268341]},
|
|
||||||
{"learn":[0.6564349549],"iteration":698,"passed_time":22.79216825,"remaining_time":42.42147482,"test":[0.6704243126]},
|
|
||||||
{"learn":[0.6564046572],"iteration":699,"passed_time":22.8167121,"remaining_time":42.37389389,"test":[0.6704235165]},
|
|
||||||
{"learn":[0.6563744107],"iteration":700,"passed_time":22.84507296,"remaining_time":42.33345189,"test":[0.6704257736]},
|
|
||||||
{"learn":[0.6563525063],"iteration":701,"passed_time":22.87088832,"remaining_time":42.28833766,"test":[0.6704247758]},
|
|
||||||
{"learn":[0.6563189867],"iteration":702,"passed_time":22.90238907,"remaining_time":42.25376759,"test":[0.6704331799]},
|
|
||||||
{"learn":[0.6562939062],"iteration":703,"passed_time":22.94246813,"remaining_time":42.23499815,"test":[0.6704252722]},
|
|
||||||
{"learn":[0.6562739297],"iteration":704,"passed_time":22.97441688,"remaining_time":42.20123385,"test":[0.6704146644]},
|
|
||||||
{"learn":[0.656256438],"iteration":705,"passed_time":23.00262167,"remaining_time":42.16061253,"test":[0.6704164122]},
|
|
||||||
{"learn":[0.6562366475],"iteration":706,"passed_time":23.033437,"remaining_time":42.12480062,"test":[0.6704118954]},
|
|
||||||
{"learn":[0.6562073096],"iteration":707,"passed_time":23.0545813,"remaining_time":42.07135458,"test":[0.6704043129]},
|
|
||||||
{"learn":[0.6561864222],"iteration":708,"passed_time":23.08699831,"remaining_time":42.03852584,"test":[0.6703978198]},
|
|
||||||
{"learn":[0.6561578826],"iteration":709,"passed_time":23.11590694,"remaining_time":41.99932387,"test":[0.6703935976]},
|
|
||||||
{"learn":[0.6561208567],"iteration":710,"passed_time":23.14362702,"remaining_time":41.9579961,"test":[0.6703839683]},
|
|
||||||
{"learn":[0.6560924703],"iteration":711,"passed_time":23.16985155,"remaining_time":41.91400112,"test":[0.6703843723]},
|
|
||||||
{"learn":[0.6560656907],"iteration":712,"passed_time":23.19510285,"remaining_time":41.86829925,"test":[0.6703879502]},
|
|
||||||
{"learn":[0.6560362588],"iteration":713,"passed_time":23.23034771,"remaining_time":41.84065429,"test":[0.6703895978]},
|
|
||||||
{"learn":[0.6560124527],"iteration":714,"passed_time":23.25923754,"remaining_time":41.80156678,"test":[0.6703894359]},
|
|
||||||
{"learn":[0.6559875055],"iteration":715,"passed_time":23.28703452,"remaining_time":41.76054794,"test":[0.6703928777]},
|
|
||||||
{"learn":[0.6559547281],"iteration":716,"passed_time":23.31161175,"remaining_time":41.71380457,"test":[0.6703933128]},
|
|
||||||
{"learn":[0.6559230866],"iteration":717,"passed_time":23.34170355,"remaining_time":41.67696929,"test":[0.6703844355]},
|
|
||||||
{"learn":[0.6558924823],"iteration":718,"passed_time":23.37263658,"remaining_time":41.64165155,"test":[0.6703825151]},
|
|
||||||
{"learn":[0.6558676469],"iteration":719,"passed_time":23.40571088,"remaining_time":41.61015268,"test":[0.6703983542]},
|
|
||||||
{"learn":[0.6558459277],"iteration":720,"passed_time":23.4389719,"remaining_time":41.57898067,"test":[0.670399556]},
|
|
||||||
{"learn":[0.6558149638],"iteration":721,"passed_time":23.48304084,"remaining_time":41.56693379,"test":[0.6703931808]},
|
|
||||||
{"learn":[0.6557812248],"iteration":722,"passed_time":23.50734531,"remaining_time":41.5198893,"test":[0.6703886918]},
|
|
||||||
{"learn":[0.6557546502],"iteration":723,"passed_time":23.54055835,"remaining_time":41.48860836,"test":[0.6703847574]},
|
|
||||||
{"learn":[0.6557274948],"iteration":724,"passed_time":23.56652491,"remaining_time":41.44457829,"test":[0.6703885941]},
|
|
||||||
{"learn":[0.6557044723],"iteration":725,"passed_time":23.59580183,"remaining_time":41.40640708,"test":[0.6703788615]},
|
|
||||||
{"learn":[0.6556751811],"iteration":726,"passed_time":23.62334313,"remaining_time":41.36522119,"test":[0.6703799906]},
|
|
||||||
{"learn":[0.6556539158],"iteration":727,"passed_time":23.64879831,"remaining_time":41.32042782,"test":[0.6703774518]},
|
|
||||||
{"learn":[0.6556182915],"iteration":728,"passed_time":23.67755213,"remaining_time":41.28143862,"test":[0.6703783496]},
|
|
||||||
{"learn":[0.6555977079],"iteration":729,"passed_time":23.70012944,"remaining_time":41.23173204,"test":[0.6703648854]},
|
|
||||||
{"learn":[0.6555667903],"iteration":730,"passed_time":23.72866102,"remaining_time":41.19243615,"test":[0.6703716654]},
|
|
||||||
{"learn":[0.6555394075],"iteration":731,"passed_time":23.75226732,"remaining_time":41.14463793,"test":[0.6703550938]},
|
|
||||||
{"learn":[0.6555122742],"iteration":732,"passed_time":23.7844108,"remaining_time":41.11166233,"test":[0.6703467057]},
|
|
||||||
{"learn":[0.6554814941],"iteration":733,"passed_time":23.80747563,"remaining_time":41.06303017,"test":[0.6703484503]},
|
|
||||||
{"learn":[0.6554517373],"iteration":734,"passed_time":23.84023587,"remaining_time":41.03115425,"test":[0.6703549183]},
|
|
||||||
{"learn":[0.655429552],"iteration":735,"passed_time":23.87042124,"remaining_time":40.99485387,"test":[0.6703501504]},
|
|
||||||
{"learn":[0.655396579],"iteration":736,"passed_time":23.9087808,"remaining_time":40.97257823,"test":[0.6703672622]},
|
|
||||||
{"learn":[0.6553735864],"iteration":737,"passed_time":23.94161529,"remaining_time":40.94081097,"test":[0.6703560249]},
|
|
||||||
{"learn":[0.6553472597],"iteration":738,"passed_time":23.97478791,"remaining_time":40.90961779,"test":[0.6703547155]},
|
|
||||||
{"learn":[0.6553252832],"iteration":739,"passed_time":24.00628859,"remaining_time":40.87557247,"test":[0.6703593236]},
|
|
||||||
{"learn":[0.6552971659],"iteration":740,"passed_time":24.03623034,"remaining_time":40.83888528,"test":[0.6703606827]},
|
|
||||||
{"learn":[0.6552763852],"iteration":741,"passed_time":24.06404686,"remaining_time":40.79861313,"test":[0.6703511404]},
|
|
||||||
{"learn":[0.6552488203],"iteration":742,"passed_time":24.09270947,"remaining_time":40.75980593,"test":[0.6703431646]},
|
|
||||||
{"learn":[0.65521229],"iteration":743,"passed_time":24.12724624,"remaining_time":40.73094258,"test":[0.6703475116]},
|
|
||||||
{"learn":[0.6551949744],"iteration":744,"passed_time":24.15397955,"remaining_time":40.68891857,"test":[0.6703483634]},
|
|
||||||
{"learn":[0.6551673797],"iteration":745,"passed_time":24.17955779,"remaining_time":40.64499392,"test":[0.6703475713]},
|
|
||||||
{"learn":[0.6551421856],"iteration":746,"passed_time":24.20715317,"remaining_time":40.60450191,"test":[0.670360457]},
|
|
||||||
{"learn":[0.6551255516],"iteration":747,"passed_time":24.23336836,"remaining_time":40.5617342,"test":[0.6703664352]},
|
|
||||||
{"learn":[0.6551019608],"iteration":748,"passed_time":24.2614437,"remaining_time":40.52211759,"test":[0.6703617612]},
|
|
||||||
{"learn":[0.6550758728],"iteration":749,"passed_time":24.29512083,"remaining_time":40.49186805,"test":[0.6703669926]},
|
|
||||||
{"learn":[0.655051966],"iteration":750,"passed_time":24.31839238,"remaining_time":40.44430371,"test":[0.6703670837]},
|
|
||||||
{"learn":[0.6550351058],"iteration":751,"passed_time":24.34977118,"remaining_time":40.41025856,"test":[0.6703706628]},
|
|
||||||
{"learn":[0.6549998756],"iteration":752,"passed_time":24.3762114,"remaining_time":40.36804198,"test":[0.670369618]},
|
|
||||||
{"learn":[0.6549721212],"iteration":753,"passed_time":24.40831154,"remaining_time":40.3352204,"test":[0.6703692351]},
|
|
||||||
{"learn":[0.6549401744],"iteration":754,"passed_time":24.44267281,"remaining_time":40.30612934,"test":[0.6703624433]},
|
|
||||||
{"learn":[0.6549207325],"iteration":755,"passed_time":24.47460721,"remaining_time":40.27303091,"test":[0.6703686285]},
|
|
||||||
{"learn":[0.6548900891],"iteration":756,"passed_time":24.50826603,"remaining_time":40.24276708,"test":[0.6703598432]},
|
|
||||||
{"learn":[0.6548682731],"iteration":757,"passed_time":24.54826542,"remaining_time":40.22288345,"test":[0.6703618766]},
|
|
||||||
{"learn":[0.6548418938],"iteration":758,"passed_time":24.57546587,"remaining_time":40.18201996,"test":[0.6703694148]},
|
|
||||||
{"learn":[0.6548234717],"iteration":759,"passed_time":24.60502723,"remaining_time":40.14504442,"test":[0.6703683652]},
|
|
||||||
{"learn":[0.6547996833],"iteration":760,"passed_time":24.63261096,"remaining_time":40.10486856,"test":[0.6703604855]},
|
|
||||||
{"learn":[0.6547726174],"iteration":761,"passed_time":24.66001655,"remaining_time":40.06443634,"test":[0.6703758987]},
|
|
||||||
{"learn":[0.6547509314],"iteration":762,"passed_time":24.68929907,"remaining_time":40.02708119,"test":[0.6703773302]},
|
|
||||||
{"learn":[0.6547168175],"iteration":763,"passed_time":24.71425118,"remaining_time":39.98274144,"test":[0.6703641028]},
|
|
||||||
{"learn":[0.6546907846],"iteration":764,"passed_time":24.74589169,"remaining_time":39.94924999,"test":[0.6703649602]},
|
|
||||||
{"learn":[0.6546671611],"iteration":765,"passed_time":24.76625006,"remaining_time":39.89758822,"test":[0.6703567811]},
|
|
||||||
{"learn":[0.6546475893],"iteration":766,"passed_time":24.79734832,"remaining_time":39.86327312,"test":[0.6703544688]},
|
|
||||||
{"learn":[0.6546206223],"iteration":767,"passed_time":24.82531049,"remaining_time":39.82393558,"test":[0.6703611821]},
|
|
||||||
{"learn":[0.6545874193],"iteration":768,"passed_time":24.85435247,"remaining_time":39.78635616,"test":[0.6703527821]},
|
|
||||||
{"learn":[0.6545620629],"iteration":769,"passed_time":24.88095966,"remaining_time":39.74490958,"test":[0.6703523616]},
|
|
||||||
{"learn":[0.6545346297],"iteration":770,"passed_time":24.90935211,"remaining_time":39.70634726,"test":[0.6703616298]},
|
|
||||||
{"learn":[0.6545172316],"iteration":771,"passed_time":24.94098876,"remaining_time":39.67297175,"test":[0.6703603551]},
|
|
||||||
{"learn":[0.6544943049],"iteration":772,"passed_time":24.97035098,"remaining_time":39.6359905,"test":[0.6703675655]},
|
|
||||||
{"learn":[0.6544632323],"iteration":773,"passed_time":25.00434422,"remaining_time":39.60636436,"test":[0.6703582411]},
|
|
||||||
{"learn":[0.6544384097],"iteration":774,"passed_time":25.03067441,"remaining_time":39.56461439,"test":[0.6703581437]},
|
|
||||||
{"learn":[0.6544084745],"iteration":775,"passed_time":25.05692652,"remaining_time":39.522781,"test":[0.6703551885]},
|
|
||||||
{"learn":[0.6543765257],"iteration":776,"passed_time":25.08660163,"remaining_time":39.48637554,"test":[0.6703608491]},
|
|
||||||
{"learn":[0.6543536123],"iteration":777,"passed_time":25.10764591,"remaining_time":39.43643098,"test":[0.6703674554]},
|
|
||||||
{"learn":[0.6543303593],"iteration":778,"passed_time":25.13940138,"remaining_time":39.40334928,"test":[0.6703679619]},
|
|
||||||
{"learn":[0.6543005831],"iteration":779,"passed_time":25.15916899,"remaining_time":39.35152074,"test":[0.6703701757]},
|
|
||||||
{"learn":[0.6542678123],"iteration":780,"passed_time":25.18841105,"remaining_time":39.31456219,"test":[0.6703603462]},
|
|
||||||
{"learn":[0.6542439303],"iteration":781,"passed_time":25.21444083,"remaining_time":39.27262012,"test":[0.670359801]},
|
|
||||||
{"learn":[0.6542100401],"iteration":782,"passed_time":25.24017824,"remaining_time":39.23026426,"test":[0.6703523669]},
|
|
||||||
{"learn":[0.6541836178],"iteration":783,"passed_time":25.2660091,"remaining_time":39.18809574,"test":[0.6703365674]},
|
|
||||||
{"learn":[0.654158129],"iteration":784,"passed_time":25.28891553,"remaining_time":39.1414425,"test":[0.6703486118]},
|
|
||||||
{"learn":[0.6541343464],"iteration":785,"passed_time":25.31589904,"remaining_time":39.10114686,"test":[0.6703450011]},
|
|
||||||
{"learn":[0.6541092921],"iteration":786,"passed_time":25.34123581,"remaining_time":39.05834694,"test":[0.6703473135]},
|
|
||||||
{"learn":[0.6540812254],"iteration":787,"passed_time":25.36728606,"remaining_time":39.01668871,"test":[0.670350998]},
|
|
||||||
{"learn":[0.654060259],"iteration":788,"passed_time":25.39177931,"remaining_time":38.97268028,"test":[0.6703417767]},
|
|
||||||
{"learn":[0.6540467253],"iteration":789,"passed_time":25.41712461,"remaining_time":38.9300263,"test":[0.6703349821]},
|
|
||||||
{"learn":[0.6540306837],"iteration":790,"passed_time":25.44804125,"remaining_time":38.89593157,"test":[0.6703457717]},
|
|
||||||
{"learn":[0.6540103667],"iteration":791,"passed_time":25.48249341,"remaining_time":38.86723743,"test":[0.6703506266]},
|
|
||||||
{"learn":[0.6539821302],"iteration":792,"passed_time":25.51450657,"remaining_time":38.83481643,"test":[0.6703596395]},
|
|
||||||
{"learn":[0.6539577914],"iteration":793,"passed_time":25.54216564,"remaining_time":38.79578307,"test":[0.6703799895]},
|
|
||||||
{"learn":[0.653923724],"iteration":794,"passed_time":25.56982738,"remaining_time":38.75678238,"test":[0.6703687687]},
|
|
||||||
{"learn":[0.6539086888],"iteration":795,"passed_time":25.59539769,"remaining_time":38.71464675,"test":[0.6703780675]},
|
|
||||||
{"learn":[0.6538798424],"iteration":796,"passed_time":25.61874122,"remaining_time":38.66919157,"test":[0.670374835]},
|
|
||||||
{"learn":[0.6538566996],"iteration":797,"passed_time":25.64394874,"remaining_time":38.62659947,"test":[0.6703831387]},
|
|
||||||
{"learn":[0.6538290752],"iteration":798,"passed_time":25.66776244,"remaining_time":38.58195581,"test":[0.670377656]},
|
|
||||||
{"learn":[0.6538051255],"iteration":799,"passed_time":25.69593415,"remaining_time":38.54390122,"test":[0.6703689741]},
|
|
||||||
{"learn":[0.6537917354],"iteration":800,"passed_time":25.71651353,"remaining_time":38.49450652,"test":[0.6703709756]},
|
|
||||||
{"learn":[0.6537684302],"iteration":801,"passed_time":25.74304126,"remaining_time":38.45406912,"test":[0.6703737517]},
|
|
||||||
{"learn":[0.6537402991],"iteration":802,"passed_time":25.77084871,"remaining_time":38.41557398,"test":[0.6703818964]},
|
|
||||||
{"learn":[0.6537165427],"iteration":803,"passed_time":25.79028824,"remaining_time":38.36465763,"test":[0.6703812173]},
|
|
||||||
{"learn":[0.6536853601],"iteration":804,"passed_time":25.82203653,"remaining_time":38.3320915,"test":[0.6703960068]},
|
|
||||||
{"learn":[0.6536681479],"iteration":805,"passed_time":25.84395064,"remaining_time":38.28495914,"test":[0.6703976729]},
|
|
||||||
{"learn":[0.6536409101],"iteration":806,"passed_time":25.87390688,"remaining_time":38.24977808,"test":[0.6704024604]},
|
|
||||||
{"learn":[0.6536120189],"iteration":807,"passed_time":25.89606204,"remaining_time":38.20310143,"test":[0.6704085008]},
|
|
||||||
{"learn":[0.6535912493],"iteration":808,"passed_time":25.92585483,"remaining_time":38.16772942,"test":[0.6704076633]},
|
|
||||||
{"learn":[0.6535617421],"iteration":809,"passed_time":25.95539059,"remaining_time":38.13199358,"test":[0.6704111719]},
|
|
||||||
{"learn":[0.6535315174],"iteration":810,"passed_time":25.98822968,"remaining_time":38.10111601,"test":[0.6704220803]},
|
|
||||||
{"learn":[0.6534972927],"iteration":811,"passed_time":26.02835773,"remaining_time":38.08089777,"test":[0.6704265011]},
|
|
||||||
{"learn":[0.6534818476],"iteration":812,"passed_time":26.0558565,"remaining_time":38.04219146,"test":[0.6704251162]},
|
|
||||||
{"learn":[0.6534498323],"iteration":813,"passed_time":26.08151817,"remaining_time":38.00083606,"test":[0.6704375472]},
|
|
||||||
{"learn":[0.6534305025],"iteration":814,"passed_time":26.10848988,"remaining_time":37.96142393,"test":[0.6704319336]},
|
|
||||||
{"learn":[0.6534081059],"iteration":815,"passed_time":26.13143346,"remaining_time":37.91619757,"test":[0.670437614]},
|
|
||||||
{"learn":[0.6533765804],"iteration":816,"passed_time":26.15923661,"remaining_time":37.87806231,"test":[0.6704554331]},
|
|
||||||
{"learn":[0.6533441549],"iteration":817,"passed_time":26.18805523,"remaining_time":37.84141966,"test":[0.6704603317]},
|
|
||||||
{"learn":[0.6533053405],"iteration":818,"passed_time":26.2140726,"remaining_time":37.8007567,"test":[0.6704548042]},
|
|
||||||
{"learn":[0.6532838469],"iteration":819,"passed_time":26.24289367,"remaining_time":37.76416405,"test":[0.6704502654]},
|
|
||||||
{"learn":[0.6532604302],"iteration":820,"passed_time":26.27260776,"remaining_time":37.72887277,"test":[0.6704512072]},
|
|
||||||
{"learn":[0.6532364412],"iteration":821,"passed_time":26.29880394,"remaining_time":37.68855358,"test":[0.6704433481]},
|
|
||||||
{"learn":[0.6532100089],"iteration":822,"passed_time":26.32785215,"remaining_time":37.65234749,"test":[0.6704095112]},
|
|
||||||
{"learn":[0.6531782515],"iteration":823,"passed_time":26.35925682,"remaining_time":37.61952188,"test":[0.6704086019]},
|
|
||||||
{"learn":[0.6531449701],"iteration":824,"passed_time":26.38596096,"remaining_time":37.580005,"test":[0.6703987131]},
|
|
||||||
{"learn":[0.653115452],"iteration":825,"passed_time":26.40854839,"remaining_time":37.53466805,"test":[0.6704019708]},
|
|
||||||
{"learn":[0.6530787602],"iteration":826,"passed_time":26.44419918,"remaining_time":37.50791492,"test":[0.6704046556]},
|
|
||||||
{"learn":[0.653052397],"iteration":827,"passed_time":26.47784276,"remaining_time":37.47829917,"test":[0.6704091961]},
|
|
||||||
{"learn":[0.6530313579],"iteration":828,"passed_time":26.51701028,"remaining_time":37.45647652,"test":[0.6704103204]},
|
|
||||||
{"learn":[0.6530010363],"iteration":829,"passed_time":26.53963123,"remaining_time":37.41128739,"test":[0.6704074257]},
|
|
||||||
{"learn":[0.6529752146],"iteration":830,"passed_time":26.57362226,"remaining_time":37.38214732,"test":[0.6704115335]},
|
|
||||||
{"learn":[0.652954801],"iteration":831,"passed_time":26.59767057,"remaining_time":37.33903754,"test":[0.6704041275]},
|
|
||||||
{"learn":[0.6529330351],"iteration":832,"passed_time":26.62378941,"remaining_time":37.29887425,"test":[0.6704004556]},
|
|
||||||
{"learn":[0.6528993709],"iteration":833,"passed_time":26.65024746,"remaining_time":37.25921887,"test":[0.6704037097]},
|
|
||||||
{"learn":[0.6528665883],"iteration":834,"passed_time":26.67774911,"remaining_time":37.22105115,"test":[0.6704035477]},
|
|
||||||
{"learn":[0.6528413041],"iteration":835,"passed_time":26.70473813,"remaining_time":37.1821952,"test":[0.6704025281]},
|
|
||||||
{"learn":[0.6528217161],"iteration":836,"passed_time":26.72833235,"remaining_time":37.13865056,"test":[0.6704024549]},
|
|
||||||
{"learn":[0.6527978782],"iteration":837,"passed_time":26.76384162,"remaining_time":37.11167537,"test":[0.670405721]},
|
|
||||||
{"learn":[0.6527789461],"iteration":838,"passed_time":26.79137369,"remaining_time":37.07364106,"test":[0.6703983189]},
|
|
||||||
{"learn":[0.6527432001],"iteration":839,"passed_time":26.82295602,"remaining_time":37.04122498,"test":[0.6704035256]},
|
|
||||||
{"learn":[0.6527139767],"iteration":840,"passed_time":26.87217031,"remaining_time":37.03310985,"test":[0.6704047613]},
|
|
||||||
{"learn":[0.6526857244],"iteration":841,"passed_time":26.92488006,"remaining_time":37.0297044,"test":[0.6704139617]},
|
|
||||||
{"learn":[0.652657086],"iteration":842,"passed_time":26.98258041,"remaining_time":37.03303147,"test":[0.6704066193]},
|
|
||||||
{"learn":[0.6526355016],"iteration":843,"passed_time":27.05424841,"remaining_time":37.05534497,"test":[0.670402892]},
|
|
||||||
{"learn":[0.6526054936],"iteration":844,"passed_time":27.09765154,"remaining_time":37.03880181,"test":[0.6704081961]},
|
|
||||||
{"learn":[0.6525793707],"iteration":845,"passed_time":27.12038959,"remaining_time":36.99400661,"test":[0.6704029862]},
|
|
||||||
{"learn":[0.6525584692],"iteration":846,"passed_time":27.14691224,"remaining_time":36.95441537,"test":[0.6704014281]},
|
|
||||||
{"learn":[0.6525279747],"iteration":847,"passed_time":27.18096334,"remaining_time":36.92508227,"test":[0.6704036115]},
|
|
||||||
{"learn":[0.6525038765],"iteration":848,"passed_time":27.20686017,"remaining_time":36.88468322,"test":[0.6704016777]},
|
|
||||||
{"learn":[0.6524849104],"iteration":849,"passed_time":27.23465701,"remaining_time":36.8468889,"test":[0.6704085392]},
|
|
||||||
{"learn":[0.6524610603],"iteration":850,"passed_time":27.26094834,"remaining_time":36.80708536,"test":[0.6704042952]},
|
|
||||||
{"learn":[0.6524357337],"iteration":851,"passed_time":27.28945577,"remaining_time":36.77029957,"test":[0.670394789]},
|
|
||||||
{"learn":[0.6524082286],"iteration":852,"passed_time":27.31865398,"remaining_time":36.73446203,"test":[0.6703885644]},
|
|
||||||
{"learn":[0.65238051],"iteration":853,"passed_time":27.34791322,"remaining_time":36.69872195,"test":[0.6703946813]},
|
|
||||||
{"learn":[0.6523557826],"iteration":854,"passed_time":27.3865535,"remaining_time":36.67555995,"test":[0.6704042137]},
|
|
||||||
{"learn":[0.6523391233],"iteration":855,"passed_time":27.41370907,"remaining_time":36.63701306,"test":[0.6704077517]},
|
|
||||||
{"learn":[0.652325347],"iteration":856,"passed_time":27.43905921,"remaining_time":36.5960848,"test":[0.6704118698]},
|
|
||||||
{"learn":[0.6522924958],"iteration":857,"passed_time":27.47159295,"remaining_time":36.56475425,"test":[0.6704114259]},
|
|
||||||
{"learn":[0.6522623584],"iteration":858,"passed_time":27.50124299,"remaining_time":36.52959052,"test":[0.6704157567]},
|
|
||||||
{"learn":[0.6522343891],"iteration":859,"passed_time":27.53509105,"remaining_time":36.50000442,"test":[0.6703837005]},
|
|
||||||
{"learn":[0.6522094424],"iteration":860,"passed_time":27.57211091,"remaining_time":36.47460432,"test":[0.6703829482]},
|
|
||||||
{"learn":[0.6521841478],"iteration":861,"passed_time":27.59555719,"remaining_time":36.43125764,"test":[0.6703818491]},
|
|
||||||
{"learn":[0.6521657946],"iteration":862,"passed_time":27.6272049,"remaining_time":36.39876242,"test":[0.6703826129]},
|
|
||||||
{"learn":[0.6521304278],"iteration":863,"passed_time":27.65462267,"remaining_time":36.36070759,"test":[0.6703834487]},
|
|
||||||
{"learn":[0.6521045712],"iteration":864,"passed_time":27.68321566,"remaining_time":36.3242194,"test":[0.6703868275]},
|
|
||||||
{"learn":[0.6520753696],"iteration":865,"passed_time":27.71151671,"remaining_time":36.28736714,"test":[0.6703853357]},
|
|
||||||
{"learn":[0.6520519528],"iteration":866,"passed_time":27.73884016,"remaining_time":36.2492571,"test":[0.670450644]},
|
|
||||||
{"learn":[0.6520216555],"iteration":867,"passed_time":27.76583897,"remaining_time":36.21074851,"test":[0.6704556991]},
|
|
||||||
{"learn":[0.6519926935],"iteration":868,"passed_time":27.79498714,"remaining_time":36.17506382,"test":[0.6704535742]},
|
|
||||||
{"learn":[0.6519734186],"iteration":869,"passed_time":27.82082723,"remaining_time":36.13509744,"test":[0.6704495915]}
|
|
||||||
]}
|
|
||||||
@@ -1,871 +0,0 @@
|
|||||||
iter Logloss
|
|
||||||
0 0.692389481
|
|
||||||
1 0.6916338586
|
|
||||||
2 0.6910159214
|
|
||||||
3 0.6903417151
|
|
||||||
4 0.6896961461
|
|
||||||
5 0.6890979366
|
|
||||||
6 0.6884946167
|
|
||||||
7 0.6879503686
|
|
||||||
8 0.6874528094
|
|
||||||
9 0.6869036785
|
|
||||||
10 0.6863761921
|
|
||||||
11 0.6859038678
|
|
||||||
12 0.685410175
|
|
||||||
13 0.6849483392
|
|
||||||
14 0.6845417792
|
|
||||||
15 0.6841038875
|
|
||||||
16 0.6836957422
|
|
||||||
17 0.6832947461
|
|
||||||
18 0.6829014105
|
|
||||||
19 0.6825264546
|
|
||||||
20 0.6822106577
|
|
||||||
21 0.6818649349
|
|
||||||
22 0.6815467855
|
|
||||||
23 0.6812293319
|
|
||||||
24 0.6808837443
|
|
||||||
25 0.6805816494
|
|
||||||
26 0.6803209634
|
|
||||||
27 0.6800350862
|
|
||||||
28 0.6797703947
|
|
||||||
29 0.6794926675
|
|
||||||
30 0.6792251865
|
|
||||||
31 0.6789670166
|
|
||||||
32 0.678722402
|
|
||||||
33 0.678476935
|
|
||||||
34 0.6782297335
|
|
||||||
35 0.6780226701
|
|
||||||
36 0.6778291026
|
|
||||||
37 0.6776045324
|
|
||||||
38 0.6773969079
|
|
||||||
39 0.6771819602
|
|
||||||
40 0.6769816736
|
|
||||||
41 0.6767984027
|
|
||||||
42 0.6766201184
|
|
||||||
43 0.6764394377
|
|
||||||
44 0.6762698797
|
|
||||||
45 0.6760974263
|
|
||||||
46 0.6759245179
|
|
||||||
47 0.6757673909
|
|
||||||
48 0.6756172628
|
|
||||||
49 0.675474531
|
|
||||||
50 0.6753286933
|
|
||||||
51 0.6751900513
|
|
||||||
52 0.6750574835
|
|
||||||
53 0.6749329567
|
|
||||||
54 0.6748033265
|
|
||||||
55 0.6746797823
|
|
||||||
56 0.674535525
|
|
||||||
57 0.6744256514
|
|
||||||
58 0.674310819
|
|
||||||
59 0.6741967947
|
|
||||||
60 0.6740879654
|
|
||||||
61 0.6739772476
|
|
||||||
62 0.67388281
|
|
||||||
63 0.6737789726
|
|
||||||
64 0.6736812332
|
|
||||||
65 0.6735930009
|
|
||||||
66 0.6734947116
|
|
||||||
67 0.6733961481
|
|
||||||
68 0.6732990195
|
|
||||||
69 0.6732133575
|
|
||||||
70 0.673111539
|
|
||||||
71 0.6730080451
|
|
||||||
72 0.6729157861
|
|
||||||
73 0.6728347949
|
|
||||||
74 0.6727640693
|
|
||||||
75 0.6726808811
|
|
||||||
76 0.6726029645
|
|
||||||
77 0.6725356026
|
|
||||||
78 0.6724606887
|
|
||||||
79 0.6723849561
|
|
||||||
80 0.6723050519
|
|
||||||
81 0.6722508802
|
|
||||||
82 0.6721773904
|
|
||||||
83 0.6721007598
|
|
||||||
84 0.6720353564
|
|
||||||
85 0.6719790902
|
|
||||||
86 0.6719140024
|
|
||||||
87 0.6718573633
|
|
||||||
88 0.671795602
|
|
||||||
89 0.6717369134
|
|
||||||
90 0.6716711079
|
|
||||||
91 0.6716070843
|
|
||||||
92 0.6715517232
|
|
||||||
93 0.6714957378
|
|
||||||
94 0.6714364567
|
|
||||||
95 0.6713881758
|
|
||||||
96 0.6713336502
|
|
||||||
97 0.6712700267
|
|
||||||
98 0.6712154424
|
|
||||||
99 0.6711600413
|
|
||||||
100 0.6711060533
|
|
||||||
101 0.6710494943
|
|
||||||
102 0.6709936897
|
|
||||||
103 0.6709472183
|
|
||||||
104 0.6708914508
|
|
||||||
105 0.6708388195
|
|
||||||
106 0.6707885854
|
|
||||||
107 0.6707454167
|
|
||||||
108 0.6706973013
|
|
||||||
109 0.6706577031
|
|
||||||
110 0.67061108
|
|
||||||
111 0.6705625485
|
|
||||||
112 0.6705146484
|
|
||||||
113 0.6704704423
|
|
||||||
114 0.6704155922
|
|
||||||
115 0.6703687117
|
|
||||||
116 0.6703324232
|
|
||||||
117 0.6702884624
|
|
||||||
118 0.670253478
|
|
||||||
119 0.6702140804
|
|
||||||
120 0.6701682529
|
|
||||||
121 0.6701320588
|
|
||||||
122 0.6700939824
|
|
||||||
123 0.6700655902
|
|
||||||
124 0.6700190743
|
|
||||||
125 0.6699792296
|
|
||||||
126 0.6699379404
|
|
||||||
127 0.669895454
|
|
||||||
128 0.6698563938
|
|
||||||
129 0.6698215571
|
|
||||||
130 0.6697857067
|
|
||||||
131 0.6697449303
|
|
||||||
132 0.6697052425
|
|
||||||
133 0.6696695553
|
|
||||||
134 0.6696269265
|
|
||||||
135 0.6695969271
|
|
||||||
136 0.6695489786
|
|
||||||
137 0.6695173859
|
|
||||||
138 0.6694811164
|
|
||||||
139 0.6694477439
|
|
||||||
140 0.6694082161
|
|
||||||
141 0.6693679185
|
|
||||||
142 0.6693341916
|
|
||||||
143 0.6692933159
|
|
||||||
144 0.6692619696
|
|
||||||
145 0.6692229289
|
|
||||||
146 0.6691840164
|
|
||||||
147 0.6691581406
|
|
||||||
148 0.6691177196
|
|
||||||
149 0.6690851126
|
|
||||||
150 0.6690518144
|
|
||||||
151 0.6690149711
|
|
||||||
152 0.668993877
|
|
||||||
153 0.6689596579
|
|
||||||
154 0.6689372651
|
|
||||||
155 0.6689003045
|
|
||||||
156 0.6688680182
|
|
||||||
157 0.6688348164
|
|
||||||
158 0.6687947046
|
|
||||||
159 0.6687605251
|
|
||||||
160 0.668726253
|
|
||||||
161 0.6686862718
|
|
||||||
162 0.668663478
|
|
||||||
163 0.6686399521
|
|
||||||
164 0.6686058279
|
|
||||||
165 0.6685761282
|
|
||||||
166 0.6685469327
|
|
||||||
167 0.6685157003
|
|
||||||
168 0.6684805143
|
|
||||||
169 0.6684485765
|
|
||||||
170 0.6684144429
|
|
||||||
171 0.6683849752
|
|
||||||
172 0.6683568537
|
|
||||||
173 0.6683266628
|
|
||||||
174 0.6682937842
|
|
||||||
175 0.6682657097
|
|
||||||
176 0.6682301443
|
|
||||||
177 0.6681995916
|
|
||||||
178 0.6681658267
|
|
||||||
179 0.6681422687
|
|
||||||
180 0.6681216601
|
|
||||||
181 0.6680899019
|
|
||||||
182 0.6680676394
|
|
||||||
183 0.6680413672
|
|
||||||
184 0.6680088406
|
|
||||||
185 0.6679873982
|
|
||||||
186 0.6679663544
|
|
||||||
187 0.6679417375
|
|
||||||
188 0.6679100197
|
|
||||||
189 0.667881208
|
|
||||||
190 0.6678475427
|
|
||||||
191 0.6678310341
|
|
||||||
192 0.6678060257
|
|
||||||
193 0.6677789336
|
|
||||||
194 0.6677478773
|
|
||||||
195 0.6677212408
|
|
||||||
196 0.667704316
|
|
||||||
197 0.6676819639
|
|
||||||
198 0.6676554448
|
|
||||||
199 0.6676318346
|
|
||||||
200 0.6676074705
|
|
||||||
201 0.6675849784
|
|
||||||
202 0.6675631744
|
|
||||||
203 0.6675397619
|
|
||||||
204 0.6675169086
|
|
||||||
205 0.6674864762
|
|
||||||
206 0.6674670714
|
|
||||||
207 0.6674375599
|
|
||||||
208 0.6674148457
|
|
||||||
209 0.6673974446
|
|
||||||
210 0.6673812139
|
|
||||||
211 0.6673515687
|
|
||||||
212 0.6673197956
|
|
||||||
213 0.6672900754
|
|
||||||
214 0.6672550009
|
|
||||||
215 0.6672271563
|
|
||||||
216 0.667204521
|
|
||||||
217 0.667181968
|
|
||||||
218 0.6671640023
|
|
||||||
219 0.66714351
|
|
||||||
220 0.6671167156
|
|
||||||
221 0.6670915937
|
|
||||||
222 0.6670595279
|
|
||||||
223 0.667033994
|
|
||||||
224 0.6670008246
|
|
||||||
225 0.6669858319
|
|
||||||
226 0.6669553964
|
|
||||||
227 0.6669274683
|
|
||||||
228 0.666896348
|
|
||||||
229 0.6668698686
|
|
||||||
230 0.6668513411
|
|
||||||
231 0.6668309985
|
|
||||||
232 0.6668058585
|
|
||||||
233 0.6667845908
|
|
||||||
234 0.6667582863
|
|
||||||
235 0.6667332943
|
|
||||||
236 0.6667070085
|
|
||||||
237 0.6666907315
|
|
||||||
238 0.6666633028
|
|
||||||
239 0.6666406707
|
|
||||||
240 0.6666134624
|
|
||||||
241 0.6665850522
|
|
||||||
242 0.6665631193
|
|
||||||
243 0.6665412643
|
|
||||||
244 0.6665168385
|
|
||||||
245 0.6664904845
|
|
||||||
246 0.6664678274
|
|
||||||
247 0.6664539777
|
|
||||||
248 0.6664334121
|
|
||||||
249 0.6664121724
|
|
||||||
250 0.666392034
|
|
||||||
251 0.666366899
|
|
||||||
252 0.6663414098
|
|
||||||
253 0.6663157816
|
|
||||||
254 0.6662989799
|
|
||||||
255 0.6662696102
|
|
||||||
256 0.6662479711
|
|
||||||
257 0.6662231874
|
|
||||||
258 0.6661947927
|
|
||||||
259 0.6661669951
|
|
||||||
260 0.6661426137
|
|
||||||
261 0.6661216749
|
|
||||||
262 0.6660983123
|
|
||||||
263 0.6660803402
|
|
||||||
264 0.6660617842
|
|
||||||
265 0.6660443878
|
|
||||||
266 0.6660176079
|
|
||||||
267 0.6659967546
|
|
||||||
268 0.6659751467
|
|
||||||
269 0.6659539329
|
|
||||||
270 0.6659263951
|
|
||||||
271 0.6659038921
|
|
||||||
272 0.6658767418
|
|
||||||
273 0.6658510507
|
|
||||||
274 0.6658210119
|
|
||||||
275 0.6657963011
|
|
||||||
276 0.6657748552
|
|
||||||
277 0.6657490013
|
|
||||||
278 0.665732402
|
|
||||||
279 0.6657118786
|
|
||||||
280 0.665684467
|
|
||||||
281 0.6656584634
|
|
||||||
282 0.6656309991
|
|
||||||
283 0.6656073482
|
|
||||||
284 0.6655890957
|
|
||||||
285 0.6655665563
|
|
||||||
286 0.6655452454
|
|
||||||
287 0.6655255286
|
|
||||||
288 0.6655053548
|
|
||||||
289 0.6654893396
|
|
||||||
290 0.6654648912
|
|
||||||
291 0.6654442759
|
|
||||||
292 0.6654173127
|
|
||||||
293 0.6653914518
|
|
||||||
294 0.6653648946
|
|
||||||
295 0.665344141
|
|
||||||
296 0.6653140817
|
|
||||||
297 0.665295365
|
|
||||||
298 0.6652787488
|
|
||||||
299 0.6652502991
|
|
||||||
300 0.665231168
|
|
||||||
301 0.6652136682
|
|
||||||
302 0.6651903001
|
|
||||||
303 0.6651697153
|
|
||||||
304 0.6651525958
|
|
||||||
305 0.6651322685
|
|
||||||
306 0.6651113828
|
|
||||||
307 0.6650886807
|
|
||||||
308 0.6650622251
|
|
||||||
309 0.6650429987
|
|
||||||
310 0.665015513
|
|
||||||
311 0.6650019022
|
|
||||||
312 0.664979951
|
|
||||||
313 0.6649549638
|
|
||||||
314 0.6649340455
|
|
||||||
315 0.6649162445
|
|
||||||
316 0.6649048119
|
|
||||||
317 0.6648796463
|
|
||||||
318 0.6648605481
|
|
||||||
319 0.6648429084
|
|
||||||
320 0.6648238121
|
|
||||||
321 0.6647969527
|
|
||||||
322 0.6647854723
|
|
||||||
323 0.6647589304
|
|
||||||
324 0.6647429024
|
|
||||||
325 0.6647237508
|
|
||||||
326 0.6647059396
|
|
||||||
327 0.664686288
|
|
||||||
328 0.6646532527
|
|
||||||
329 0.6646306438
|
|
||||||
330 0.6646098516
|
|
||||||
331 0.6645858284
|
|
||||||
332 0.6645707188
|
|
||||||
333 0.6645485788
|
|
||||||
334 0.6645305696
|
|
||||||
335 0.6645108881
|
|
||||||
336 0.6644923286
|
|
||||||
337 0.6644805222
|
|
||||||
338 0.6644572776
|
|
||||||
339 0.6644320741
|
|
||||||
340 0.6644115048
|
|
||||||
341 0.6643949013
|
|
||||||
342 0.6643619789
|
|
||||||
343 0.6643389502
|
|
||||||
344 0.6643088915
|
|
||||||
345 0.664286972
|
|
||||||
346 0.664274149
|
|
||||||
347 0.6642536926
|
|
||||||
348 0.6642357634
|
|
||||||
349 0.664207914
|
|
||||||
350 0.6641853097
|
|
||||||
351 0.6641654917
|
|
||||||
352 0.664143804
|
|
||||||
353 0.6641290647
|
|
||||||
354 0.6641117244
|
|
||||||
355 0.6640880219
|
|
||||||
356 0.6640669415
|
|
||||||
357 0.6640462999
|
|
||||||
358 0.664030296
|
|
||||||
359 0.6640028542
|
|
||||||
360 0.6639813347
|
|
||||||
361 0.6639597941
|
|
||||||
362 0.6639429832
|
|
||||||
363 0.6639222708
|
|
||||||
364 0.6639065546
|
|
||||||
365 0.6638823236
|
|
||||||
366 0.6638648195
|
|
||||||
367 0.6638436235
|
|
||||||
368 0.6638208732
|
|
||||||
369 0.6637956357
|
|
||||||
370 0.6637718453
|
|
||||||
371 0.663756918
|
|
||||||
372 0.6637353525
|
|
||||||
373 0.6637143112
|
|
||||||
374 0.6636956547
|
|
||||||
375 0.663680995
|
|
||||||
376 0.66366728
|
|
||||||
377 0.6636487567
|
|
||||||
378 0.6636266904
|
|
||||||
379 0.6636116064
|
|
||||||
380 0.6635902746
|
|
||||||
381 0.6635654896
|
|
||||||
382 0.6635393029
|
|
||||||
383 0.6635171734
|
|
||||||
384 0.663500789
|
|
||||||
385 0.663477743
|
|
||||||
386 0.6634584806
|
|
||||||
387 0.6634337499
|
|
||||||
388 0.6634135584
|
|
||||||
389 0.6633868455
|
|
||||||
390 0.6633755323
|
|
||||||
391 0.663356103
|
|
||||||
392 0.6633337631
|
|
||||||
393 0.663319422
|
|
||||||
394 0.6632911566
|
|
||||||
395 0.6632687875
|
|
||||||
396 0.6632431997
|
|
||||||
397 0.6632189331
|
|
||||||
398 0.663201035
|
|
||||||
399 0.6631898553
|
|
||||||
400 0.6631712482
|
|
||||||
401 0.663143025
|
|
||||||
402 0.663121538
|
|
||||||
403 0.6631087792
|
|
||||||
404 0.6630859067
|
|
||||||
405 0.663066483
|
|
||||||
406 0.6630443652
|
|
||||||
407 0.6630250376
|
|
||||||
408 0.6630007822
|
|
||||||
409 0.6629768728
|
|
||||||
410 0.6629528093
|
|
||||||
411 0.6629260936
|
|
||||||
412 0.6629102182
|
|
||||||
413 0.6628863488
|
|
||||||
414 0.6628648972
|
|
||||||
415 0.6628454339
|
|
||||||
416 0.6628200274
|
|
||||||
417 0.6627942591
|
|
||||||
418 0.6627744647
|
|
||||||
419 0.662765485
|
|
||||||
420 0.6627503257
|
|
||||||
421 0.6627323029
|
|
||||||
422 0.6627111509
|
|
||||||
423 0.6626785863
|
|
||||||
424 0.6626576561
|
|
||||||
425 0.6626363113
|
|
||||||
426 0.6626181065
|
|
||||||
427 0.66259794
|
|
||||||
428 0.6625765658
|
|
||||||
429 0.6625526572
|
|
||||||
430 0.66253135
|
|
||||||
431 0.6625035695
|
|
||||||
432 0.662480212
|
|
||||||
433 0.6624611632
|
|
||||||
434 0.6624332625
|
|
||||||
435 0.6624120584
|
|
||||||
436 0.6623941719
|
|
||||||
437 0.6623766304
|
|
||||||
438 0.6623623329
|
|
||||||
439 0.6623442925
|
|
||||||
440 0.6623212715
|
|
||||||
441 0.6623025941
|
|
||||||
442 0.6622749791
|
|
||||||
443 0.6622534499
|
|
||||||
444 0.6622305473
|
|
||||||
445 0.6622059333
|
|
||||||
446 0.6621871707
|
|
||||||
447 0.6621638454
|
|
||||||
448 0.6621511296
|
|
||||||
449 0.6621349978
|
|
||||||
450 0.6621120424
|
|
||||||
451 0.6620958271
|
|
||||||
452 0.6620793528
|
|
||||||
453 0.6620572713
|
|
||||||
454 0.6620395025
|
|
||||||
455 0.6620188044
|
|
||||||
456 0.6620017347
|
|
||||||
457 0.6619811454
|
|
||||||
458 0.6619695569
|
|
||||||
459 0.661952377
|
|
||||||
460 0.6619237442
|
|
||||||
461 0.6619089407
|
|
||||||
462 0.6618886168
|
|
||||||
463 0.6618831383
|
|
||||||
464 0.6618690774
|
|
||||||
465 0.661845878
|
|
||||||
466 0.6618290213
|
|
||||||
467 0.6618050064
|
|
||||||
468 0.6617832833
|
|
||||||
469 0.6617652311
|
|
||||||
470 0.6617443144
|
|
||||||
471 0.6617202619
|
|
||||||
472 0.6617005831
|
|
||||||
473 0.6616824419
|
|
||||||
474 0.6616538226
|
|
||||||
475 0.6616314155
|
|
||||||
476 0.6616127861
|
|
||||||
477 0.6616029072
|
|
||||||
478 0.6615843751
|
|
||||||
479 0.661563216
|
|
||||||
480 0.6615432257
|
|
||||||
481 0.6615263324
|
|
||||||
482 0.6615033259
|
|
||||||
483 0.661484293
|
|
||||||
484 0.6614678231
|
|
||||||
485 0.6614463024
|
|
||||||
486 0.6614155436
|
|
||||||
487 0.6613958945
|
|
||||||
488 0.661380611
|
|
||||||
489 0.6613677802
|
|
||||||
490 0.6613530086
|
|
||||||
491 0.6613248211
|
|
||||||
492 0.6613059359
|
|
||||||
493 0.6612729965
|
|
||||||
494 0.6612624948
|
|
||||||
495 0.6612401679
|
|
||||||
496 0.6612191637
|
|
||||||
497 0.6611912219
|
|
||||||
498 0.6611773017
|
|
||||||
499 0.6611638216
|
|
||||||
500 0.6611450533
|
|
||||||
501 0.6611179111
|
|
||||||
502 0.6610959069
|
|
||||||
503 0.6610728788
|
|
||||||
504 0.6610436668
|
|
||||||
505 0.6610188976
|
|
||||||
506 0.6610030555
|
|
||||||
507 0.6609831174
|
|
||||||
508 0.6609586562
|
|
||||||
509 0.660935882
|
|
||||||
510 0.6609202024
|
|
||||||
511 0.6609011137
|
|
||||||
512 0.6608726737
|
|
||||||
513 0.6608608849
|
|
||||||
514 0.6608387256
|
|
||||||
515 0.6608136063
|
|
||||||
516 0.6607946343
|
|
||||||
517 0.6607703935
|
|
||||||
518 0.6607509625
|
|
||||||
519 0.6607238109
|
|
||||||
520 0.6606999858
|
|
||||||
521 0.6606813873
|
|
||||||
522 0.6606610372
|
|
||||||
523 0.660638456
|
|
||||||
524 0.6606156483
|
|
||||||
525 0.6605968623
|
|
||||||
526 0.6605735776
|
|
||||||
527 0.6605517294
|
|
||||||
528 0.6605309239
|
|
||||||
529 0.6605086434
|
|
||||||
530 0.6604803349
|
|
||||||
531 0.6604566326
|
|
||||||
532 0.6604430839
|
|
||||||
533 0.6604273738
|
|
||||||
534 0.6604048016
|
|
||||||
535 0.6603845173
|
|
||||||
536 0.6603669212
|
|
||||||
537 0.6603488983
|
|
||||||
538 0.6603176881
|
|
||||||
539 0.6602953862
|
|
||||||
540 0.6602672025
|
|
||||||
541 0.6602568636
|
|
||||||
542 0.660235705
|
|
||||||
543 0.6602152295
|
|
||||||
544 0.6601897709
|
|
||||||
545 0.6601683731
|
|
||||||
546 0.6601472267
|
|
||||||
547 0.6601262337
|
|
||||||
548 0.6601119991
|
|
||||||
549 0.6600869973
|
|
||||||
550 0.6600667497
|
|
||||||
551 0.6600397508
|
|
||||||
552 0.660016863
|
|
||||||
553 0.6599933158
|
|
||||||
554 0.6599632649
|
|
||||||
555 0.6599446007
|
|
||||||
556 0.6599138126
|
|
||||||
557 0.6598965504
|
|
||||||
558 0.6598785723
|
|
||||||
559 0.659860838
|
|
||||||
560 0.6598408724
|
|
||||||
561 0.6598244857
|
|
||||||
562 0.6598082469
|
|
||||||
563 0.6597851673
|
|
||||||
564 0.6597683521
|
|
||||||
565 0.6597479006
|
|
||||||
566 0.6597310938
|
|
||||||
567 0.6597096581
|
|
||||||
568 0.6596862311
|
|
||||||
569 0.6596574779
|
|
||||||
570 0.6596385418
|
|
||||||
571 0.6596189903
|
|
||||||
572 0.65959275
|
|
||||||
573 0.6595730662
|
|
||||||
574 0.6595566809
|
|
||||||
575 0.6595365076
|
|
||||||
576 0.6595163446
|
|
||||||
577 0.6594816637
|
|
||||||
578 0.6594570142
|
|
||||||
579 0.6594353055
|
|
||||||
580 0.6594162362
|
|
||||||
581 0.659395036
|
|
||||||
582 0.6593798831
|
|
||||||
583 0.6593556719
|
|
||||||
584 0.6593292627
|
|
||||||
585 0.6592976737
|
|
||||||
586 0.6592754841
|
|
||||||
587 0.6592510441
|
|
||||||
588 0.6592290326
|
|
||||||
589 0.6592097404
|
|
||||||
590 0.6591876204
|
|
||||||
591 0.6591705995
|
|
||||||
592 0.6591456195
|
|
||||||
593 0.6591107122
|
|
||||||
594 0.6590819533
|
|
||||||
595 0.6590551327
|
|
||||||
596 0.6590373916
|
|
||||||
597 0.6590177149
|
|
||||||
598 0.6589946095
|
|
||||||
599 0.6589697628
|
|
||||||
600 0.6589442269
|
|
||||||
601 0.6589182437
|
|
||||||
602 0.6588837179
|
|
||||||
603 0.6588674101
|
|
||||||
604 0.6588406916
|
|
||||||
605 0.6588149945
|
|
||||||
606 0.6587866031
|
|
||||||
607 0.6587636648
|
|
||||||
608 0.6587502469
|
|
||||||
609 0.6587292784
|
|
||||||
610 0.6587104112
|
|
||||||
611 0.6586953782
|
|
||||||
612 0.6586641191
|
|
||||||
613 0.6586450136
|
|
||||||
614 0.6586136263
|
|
||||||
615 0.6585862768
|
|
||||||
616 0.6585585235
|
|
||||||
617 0.6585371631
|
|
||||||
618 0.6585092632
|
|
||||||
619 0.6584914317
|
|
||||||
620 0.6584662432
|
|
||||||
621 0.6584454668
|
|
||||||
622 0.6584249408
|
|
||||||
623 0.6583931228
|
|
||||||
624 0.6583660767
|
|
||||||
625 0.658354264
|
|
||||||
626 0.6583253625
|
|
||||||
627 0.6582968632
|
|
||||||
628 0.6582687399
|
|
||||||
629 0.658242535
|
|
||||||
630 0.6582199874
|
|
||||||
631 0.6581918101
|
|
||||||
632 0.6581735218
|
|
||||||
633 0.6581445869
|
|
||||||
634 0.6581202427
|
|
||||||
635 0.6580977862
|
|
||||||
636 0.6580724179
|
|
||||||
637 0.6580426322
|
|
||||||
638 0.6580111256
|
|
||||||
639 0.6579834747
|
|
||||||
640 0.6579541367
|
|
||||||
641 0.6579254503
|
|
||||||
642 0.657898555
|
|
||||||
643 0.6578676875
|
|
||||||
644 0.6578324163
|
|
||||||
645 0.6578062223
|
|
||||||
646 0.6577760631
|
|
||||||
647 0.6577483474
|
|
||||||
648 0.6577249642
|
|
||||||
649 0.6576974966
|
|
||||||
650 0.657675114
|
|
||||||
651 0.6576447891
|
|
||||||
652 0.6576102356
|
|
||||||
653 0.6575793887
|
|
||||||
654 0.6575543309
|
|
||||||
655 0.6575340787
|
|
||||||
656 0.6575061464
|
|
||||||
657 0.657476113
|
|
||||||
658 0.6574447014
|
|
||||||
659 0.6574247361
|
|
||||||
660 0.6574034983
|
|
||||||
661 0.6573783832
|
|
||||||
662 0.657357694
|
|
||||||
663 0.6573411592
|
|
||||||
664 0.6573118559
|
|
||||||
665 0.6572819076
|
|
||||||
666 0.6572430097
|
|
||||||
667 0.6572160391
|
|
||||||
668 0.6571931413
|
|
||||||
669 0.6571737099
|
|
||||||
670 0.6571532872
|
|
||||||
671 0.6571208939
|
|
||||||
672 0.6570887673
|
|
||||||
673 0.6570633692
|
|
||||||
674 0.6570454361
|
|
||||||
675 0.6570231031
|
|
||||||
676 0.6570052089
|
|
||||||
677 0.6569855794
|
|
||||||
678 0.6569579709
|
|
||||||
679 0.6569333354
|
|
||||||
680 0.6569069617
|
|
||||||
681 0.6568931857
|
|
||||||
682 0.6568734532
|
|
||||||
683 0.6568435196
|
|
||||||
684 0.6568108038
|
|
||||||
685 0.6567811374
|
|
||||||
686 0.6567467284
|
|
||||||
687 0.6567172734
|
|
||||||
688 0.6566967606
|
|
||||||
689 0.6566720128
|
|
||||||
690 0.6566441608
|
|
||||||
691 0.6566172287
|
|
||||||
692 0.6565952549
|
|
||||||
693 0.6565702687
|
|
||||||
694 0.6565392213
|
|
||||||
695 0.6565157938
|
|
||||||
696 0.6564902789
|
|
||||||
697 0.6564644734
|
|
||||||
698 0.6564349549
|
|
||||||
699 0.6564046572
|
|
||||||
700 0.6563744107
|
|
||||||
701 0.6563525063
|
|
||||||
702 0.6563189867
|
|
||||||
703 0.6562939062
|
|
||||||
704 0.6562739297
|
|
||||||
705 0.656256438
|
|
||||||
706 0.6562366475
|
|
||||||
707 0.6562073096
|
|
||||||
708 0.6561864222
|
|
||||||
709 0.6561578826
|
|
||||||
710 0.6561208567
|
|
||||||
711 0.6560924703
|
|
||||||
712 0.6560656907
|
|
||||||
713 0.6560362588
|
|
||||||
714 0.6560124527
|
|
||||||
715 0.6559875055
|
|
||||||
716 0.6559547281
|
|
||||||
717 0.6559230866
|
|
||||||
718 0.6558924823
|
|
||||||
719 0.6558676469
|
|
||||||
720 0.6558459277
|
|
||||||
721 0.6558149638
|
|
||||||
722 0.6557812248
|
|
||||||
723 0.6557546502
|
|
||||||
724 0.6557274948
|
|
||||||
725 0.6557044723
|
|
||||||
726 0.6556751811
|
|
||||||
727 0.6556539158
|
|
||||||
728 0.6556182915
|
|
||||||
729 0.6555977079
|
|
||||||
730 0.6555667903
|
|
||||||
731 0.6555394075
|
|
||||||
732 0.6555122742
|
|
||||||
733 0.6554814941
|
|
||||||
734 0.6554517373
|
|
||||||
735 0.655429552
|
|
||||||
736 0.655396579
|
|
||||||
737 0.6553735864
|
|
||||||
738 0.6553472597
|
|
||||||
739 0.6553252832
|
|
||||||
740 0.6552971659
|
|
||||||
741 0.6552763852
|
|
||||||
742 0.6552488203
|
|
||||||
743 0.65521229
|
|
||||||
744 0.6551949744
|
|
||||||
745 0.6551673797
|
|
||||||
746 0.6551421856
|
|
||||||
747 0.6551255516
|
|
||||||
748 0.6551019608
|
|
||||||
749 0.6550758728
|
|
||||||
750 0.655051966
|
|
||||||
751 0.6550351058
|
|
||||||
752 0.6549998756
|
|
||||||
753 0.6549721212
|
|
||||||
754 0.6549401744
|
|
||||||
755 0.6549207325
|
|
||||||
756 0.6548900891
|
|
||||||
757 0.6548682731
|
|
||||||
758 0.6548418938
|
|
||||||
759 0.6548234717
|
|
||||||
760 0.6547996833
|
|
||||||
761 0.6547726174
|
|
||||||
762 0.6547509314
|
|
||||||
763 0.6547168175
|
|
||||||
764 0.6546907846
|
|
||||||
765 0.6546671611
|
|
||||||
766 0.6546475893
|
|
||||||
767 0.6546206223
|
|
||||||
768 0.6545874193
|
|
||||||
769 0.6545620629
|
|
||||||
770 0.6545346297
|
|
||||||
771 0.6545172316
|
|
||||||
772 0.6544943049
|
|
||||||
773 0.6544632323
|
|
||||||
774 0.6544384097
|
|
||||||
775 0.6544084745
|
|
||||||
776 0.6543765257
|
|
||||||
777 0.6543536123
|
|
||||||
778 0.6543303593
|
|
||||||
779 0.6543005831
|
|
||||||
780 0.6542678123
|
|
||||||
781 0.6542439303
|
|
||||||
782 0.6542100401
|
|
||||||
783 0.6541836178
|
|
||||||
784 0.654158129
|
|
||||||
785 0.6541343464
|
|
||||||
786 0.6541092921
|
|
||||||
787 0.6540812254
|
|
||||||
788 0.654060259
|
|
||||||
789 0.6540467253
|
|
||||||
790 0.6540306837
|
|
||||||
791 0.6540103667
|
|
||||||
792 0.6539821302
|
|
||||||
793 0.6539577914
|
|
||||||
794 0.653923724
|
|
||||||
795 0.6539086888
|
|
||||||
796 0.6538798424
|
|
||||||
797 0.6538566996
|
|
||||||
798 0.6538290752
|
|
||||||
799 0.6538051255
|
|
||||||
800 0.6537917354
|
|
||||||
801 0.6537684302
|
|
||||||
802 0.6537402991
|
|
||||||
803 0.6537165427
|
|
||||||
804 0.6536853601
|
|
||||||
805 0.6536681479
|
|
||||||
806 0.6536409101
|
|
||||||
807 0.6536120189
|
|
||||||
808 0.6535912493
|
|
||||||
809 0.6535617421
|
|
||||||
810 0.6535315174
|
|
||||||
811 0.6534972927
|
|
||||||
812 0.6534818476
|
|
||||||
813 0.6534498323
|
|
||||||
814 0.6534305025
|
|
||||||
815 0.6534081059
|
|
||||||
816 0.6533765804
|
|
||||||
817 0.6533441549
|
|
||||||
818 0.6533053405
|
|
||||||
819 0.6532838469
|
|
||||||
820 0.6532604302
|
|
||||||
821 0.6532364412
|
|
||||||
822 0.6532100089
|
|
||||||
823 0.6531782515
|
|
||||||
824 0.6531449701
|
|
||||||
825 0.653115452
|
|
||||||
826 0.6530787602
|
|
||||||
827 0.653052397
|
|
||||||
828 0.6530313579
|
|
||||||
829 0.6530010363
|
|
||||||
830 0.6529752146
|
|
||||||
831 0.652954801
|
|
||||||
832 0.6529330351
|
|
||||||
833 0.6528993709
|
|
||||||
834 0.6528665883
|
|
||||||
835 0.6528413041
|
|
||||||
836 0.6528217161
|
|
||||||
837 0.6527978782
|
|
||||||
838 0.6527789461
|
|
||||||
839 0.6527432001
|
|
||||||
840 0.6527139767
|
|
||||||
841 0.6526857244
|
|
||||||
842 0.652657086
|
|
||||||
843 0.6526355016
|
|
||||||
844 0.6526054936
|
|
||||||
845 0.6525793707
|
|
||||||
846 0.6525584692
|
|
||||||
847 0.6525279747
|
|
||||||
848 0.6525038765
|
|
||||||
849 0.6524849104
|
|
||||||
850 0.6524610603
|
|
||||||
851 0.6524357337
|
|
||||||
852 0.6524082286
|
|
||||||
853 0.65238051
|
|
||||||
854 0.6523557826
|
|
||||||
855 0.6523391233
|
|
||||||
856 0.652325347
|
|
||||||
857 0.6522924958
|
|
||||||
858 0.6522623584
|
|
||||||
859 0.6522343891
|
|
||||||
860 0.6522094424
|
|
||||||
861 0.6521841478
|
|
||||||
862 0.6521657946
|
|
||||||
863 0.6521304278
|
|
||||||
864 0.6521045712
|
|
||||||
865 0.6520753696
|
|
||||||
866 0.6520519528
|
|
||||||
867 0.6520216555
|
|
||||||
868 0.6519926935
|
|
||||||
869 0.6519734186
|
|
||||||
|
@@ -1,871 +0,0 @@
|
|||||||
iter Passed Remaining
|
|
||||||
0 46 93548
|
|
||||||
1 83 83419
|
|
||||||
2 132 88415
|
|
||||||
3 162 81250
|
|
||||||
4 196 78573
|
|
||||||
5 230 76747
|
|
||||||
6 269 76701
|
|
||||||
7 319 79674
|
|
||||||
8 364 80653
|
|
||||||
9 411 81918
|
|
||||||
10 456 82497
|
|
||||||
11 491 81432
|
|
||||||
12 522 79809
|
|
||||||
13 555 78774
|
|
||||||
14 595 78777
|
|
||||||
15 630 78123
|
|
||||||
16 662 77290
|
|
||||||
17 700 77124
|
|
||||||
18 730 76120
|
|
||||||
19 764 75651
|
|
||||||
20 804 75774
|
|
||||||
21 835 75128
|
|
||||||
22 886 76169
|
|
||||||
23 920 75764
|
|
||||||
24 960 75853
|
|
||||||
25 989 75130
|
|
||||||
26 1025 74941
|
|
||||||
27 1060 74714
|
|
||||||
28 1104 75079
|
|
||||||
29 1141 74976
|
|
||||||
30 1180 74975
|
|
||||||
31 1213 74640
|
|
||||||
32 1245 74260
|
|
||||||
33 1287 74434
|
|
||||||
34 1327 74528
|
|
||||||
35 1376 75071
|
|
||||||
36 1427 75741
|
|
||||||
37 1468 75804
|
|
||||||
38 1508 75857
|
|
||||||
39 1549 75922
|
|
||||||
40 1586 75781
|
|
||||||
41 1621 75590
|
|
||||||
42 1663 75705
|
|
||||||
43 1701 75621
|
|
||||||
44 1739 75591
|
|
||||||
45 1776 75460
|
|
||||||
46 1819 75616
|
|
||||||
47 1869 76025
|
|
||||||
48 1916 76288
|
|
||||||
49 1953 76191
|
|
||||||
50 1993 76197
|
|
||||||
51 2038 76381
|
|
||||||
52 2080 76420
|
|
||||||
53 2158 77788
|
|
||||||
54 2220 78529
|
|
||||||
55 2286 79390
|
|
||||||
56 2328 79372
|
|
||||||
57 2367 79254
|
|
||||||
58 2409 79257
|
|
||||||
59 2444 79049
|
|
||||||
60 2484 78985
|
|
||||||
61 2521 78820
|
|
||||||
62 2554 78528
|
|
||||||
63 2593 78466
|
|
||||||
64 2623 78111
|
|
||||||
65 2660 77969
|
|
||||||
66 2695 77776
|
|
||||||
67 2725 77446
|
|
||||||
68 2761 77291
|
|
||||||
69 2791 76975
|
|
||||||
70 2824 76739
|
|
||||||
71 2861 76611
|
|
||||||
72 2897 76476
|
|
||||||
73 2935 76408
|
|
||||||
74 3040 78027
|
|
||||||
75 3097 78411
|
|
||||||
76 3152 78741
|
|
||||||
77 3216 79248
|
|
||||||
78 3256 79195
|
|
||||||
79 3305 79336
|
|
||||||
80 3348 79320
|
|
||||||
81 3381 79089
|
|
||||||
82 3416 78911
|
|
||||||
83 3480 79399
|
|
||||||
84 3535 79649
|
|
||||||
85 3581 79716
|
|
||||||
86 3612 79428
|
|
||||||
87 3644 79185
|
|
||||||
88 3678 78975
|
|
||||||
89 3712 78785
|
|
||||||
90 3743 78531
|
|
||||||
91 3775 78297
|
|
||||||
92 3806 78047
|
|
||||||
93 3837 77821
|
|
||||||
94 3871 77629
|
|
||||||
95 3913 77618
|
|
||||||
96 3945 77403
|
|
||||||
97 3989 77433
|
|
||||||
98 4020 77204
|
|
||||||
99 4053 77020
|
|
||||||
100 4084 76789
|
|
||||||
101 4116 76597
|
|
||||||
102 4148 76401
|
|
||||||
103 4176 76141
|
|
||||||
104 4202 75845
|
|
||||||
105 4232 75634
|
|
||||||
106 4261 75390
|
|
||||||
107 4290 75168
|
|
||||||
108 4324 75018
|
|
||||||
109 4351 74766
|
|
||||||
110 4386 74648
|
|
||||||
111 4424 74577
|
|
||||||
112 4458 74455
|
|
||||||
113 4497 74400
|
|
||||||
114 4533 74307
|
|
||||||
115 4564 74136
|
|
||||||
116 4596 73981
|
|
||||||
117 4628 73818
|
|
||||||
118 4668 73786
|
|
||||||
119 4692 73509
|
|
||||||
120 4723 73354
|
|
||||||
121 4756 73220
|
|
||||||
122 4788 73065
|
|
||||||
123 4815 72854
|
|
||||||
124 4843 72647
|
|
||||||
125 4875 72514
|
|
||||||
126 4916 72515
|
|
||||||
127 4952 72436
|
|
||||||
128 4991 72397
|
|
||||||
129 5028 72327
|
|
||||||
130 5059 72180
|
|
||||||
131 5096 72116
|
|
||||||
132 5125 71946
|
|
||||||
133 5156 71804
|
|
||||||
134 5190 71704
|
|
||||||
135 5221 71564
|
|
||||||
136 5251 71407
|
|
||||||
137 5274 71165
|
|
||||||
138 5309 71084
|
|
||||||
139 5344 71008
|
|
||||||
140 5377 70902
|
|
||||||
141 5416 70866
|
|
||||||
142 5452 70803
|
|
||||||
143 5490 70760
|
|
||||||
144 5521 70641
|
|
||||||
145 5553 70522
|
|
||||||
146 5582 70365
|
|
||||||
147 5611 70217
|
|
||||||
148 5636 70026
|
|
||||||
149 5673 69975
|
|
||||||
150 5706 69874
|
|
||||||
151 5738 69764
|
|
||||||
152 5765 69605
|
|
||||||
153 5795 69471
|
|
||||||
154 5817 69246
|
|
||||||
155 5853 69191
|
|
||||||
156 5888 69122
|
|
||||||
157 5924 69070
|
|
||||||
158 5964 69061
|
|
||||||
159 5996 68963
|
|
||||||
160 6022 68789
|
|
||||||
161 6050 68650
|
|
||||||
162 6079 68510
|
|
||||||
163 6108 68385
|
|
||||||
164 6140 68292
|
|
||||||
165 6169 68162
|
|
||||||
166 6202 68074
|
|
||||||
167 6231 67953
|
|
||||||
168 6263 67858
|
|
||||||
169 6295 67764
|
|
||||||
170 6325 67656
|
|
||||||
171 6356 67561
|
|
||||||
172 6395 67545
|
|
||||||
173 6437 67554
|
|
||||||
174 6472 67495
|
|
||||||
175 6503 67395
|
|
||||||
176 6533 67291
|
|
||||||
177 6562 67174
|
|
||||||
178 6590 67049
|
|
||||||
179 6624 66982
|
|
||||||
180 6655 66882
|
|
||||||
181 6687 66804
|
|
||||||
182 6718 66703
|
|
||||||
183 6751 66632
|
|
||||||
184 6784 66559
|
|
||||||
185 6810 66424
|
|
||||||
186 6832 66246
|
|
||||||
187 6867 66187
|
|
||||||
188 6918 66294
|
|
||||||
189 6969 66393
|
|
||||||
190 7018 66470
|
|
||||||
191 7074 66614
|
|
||||||
192 7117 66635
|
|
||||||
193 7191 66943
|
|
||||||
194 7242 67036
|
|
||||||
195 7282 67027
|
|
||||||
196 7317 66967
|
|
||||||
197 7351 66903
|
|
||||||
198 7389 66879
|
|
||||||
199 7432 66896
|
|
||||||
200 7471 66869
|
|
||||||
201 7506 66814
|
|
||||||
202 7540 66752
|
|
||||||
203 7568 66628
|
|
||||||
204 7605 66596
|
|
||||||
205 7638 66519
|
|
||||||
206 7665 66397
|
|
||||||
207 7700 66340
|
|
||||||
208 7734 66276
|
|
||||||
209 7766 66197
|
|
||||||
210 7796 66106
|
|
||||||
211 7831 66053
|
|
||||||
212 7871 66037
|
|
||||||
213 7910 66016
|
|
||||||
214 7951 66014
|
|
||||||
215 7989 65983
|
|
||||||
216 8025 65946
|
|
||||||
217 8058 65872
|
|
||||||
218 8087 65768
|
|
||||||
219 8112 65638
|
|
||||||
220 8148 65594
|
|
||||||
221 8197 65655
|
|
||||||
222 8239 65655
|
|
||||||
223 8268 65556
|
|
||||||
224 8298 65466
|
|
||||||
225 8327 65366
|
|
||||||
226 8357 65278
|
|
||||||
227 8384 65167
|
|
||||||
228 8418 65103
|
|
||||||
229 8453 65058
|
|
||||||
230 8490 65020
|
|
||||||
231 8523 64958
|
|
||||||
232 8550 64848
|
|
||||||
233 8575 64718
|
|
||||||
234 8607 64648
|
|
||||||
235 8635 64545
|
|
||||||
236 8660 64426
|
|
||||||
237 8691 64345
|
|
||||||
238 8719 64250
|
|
||||||
239 8746 64137
|
|
||||||
240 8773 64038
|
|
||||||
241 8803 63951
|
|
||||||
242 8833 63873
|
|
||||||
243 8862 63779
|
|
||||||
244 8892 63698
|
|
||||||
245 8932 63688
|
|
||||||
246 8962 63611
|
|
||||||
247 8991 63521
|
|
||||||
248 9021 63442
|
|
||||||
249 9051 63358
|
|
||||||
250 9085 63306
|
|
||||||
251 9110 63193
|
|
||||||
252 9137 63093
|
|
||||||
253 9174 63066
|
|
||||||
254 9196 62935
|
|
||||||
255 9238 62934
|
|
||||||
256 9267 62855
|
|
||||||
257 9297 62776
|
|
||||||
258 9324 62681
|
|
||||||
259 9357 62625
|
|
||||||
260 9388 62552
|
|
||||||
261 9427 62536
|
|
||||||
262 9461 62491
|
|
||||||
263 9496 62443
|
|
||||||
264 9524 62356
|
|
||||||
265 9553 62278
|
|
||||||
266 9590 62247
|
|
||||||
267 9620 62172
|
|
||||||
268 9645 62071
|
|
||||||
269 9682 62040
|
|
||||||
270 9711 61962
|
|
||||||
271 9739 61872
|
|
||||||
272 9768 61797
|
|
||||||
273 9804 61761
|
|
||||||
274 9848 61777
|
|
||||||
275 9886 61755
|
|
||||||
276 9925 61740
|
|
||||||
277 9965 61728
|
|
||||||
278 9995 61656
|
|
||||||
279 10022 61564
|
|
||||||
280 10055 61516
|
|
||||||
281 10080 61410
|
|
||||||
282 10111 61344
|
|
||||||
283 10147 61311
|
|
||||||
284 10175 61230
|
|
||||||
285 10202 61141
|
|
||||||
286 10234 61084
|
|
||||||
287 10264 61018
|
|
||||||
288 10299 60977
|
|
||||||
289 10323 60874
|
|
||||||
290 10353 60804
|
|
||||||
291 10394 60803
|
|
||||||
292 10431 60773
|
|
||||||
293 10471 60763
|
|
||||||
294 10503 60707
|
|
||||||
295 10534 60645
|
|
||||||
296 10576 60646
|
|
||||||
297 10612 60612
|
|
||||||
298 10639 60525
|
|
||||||
299 10668 60453
|
|
||||||
300 10702 60411
|
|
||||||
301 10729 60326
|
|
||||||
302 10764 60290
|
|
||||||
303 10801 60263
|
|
||||||
304 10829 60182
|
|
||||||
305 10857 60108
|
|
||||||
306 10892 60067
|
|
||||||
307 10930 60047
|
|
||||||
308 10972 60045
|
|
||||||
309 11002 59983
|
|
||||||
310 11030 59902
|
|
||||||
311 11058 59828
|
|
||||||
312 11092 59788
|
|
||||||
313 11117 59696
|
|
||||||
314 11149 59641
|
|
||||||
315 11187 59617
|
|
||||||
316 11211 59525
|
|
||||||
317 11243 59468
|
|
||||||
318 11274 59413
|
|
||||||
319 11304 59346
|
|
||||||
320 11334 59287
|
|
||||||
321 11362 59209
|
|
||||||
322 11394 59158
|
|
||||||
323 11436 59158
|
|
||||||
324 11477 59153
|
|
||||||
325 11513 59122
|
|
||||||
326 11547 59081
|
|
||||||
327 11572 58991
|
|
||||||
328 11607 58956
|
|
||||||
329 11637 58894
|
|
||||||
330 11668 58833
|
|
||||||
331 11700 58785
|
|
||||||
332 11724 58694
|
|
||||||
333 11757 58648
|
|
||||||
334 11780 58550
|
|
||||||
335 11815 58515
|
|
||||||
336 11844 58451
|
|
||||||
337 11869 58364
|
|
||||||
338 11905 58335
|
|
||||||
339 11941 58302
|
|
||||||
340 11986 58315
|
|
||||||
341 12020 58274
|
|
||||||
342 12066 58292
|
|
||||||
343 12122 58358
|
|
||||||
344 12177 58415
|
|
||||||
345 12221 58422
|
|
||||||
346 12264 58423
|
|
||||||
347 12300 58394
|
|
||||||
348 12324 58304
|
|
||||||
349 12354 58243
|
|
||||||
350 12401 58262
|
|
||||||
351 12438 58232
|
|
||||||
352 12479 58228
|
|
||||||
353 12512 58179
|
|
||||||
354 12541 58116
|
|
||||||
355 12569 58044
|
|
||||||
356 12597 57977
|
|
||||||
357 12628 57920
|
|
||||||
358 12653 57839
|
|
||||||
359 12682 57775
|
|
||||||
360 12720 57752
|
|
||||||
361 12744 57666
|
|
||||||
362 12770 57592
|
|
||||||
363 12811 57583
|
|
||||||
364 12841 57522
|
|
||||||
365 12870 57460
|
|
||||||
366 12897 57386
|
|
||||||
367 12938 57378
|
|
||||||
368 12974 57347
|
|
||||||
369 13009 57313
|
|
||||||
370 13038 57249
|
|
||||||
371 13078 57235
|
|
||||||
372 13117 57216
|
|
||||||
373 13147 57159
|
|
||||||
374 13181 57118
|
|
||||||
375 13205 57036
|
|
||||||
376 13235 56979
|
|
||||||
377 13274 56960
|
|
||||||
378 13306 56911
|
|
||||||
379 13333 56841
|
|
||||||
380 13366 56798
|
|
||||||
381 13396 56741
|
|
||||||
382 13421 56666
|
|
||||||
383 13467 56674
|
|
||||||
384 13508 56664
|
|
||||||
385 13540 56616
|
|
||||||
386 13569 56559
|
|
||||||
387 13598 56496
|
|
||||||
388 13627 56438
|
|
||||||
389 13656 56376
|
|
||||||
390 13685 56317
|
|
||||||
391 13717 56271
|
|
||||||
392 13750 56227
|
|
||||||
393 13771 56135
|
|
||||||
394 13804 56090
|
|
||||||
395 13825 55999
|
|
||||||
396 13858 55957
|
|
||||||
397 13888 55904
|
|
||||||
398 13917 55843
|
|
||||||
399 13953 55812
|
|
||||||
400 13994 55802
|
|
||||||
401 14025 55752
|
|
||||||
402 14048 55670
|
|
||||||
403 14076 55607
|
|
||||||
404 14105 55551
|
|
||||||
405 14142 55526
|
|
||||||
406 14182 55511
|
|
||||||
407 14214 55464
|
|
||||||
408 14240 55394
|
|
||||||
409 14267 55328
|
|
||||||
410 14299 55284
|
|
||||||
411 14324 55213
|
|
||||||
412 14351 55146
|
|
||||||
413 14379 55086
|
|
||||||
414 14410 55036
|
|
||||||
415 14451 55025
|
|
||||||
416 14484 54984
|
|
||||||
417 14513 54929
|
|
||||||
418 14536 54851
|
|
||||||
419 14565 54793
|
|
||||||
420 14587 54710
|
|
||||||
421 14615 54650
|
|
||||||
422 14642 54588
|
|
||||||
423 14666 54515
|
|
||||||
424 14690 54441
|
|
||||||
425 14719 54384
|
|
||||||
426 14739 54297
|
|
||||||
427 14772 54257
|
|
||||||
428 14790 54164
|
|
||||||
429 14824 54125
|
|
||||||
430 14844 54039
|
|
||||||
431 14876 53995
|
|
||||||
432 14906 53946
|
|
||||||
433 14938 53902
|
|
||||||
434 14980 53894
|
|
||||||
435 15006 53829
|
|
||||||
436 15033 53770
|
|
||||||
437 15059 53706
|
|
||||||
438 15085 53639
|
|
||||||
439 15110 53574
|
|
||||||
440 15134 53503
|
|
||||||
441 15160 53438
|
|
||||||
442 15184 53369
|
|
||||||
443 15211 53308
|
|
||||||
444 15234 53236
|
|
||||||
445 15266 53193
|
|
||||||
446 15287 53114
|
|
||||||
447 15316 53059
|
|
||||||
448 15336 52978
|
|
||||||
449 15366 52929
|
|
||||||
450 15393 52870
|
|
||||||
451 15429 52843
|
|
||||||
452 15469 52828
|
|
||||||
453 15490 52748
|
|
||||||
454 15523 52712
|
|
||||||
455 15550 52653
|
|
||||||
456 15577 52594
|
|
||||||
457 15604 52536
|
|
||||||
458 15630 52476
|
|
||||||
459 15656 52414
|
|
||||||
460 15682 52353
|
|
||||||
461 15711 52304
|
|
||||||
462 15736 52238
|
|
||||||
463 15765 52188
|
|
||||||
464 15786 52112
|
|
||||||
465 15817 52068
|
|
||||||
466 15839 51996
|
|
||||||
467 15873 51961
|
|
||||||
468 15903 51916
|
|
||||||
469 15935 51873
|
|
||||||
470 15969 51840
|
|
||||||
471 15994 51779
|
|
||||||
472 16022 51726
|
|
||||||
473 16047 51663
|
|
||||||
474 16073 51605
|
|
||||||
475 16099 51546
|
|
||||||
476 16128 51495
|
|
||||||
477 16152 51431
|
|
||||||
478 16176 51367
|
|
||||||
479 16205 51317
|
|
||||||
480 16228 51250
|
|
||||||
481 16255 51194
|
|
||||||
482 16277 51123
|
|
||||||
483 16305 51071
|
|
||||||
484 16328 51005
|
|
||||||
485 16362 50973
|
|
||||||
486 16392 50928
|
|
||||||
487 16426 50894
|
|
||||||
488 16459 50860
|
|
||||||
489 16480 50787
|
|
||||||
490 16510 50743
|
|
||||||
491 16530 50668
|
|
||||||
492 16561 50625
|
|
||||||
493 16585 50562
|
|
||||||
494 16613 50510
|
|
||||||
495 16638 50453
|
|
||||||
496 16663 50393
|
|
||||||
497 16690 50339
|
|
||||||
498 16716 50282
|
|
||||||
499 16740 50222
|
|
||||||
500 16773 50186
|
|
||||||
501 16802 50139
|
|
||||||
502 16836 50107
|
|
||||||
503 16873 50085
|
|
||||||
504 16921 50094
|
|
||||||
505 16989 50163
|
|
||||||
506 17038 50173
|
|
||||||
507 17069 50132
|
|
||||||
508 17110 50121
|
|
||||||
509 17145 50091
|
|
||||||
510 17190 50091
|
|
||||||
511 17219 50044
|
|
||||||
512 17247 49994
|
|
||||||
513 17271 49932
|
|
||||||
514 17298 49878
|
|
||||||
515 17343 49878
|
|
||||||
516 17373 49836
|
|
||||||
517 17417 49831
|
|
||||||
518 17460 49823
|
|
||||||
519 17490 49781
|
|
||||||
520 17518 49731
|
|
||||||
521 17546 49680
|
|
||||||
522 17571 49622
|
|
||||||
523 17600 49577
|
|
||||||
524 17625 49520
|
|
||||||
525 17655 49474
|
|
||||||
526 17679 49414
|
|
||||||
527 17707 49366
|
|
||||||
528 17729 49300
|
|
||||||
529 17758 49254
|
|
||||||
530 17781 49191
|
|
||||||
531 17808 49141
|
|
||||||
532 17829 49071
|
|
||||||
533 17862 49038
|
|
||||||
534 17905 49031
|
|
||||||
535 18028 49241
|
|
||||||
536 18072 49236
|
|
||||||
537 18106 49203
|
|
||||||
538 18135 49157
|
|
||||||
539 18165 49114
|
|
||||||
540 18200 49083
|
|
||||||
541 18223 49022
|
|
||||||
542 18254 48980
|
|
||||||
543 18280 48927
|
|
||||||
544 18307 48876
|
|
||||||
545 18338 48834
|
|
||||||
546 18367 48790
|
|
||||||
547 18411 48783
|
|
||||||
548 18444 48747
|
|
||||||
549 18470 48693
|
|
||||||
550 18503 48660
|
|
||||||
551 18531 48611
|
|
||||||
552 18557 48558
|
|
||||||
553 18584 48508
|
|
||||||
554 18625 48493
|
|
||||||
555 18650 48436
|
|
||||||
556 18677 48388
|
|
||||||
557 18703 48333
|
|
||||||
558 18729 48282
|
|
||||||
559 18756 48231
|
|
||||||
560 18781 48176
|
|
||||||
561 18808 48126
|
|
||||||
562 18834 48074
|
|
||||||
563 18869 48043
|
|
||||||
564 18902 48008
|
|
||||||
565 18930 47960
|
|
||||||
566 18958 47914
|
|
||||||
567 18983 47859
|
|
||||||
568 19016 47824
|
|
||||||
569 19037 47761
|
|
||||||
570 19068 47720
|
|
||||||
571 19090 47660
|
|
||||||
572 19111 47595
|
|
||||||
573 19141 47553
|
|
||||||
574 19164 47494
|
|
||||||
575 19196 47458
|
|
||||||
576 19217 47393
|
|
||||||
577 19249 47358
|
|
||||||
578 19274 47303
|
|
||||||
579 19298 47247
|
|
||||||
580 19324 47195
|
|
||||||
581 19357 47162
|
|
||||||
582 19391 47130
|
|
||||||
583 19427 47103
|
|
||||||
584 19460 47070
|
|
||||||
585 19483 47012
|
|
||||||
586 19511 46967
|
|
||||||
587 19542 46929
|
|
||||||
588 19564 46867
|
|
||||||
589 19597 46833
|
|
||||||
590 19621 46779
|
|
||||||
591 19647 46729
|
|
||||||
592 19670 46672
|
|
||||||
593 19699 46627
|
|
||||||
594 19726 46582
|
|
||||||
595 19753 46532
|
|
||||||
596 19778 46480
|
|
||||||
597 19803 46429
|
|
||||||
598 19830 46381
|
|
||||||
599 19857 46335
|
|
||||||
600 19896 46313
|
|
||||||
601 19925 46271
|
|
||||||
602 19957 46236
|
|
||||||
603 19991 46204
|
|
||||||
604 20019 46159
|
|
||||||
605 20047 46115
|
|
||||||
606 20072 46063
|
|
||||||
607 20098 46015
|
|
||||||
608 20123 45963
|
|
||||||
609 20149 45913
|
|
||||||
610 20176 45867
|
|
||||||
611 20202 45817
|
|
||||||
612 20230 45774
|
|
||||||
613 20253 45719
|
|
||||||
614 20285 45682
|
|
||||||
615 20307 45626
|
|
||||||
616 20338 45589
|
|
||||||
617 20361 45532
|
|
||||||
618 20394 45500
|
|
||||||
619 20423 45459
|
|
||||||
620 20454 45420
|
|
||||||
621 20488 45390
|
|
||||||
622 20510 45333
|
|
||||||
623 20543 45301
|
|
||||||
624 20569 45252
|
|
||||||
625 20594 45201
|
|
||||||
626 20619 45151
|
|
||||||
627 20646 45107
|
|
||||||
628 20675 45066
|
|
||||||
629 20701 45016
|
|
||||||
630 20727 44970
|
|
||||||
631 20752 44919
|
|
||||||
632 20782 44881
|
|
||||||
633 20804 44825
|
|
||||||
634 20837 44791
|
|
||||||
635 20862 44742
|
|
||||||
636 20892 44704
|
|
||||||
637 20931 44683
|
|
||||||
638 20960 44643
|
|
||||||
639 20994 44612
|
|
||||||
640 21022 44570
|
|
||||||
641 21052 44531
|
|
||||||
642 21082 44493
|
|
||||||
643 21107 44443
|
|
||||||
644 21135 44401
|
|
||||||
645 21160 44351
|
|
||||||
646 21185 44302
|
|
||||||
647 21210 44253
|
|
||||||
648 21236 44208
|
|
||||||
649 21262 44161
|
|
||||||
650 21288 44113
|
|
||||||
651 21315 44068
|
|
||||||
652 21343 44027
|
|
||||||
653 21377 43997
|
|
||||||
654 21403 43949
|
|
||||||
655 21440 43926
|
|
||||||
656 21477 43903
|
|
||||||
657 21502 43854
|
|
||||||
658 21533 43819
|
|
||||||
659 21559 43772
|
|
||||||
660 21586 43727
|
|
||||||
661 21611 43680
|
|
||||||
662 21637 43633
|
|
||||||
663 21662 43586
|
|
||||||
664 21688 43539
|
|
||||||
665 21714 43493
|
|
||||||
666 21742 43451
|
|
||||||
667 21771 43413
|
|
||||||
668 21818 43409
|
|
||||||
669 21846 43366
|
|
||||||
670 21888 43352
|
|
||||||
671 21934 43345
|
|
||||||
672 21971 43322
|
|
||||||
673 22019 43320
|
|
||||||
674 22053 43289
|
|
||||||
675 22090 43266
|
|
||||||
676 22141 43269
|
|
||||||
677 22176 43240
|
|
||||||
678 22213 43215
|
|
||||||
679 22239 43171
|
|
||||||
680 22270 43134
|
|
||||||
681 22296 43088
|
|
||||||
682 22321 43041
|
|
||||||
683 22350 43002
|
|
||||||
684 22379 42962
|
|
||||||
685 22419 42944
|
|
||||||
686 22452 42912
|
|
||||||
687 22484 42878
|
|
||||||
688 22511 42834
|
|
||||||
689 22537 42789
|
|
||||||
690 22571 42757
|
|
||||||
691 22598 42714
|
|
||||||
692 22624 42669
|
|
||||||
693 22653 42630
|
|
||||||
694 22680 42586
|
|
||||||
695 22708 42545
|
|
||||||
696 22739 42510
|
|
||||||
697 22761 42457
|
|
||||||
698 22792 42421
|
|
||||||
699 22816 42373
|
|
||||||
700 22845 42333
|
|
||||||
701 22870 42288
|
|
||||||
702 22902 42253
|
|
||||||
703 22942 42234
|
|
||||||
704 22974 42201
|
|
||||||
705 23002 42160
|
|
||||||
706 23033 42124
|
|
||||||
707 23054 42071
|
|
||||||
708 23086 42038
|
|
||||||
709 23115 41999
|
|
||||||
710 23143 41957
|
|
||||||
711 23169 41914
|
|
||||||
712 23195 41868
|
|
||||||
713 23230 41840
|
|
||||||
714 23259 41801
|
|
||||||
715 23287 41760
|
|
||||||
716 23311 41713
|
|
||||||
717 23341 41676
|
|
||||||
718 23372 41641
|
|
||||||
719 23405 41610
|
|
||||||
720 23438 41578
|
|
||||||
721 23483 41566
|
|
||||||
722 23507 41519
|
|
||||||
723 23540 41488
|
|
||||||
724 23566 41444
|
|
||||||
725 23595 41406
|
|
||||||
726 23623 41365
|
|
||||||
727 23648 41320
|
|
||||||
728 23677 41281
|
|
||||||
729 23700 41231
|
|
||||||
730 23728 41192
|
|
||||||
731 23752 41144
|
|
||||||
732 23784 41111
|
|
||||||
733 23807 41063
|
|
||||||
734 23840 41031
|
|
||||||
735 23870 40994
|
|
||||||
736 23908 40972
|
|
||||||
737 23941 40940
|
|
||||||
738 23974 40909
|
|
||||||
739 24006 40875
|
|
||||||
740 24036 40838
|
|
||||||
741 24064 40798
|
|
||||||
742 24092 40759
|
|
||||||
743 24127 40730
|
|
||||||
744 24153 40688
|
|
||||||
745 24179 40644
|
|
||||||
746 24207 40604
|
|
||||||
747 24233 40561
|
|
||||||
748 24261 40522
|
|
||||||
749 24295 40491
|
|
||||||
750 24318 40444
|
|
||||||
751 24349 40410
|
|
||||||
752 24376 40368
|
|
||||||
753 24408 40335
|
|
||||||
754 24442 40306
|
|
||||||
755 24474 40273
|
|
||||||
756 24508 40242
|
|
||||||
757 24548 40222
|
|
||||||
758 24575 40182
|
|
||||||
759 24605 40145
|
|
||||||
760 24632 40104
|
|
||||||
761 24660 40064
|
|
||||||
762 24689 40027
|
|
||||||
763 24714 39982
|
|
||||||
764 24745 39949
|
|
||||||
765 24766 39897
|
|
||||||
766 24797 39863
|
|
||||||
767 24825 39823
|
|
||||||
768 24854 39786
|
|
||||||
769 24880 39744
|
|
||||||
770 24909 39706
|
|
||||||
771 24940 39672
|
|
||||||
772 24970 39635
|
|
||||||
773 25004 39606
|
|
||||||
774 25030 39564
|
|
||||||
775 25056 39522
|
|
||||||
776 25086 39486
|
|
||||||
777 25107 39436
|
|
||||||
778 25139 39403
|
|
||||||
779 25159 39351
|
|
||||||
780 25188 39314
|
|
||||||
781 25214 39272
|
|
||||||
782 25240 39230
|
|
||||||
783 25266 39188
|
|
||||||
784 25288 39141
|
|
||||||
785 25315 39101
|
|
||||||
786 25341 39058
|
|
||||||
787 25367 39016
|
|
||||||
788 25391 38972
|
|
||||||
789 25417 38930
|
|
||||||
790 25448 38895
|
|
||||||
791 25482 38867
|
|
||||||
792 25514 38834
|
|
||||||
793 25542 38795
|
|
||||||
794 25569 38756
|
|
||||||
795 25595 38714
|
|
||||||
796 25618 38669
|
|
||||||
797 25643 38626
|
|
||||||
798 25667 38581
|
|
||||||
799 25695 38543
|
|
||||||
800 25716 38494
|
|
||||||
801 25743 38454
|
|
||||||
802 25770 38415
|
|
||||||
803 25790 38364
|
|
||||||
804 25822 38332
|
|
||||||
805 25843 38284
|
|
||||||
806 25873 38249
|
|
||||||
807 25896 38203
|
|
||||||
808 25925 38167
|
|
||||||
809 25955 38131
|
|
||||||
810 25988 38101
|
|
||||||
811 26028 38080
|
|
||||||
812 26055 38042
|
|
||||||
813 26081 38000
|
|
||||||
814 26108 37961
|
|
||||||
815 26131 37916
|
|
||||||
816 26159 37878
|
|
||||||
817 26188 37841
|
|
||||||
818 26214 37800
|
|
||||||
819 26242 37764
|
|
||||||
820 26272 37728
|
|
||||||
821 26298 37688
|
|
||||||
822 26327 37652
|
|
||||||
823 26359 37619
|
|
||||||
824 26385 37580
|
|
||||||
825 26408 37534
|
|
||||||
826 26444 37507
|
|
||||||
827 26477 37478
|
|
||||||
828 26517 37456
|
|
||||||
829 26539 37411
|
|
||||||
830 26573 37382
|
|
||||||
831 26597 37339
|
|
||||||
832 26623 37298
|
|
||||||
833 26650 37259
|
|
||||||
834 26677 37221
|
|
||||||
835 26704 37182
|
|
||||||
836 26728 37138
|
|
||||||
837 26763 37111
|
|
||||||
838 26791 37073
|
|
||||||
839 26822 37041
|
|
||||||
840 26872 37033
|
|
||||||
841 26924 37029
|
|
||||||
842 26982 37033
|
|
||||||
843 27054 37055
|
|
||||||
844 27097 37038
|
|
||||||
845 27120 36994
|
|
||||||
846 27146 36954
|
|
||||||
847 27180 36925
|
|
||||||
848 27206 36884
|
|
||||||
849 27234 36846
|
|
||||||
850 27260 36807
|
|
||||||
851 27289 36770
|
|
||||||
852 27318 36734
|
|
||||||
853 27347 36698
|
|
||||||
854 27386 36675
|
|
||||||
855 27413 36637
|
|
||||||
856 27439 36596
|
|
||||||
857 27471 36564
|
|
||||||
858 27501 36529
|
|
||||||
859 27535 36500
|
|
||||||
860 27572 36474
|
|
||||||
861 27595 36431
|
|
||||||
862 27627 36398
|
|
||||||
863 27654 36360
|
|
||||||
864 27683 36324
|
|
||||||
865 27711 36287
|
|
||||||
866 27738 36249
|
|
||||||
867 27765 36210
|
|
||||||
868 27794 36175
|
|
||||||
869 27820 36135
|
|
||||||
|
@@ -0,0 +1,131 @@
|
|||||||
|
{
|
||||||
|
"version": "2026-06-10T23:11:49",
|
||||||
|
"fitted_on": {
|
||||||
|
"days": 540,
|
||||||
|
"test_days": 90,
|
||||||
|
"n_train": 64603,
|
||||||
|
"n_test": 14450
|
||||||
|
},
|
||||||
|
"validated": {
|
||||||
|
"ece_home": {
|
||||||
|
"raw": 0.01803,
|
||||||
|
"active_before": 0.01312,
|
||||||
|
"candidate_oos": 0.01301
|
||||||
|
},
|
||||||
|
"ece_away": {
|
||||||
|
"raw": 0.01234,
|
||||||
|
"active_before": 0.01234,
|
||||||
|
"candidate_oos": 0.00845
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gates": {
|
||||||
|
"min_n": 1500,
|
||||||
|
"shrink": 0.5,
|
||||||
|
"clip": 0.05,
|
||||||
|
"min_delta": 0.004
|
||||||
|
},
|
||||||
|
"corrections": {
|
||||||
|
"ms_home": [
|
||||||
|
{
|
||||||
|
"lo": 0.05,
|
||||||
|
"hi": 0.15,
|
||||||
|
"delta": 0.0,
|
||||||
|
"n": 2124,
|
||||||
|
"raw_gap": -0.0072
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.15,
|
||||||
|
"hi": 0.25,
|
||||||
|
"delta": 0.0,
|
||||||
|
"n": 6476,
|
||||||
|
"raw_gap": -0.0031
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.25,
|
||||||
|
"hi": 0.35,
|
||||||
|
"delta": 0.0,
|
||||||
|
"n": 12565,
|
||||||
|
"raw_gap": -0.0018
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.35,
|
||||||
|
"hi": 0.45,
|
||||||
|
"delta": 0.0,
|
||||||
|
"n": 16431,
|
||||||
|
"raw_gap": 0.006
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.45,
|
||||||
|
"hi": 0.55,
|
||||||
|
"delta": 0.0124,
|
||||||
|
"n": 12995,
|
||||||
|
"raw_gap": 0.0248
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.55,
|
||||||
|
"hi": 0.65,
|
||||||
|
"delta": 0.0154,
|
||||||
|
"n": 8479,
|
||||||
|
"raw_gap": 0.0307
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.65,
|
||||||
|
"hi": 0.75,
|
||||||
|
"delta": 0.0203,
|
||||||
|
"n": 4638,
|
||||||
|
"raw_gap": 0.0407
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ms_away": [
|
||||||
|
{
|
||||||
|
"lo": 0.05,
|
||||||
|
"hi": 0.15,
|
||||||
|
"delta": -0.0077,
|
||||||
|
"n": 6762,
|
||||||
|
"raw_gap": -0.0154
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.15,
|
||||||
|
"hi": 0.25,
|
||||||
|
"delta": -0.0048,
|
||||||
|
"n": 16211,
|
||||||
|
"raw_gap": -0.0097
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.25,
|
||||||
|
"hi": 0.35,
|
||||||
|
"delta": 0.0,
|
||||||
|
"n": 18440,
|
||||||
|
"raw_gap": -0.002
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.35,
|
||||||
|
"hi": 0.45,
|
||||||
|
"delta": 0.009,
|
||||||
|
"n": 12061,
|
||||||
|
"raw_gap": 0.0181
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.45,
|
||||||
|
"hi": 0.55,
|
||||||
|
"delta": 0.0116,
|
||||||
|
"n": 5930,
|
||||||
|
"raw_gap": 0.0231
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.55,
|
||||||
|
"hi": 0.65,
|
||||||
|
"delta": 0.0199,
|
||||||
|
"n": 3287,
|
||||||
|
"raw_gap": 0.0399
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lo": 0.65,
|
||||||
|
"hi": 0.75,
|
||||||
|
"delta": 0.0295,
|
||||||
|
"n": 1580,
|
||||||
|
"raw_gap": 0.0589
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"source": "bt_10k",
|
||||||
|
"thresholds": "high:roi>10&n>=20 | low:roi<-5&n>=15 | unknown:n<10"
|
||||||
|
},
|
||||||
|
"lookup": {
|
||||||
|
"32n2r9bl6x90psj0wa7bfs6vq": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 102.2,
|
||||||
|
"bet_n": 23,
|
||||||
|
"hit": 30.4,
|
||||||
|
"name": "Sudamericana"
|
||||||
|
},
|
||||||
|
"59tpnfrwnvhnhzmnvfyug68hj": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 63.5,
|
||||||
|
"bet_n": 23,
|
||||||
|
"hit": 30.4,
|
||||||
|
"name": "Libertadores Kupası"
|
||||||
|
},
|
||||||
|
"b60nisd3qn427jm0hrg9kvmab": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 49.7,
|
||||||
|
"bet_n": 22,
|
||||||
|
"hit": 22.7,
|
||||||
|
"name": "Allsvenskan"
|
||||||
|
},
|
||||||
|
"scf9p4y91yjvqvg5jndxzhxj": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 33.8,
|
||||||
|
"bet_n": 100,
|
||||||
|
"hit": 25.0,
|
||||||
|
"name": "Serie A"
|
||||||
|
},
|
||||||
|
"4oogyu6o156iphvdvphwpck10": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 32.3,
|
||||||
|
"bet_n": 23,
|
||||||
|
"hit": 21.7,
|
||||||
|
"name": "Şampiyonlar Ligi"
|
||||||
|
},
|
||||||
|
"89ovpy1rarewwzqvi30bfdr8b": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 29.4,
|
||||||
|
"bet_n": 50,
|
||||||
|
"hit": 24.0,
|
||||||
|
"name": "1. Lig"
|
||||||
|
},
|
||||||
|
"82jkgccg7phfjpd0mltdl3pat": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 25.8,
|
||||||
|
"bet_n": 29,
|
||||||
|
"hit": 27.6,
|
||||||
|
"name": "Süper Lig"
|
||||||
|
},
|
||||||
|
"3is4bkgf3loxv9qfg3hm8zfqb": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 25.5,
|
||||||
|
"bet_n": 84,
|
||||||
|
"hit": 19.0,
|
||||||
|
"name": "LaLiga 2"
|
||||||
|
},
|
||||||
|
"enzlj1as2raqm4ids1zyb07y1": {
|
||||||
|
"label": "medium",
|
||||||
|
"bet_roi": 23.7,
|
||||||
|
"bet_n": 19,
|
||||||
|
"hit": 26.3,
|
||||||
|
"name": "USL 2. Lig"
|
||||||
|
},
|
||||||
|
"9ynnnx1qmkizq1o3qr3v0nsuk": {
|
||||||
|
"label": "high",
|
||||||
|
"bet_roi": 16.3,
|
||||||
|
"bet_n": 38,
|
||||||
|
"hit": 21.1,
|
||||||
|
"name": "Eliteserien"
|
||||||
|
},
|
||||||
|
"8ey0ww2zsosdmwr8ehsorh6t7": {
|
||||||
|
"label": "medium",
|
||||||
|
"bet_roi": 5.4,
|
||||||
|
"bet_n": 80,
|
||||||
|
"hit": 16.2,
|
||||||
|
"name": "Serie B"
|
||||||
|
},
|
||||||
|
"dm5ka0os1e3dxcp3vh05kmp33": {
|
||||||
|
"label": "low",
|
||||||
|
"bet_roi": -7.4,
|
||||||
|
"bet_n": 46,
|
||||||
|
"hit": 26.1,
|
||||||
|
"name": "Ligue 1"
|
||||||
|
},
|
||||||
|
"4zwgbb66rif2spcoeeol2motx": {
|
||||||
|
"label": "low",
|
||||||
|
"bet_roi": -12.7,
|
||||||
|
"bet_n": 39,
|
||||||
|
"hit": 23.1,
|
||||||
|
"name": "Pro Lig"
|
||||||
|
},
|
||||||
|
"a4fgj2rfbpf4ejo1qi624fefo": {
|
||||||
|
"label": "low",
|
||||||
|
"bet_roi": -14.2,
|
||||||
|
"bet_n": 73,
|
||||||
|
"hit": 17.8,
|
||||||
|
"name": "3. Lig"
|
||||||
|
},
|
||||||
|
"9chuiarcjofld1dkj9kysehmb": {
|
||||||
|
"label": "low",
|
||||||
|
"bet_roi": -14.9,
|
||||||
|
"bet_n": 22,
|
||||||
|
"hit": 13.6,
|
||||||
|
"name": "Superettan"
|
||||||
|
},
|
||||||
|
"3p81ltz6845appgkbgkzxueii": {
|
||||||
|
"label": "low",
|
||||||
|
"bet_roi": -19.8,
|
||||||
|
"bet_n": 34,
|
||||||
|
"hit": 14.7,
|
||||||
|
"name": "2. Lig"
|
||||||
|
},
|
||||||
|
"dvstmwnvw0mt5p38twn9yttyb": {
|
||||||
|
"label": "low",
|
||||||
|
"bet_roi": -37.2,
|
||||||
|
"bet_n": 19,
|
||||||
|
"hit": 26.3,
|
||||||
|
"name": "Veikkausliiga"
|
||||||
|
},
|
||||||
|
"zs18qaehvhg3w1208874zvfa": {
|
||||||
|
"label": "low",
|
||||||
|
"bet_roi": -62.0,
|
||||||
|
"bet_n": 17,
|
||||||
|
"hit": 23.5,
|
||||||
|
"name": "1. Lig"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"purpose": "A-milli erkek futbol ligleri. betting_brain milli-maç gate'i bu listeyle tetiklenir.",
|
||||||
|
"strategy": "Milli maçta SADECE MS, oran 4.0-7.0, Hazırlık+Eleme oynanabilir. Turnuva/diğer market analiz-only. Backtest: +17% ROI, kararlılık-test geçti (eski/yeni yarı +22/+24%).",
|
||||||
|
"source": "2300-maç milli backtest (multi_backtest_20260602) segment+grid+stability analizi",
|
||||||
|
"competition_type_rule": "lig adı: 'hazırlık'->HAZIRLIK | 'eleme'/'play-off'->ELEME | diğer->TURNUVA"
|
||||||
|
},
|
||||||
|
"league_ids": [
|
||||||
|
"cesdwwnxbc5fmajgroc0hqzy2",
|
||||||
|
"3aa4mumjl6zyetg6o9hwd5hhx",
|
||||||
|
"40yjcbx2sq6oq736iqqqczwt1",
|
||||||
|
"39q1hq42hxjfylxb7xpe9bvf9",
|
||||||
|
"cu0rmpyff5692eo06ltddjo8a",
|
||||||
|
"ax1yf4nlzqpcji4j8epdgx3zl",
|
||||||
|
"1gxlzw2ezkyeykhcaa5x8ozkk",
|
||||||
|
"gfskxsdituog2kqp9yiu7bzi",
|
||||||
|
"595nsvo7ykvoe690b1e4u5n56",
|
||||||
|
"68zplepppndhl8bfdvgy9vgu1",
|
||||||
|
"3a0j0giz3c3ajw9h59evv7lqt",
|
||||||
|
"emy1ibc8fu2l0fukh4vlu5xl5",
|
||||||
|
"2db0aw1duj2my9l5iey5gm6nq",
|
||||||
|
"cc5tzz23tryrfqbm2pbv0jill",
|
||||||
|
"8tddm56zbasf57jkkay4kbf11",
|
||||||
|
"2r1hqz453bn9ljzt53kdr2lwb",
|
||||||
|
"93i7thp7zi0ympyt6l8aa1r2i",
|
||||||
|
"45db8orh1qttbsqq9hqapmbit",
|
||||||
|
"ude9t6yj60lebbn356qzg4k4",
|
||||||
|
"9qzn8cs96sgtqmesa9gpfti23",
|
||||||
|
"ad8y7vdjhinfqv4wo8rod6dck"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -465,3 +465,105 @@ def get_calibrator() -> Calibrator:
|
|||||||
if _calibrator_instance is None:
|
if _calibrator_instance is None:
|
||||||
_calibrator_instance = Calibrator()
|
_calibrator_instance = Calibrator()
|
||||||
return _calibrator_instance
|
return _calibrator_instance
|
||||||
|
|
||||||
|
|
||||||
|
# ── FINAL-OUTPUT RECALIBRATION LAYER (V31e) ─────────────────────────────────
|
||||||
|
# A thin, LAST-STEP per-market map: production calibrated_confidence -> reality.
|
||||||
|
# Built from a 60-day backtest (scripts/fit_recalibrators.py); inference is a
|
||||||
|
# pure np.interp over a 99-point monotone grid — NO sklearn needed at runtime.
|
||||||
|
#
|
||||||
|
# WHY THIS EXISTS:
|
||||||
|
# The upstream chain (temperature scaling T=1.5 -> per-outcome isotonic ->
|
||||||
|
# POST_CAL_TRUST blend) crushes high-base-rate binary markets toward 0.5,
|
||||||
|
# so "system says 51%" can really hit 70%. MS survives (near-uniform picks),
|
||||||
|
# which is why MS is already well-calibrated and OU/HT-OU markets are not.
|
||||||
|
#
|
||||||
|
# SAFETY / "DO NO HARM":
|
||||||
|
# * Only markets whose fit-time ECE >= 5.0 carry a map (currently OU15, OU35,
|
||||||
|
# HT_OU05, HT_OU15). MS and every already-good market have NO map ->
|
||||||
|
# recalibrate_conf() returns the input UNCHANGED -> guaranteed no regression.
|
||||||
|
# * Out-of-sample validated (fit=older 65%, test=unseen 35%):
|
||||||
|
# MS ECE 1.1 -> 1.3 (flat, safe)
|
||||||
|
# HT_OU15 29.2 -> 0.8
|
||||||
|
# OU15 19.0 -> 3.3
|
||||||
|
# OU35 13.9 -> 4.3
|
||||||
|
# HT_OU05 11.5 -> 2.4
|
||||||
|
# * Adjusts ONLY the displayed confidence number. All rich analysis payload
|
||||||
|
# (probabilities, edges, vetoes, tiers, bands) is preserved untouched, and
|
||||||
|
# the pre-recalibration value is kept for audit by the caller.
|
||||||
|
FINAL_RECALIBRATOR_PATH = os.path.join(CALIBRATION_DIR, "final_recalibrators.json")
|
||||||
|
|
||||||
|
|
||||||
|
class FinalRecalibrator:
|
||||||
|
"""Per-market final-output recalibration via piecewise-linear interpolation.
|
||||||
|
|
||||||
|
Loads a compact JSON of 99-point lookup grids (x=calibrated_confidence/100,
|
||||||
|
y=reality). Markets absent from the file pass through as identity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, path: str = FINAL_RECALIBRATOR_PATH):
|
||||||
|
self.grid: Optional[np.ndarray] = None
|
||||||
|
self.maps: Dict[str, np.ndarray] = {}
|
||||||
|
self.source_path = path
|
||||||
|
self._load(path)
|
||||||
|
|
||||||
|
def _load(self, path: str) -> None:
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print(f"[FinalRecalibrator] No map file at {path} — pass-through mode (all markets unchanged)")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
with open(path, "r") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
meta = data.get("_meta", {})
|
||||||
|
grid = meta.get("grid")
|
||||||
|
if not grid:
|
||||||
|
print("[FinalRecalibrator] Map file missing _meta.grid — pass-through mode")
|
||||||
|
return
|
||||||
|
self.grid = np.asarray(grid, dtype=float)
|
||||||
|
for market, m in data.items():
|
||||||
|
if market == "_meta" or not isinstance(m, dict):
|
||||||
|
continue
|
||||||
|
y = m.get("y")
|
||||||
|
if y and len(y) == len(self.grid):
|
||||||
|
self.maps[str(market).upper()] = np.asarray(y, dtype=float)
|
||||||
|
else:
|
||||||
|
print(f"[FinalRecalibrator] Skipped {market}: grid/y length mismatch")
|
||||||
|
print(f"[FinalRecalibrator] Loaded reality maps for {sorted(self.maps.keys())} "
|
||||||
|
f"(everything else, incl. MS, passes through unchanged)")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[FinalRecalibrator] Warning: failed to load {path}: {e} — pass-through mode")
|
||||||
|
self.grid = None
|
||||||
|
self.maps = {}
|
||||||
|
|
||||||
|
def has_map(self, market: str) -> bool:
|
||||||
|
return bool(self.maps) and (market or "").upper() in self.maps
|
||||||
|
|
||||||
|
def recalibrate_conf(self, market: str, calibrated_conf: float) -> float:
|
||||||
|
"""Map a 0–100 confidence to its reality-aligned value.
|
||||||
|
|
||||||
|
Markets without a trained map (including MS and all already-good
|
||||||
|
markets) return the input UNCHANGED. Any failure also returns the
|
||||||
|
input unchanged so this layer can never regress production.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
key = (market or "").upper()
|
||||||
|
if self.grid is None or key not in self.maps:
|
||||||
|
return calibrated_conf
|
||||||
|
x = float(calibrated_conf) / 100.0
|
||||||
|
x = min(max(x, 0.0), 1.0)
|
||||||
|
y = float(np.interp(x, self.grid, self.maps[key]))
|
||||||
|
return max(1.0, min(99.0, y * 100.0))
|
||||||
|
except Exception:
|
||||||
|
return calibrated_conf
|
||||||
|
|
||||||
|
|
||||||
|
# Singleton instance
|
||||||
|
_final_recalibrator_instance: Optional[FinalRecalibrator] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_final_recalibrator() -> FinalRecalibrator:
|
||||||
|
"""Get or create the global FinalRecalibrator instance."""
|
||||||
|
global _final_recalibrator_instance
|
||||||
|
if _final_recalibrator_instance is None:
|
||||||
|
_final_recalibrator_instance = FinalRecalibrator()
|
||||||
|
return _final_recalibrator_instance
|
||||||
|
|||||||
@@ -0,0 +1,532 @@
|
|||||||
|
{
|
||||||
|
"_meta": {
|
||||||
|
"grid": [
|
||||||
|
0.01,
|
||||||
|
0.02,
|
||||||
|
0.03,
|
||||||
|
0.04,
|
||||||
|
0.05,
|
||||||
|
0.06,
|
||||||
|
0.07,
|
||||||
|
0.08,
|
||||||
|
0.09,
|
||||||
|
0.1,
|
||||||
|
0.11,
|
||||||
|
0.12,
|
||||||
|
0.13,
|
||||||
|
0.14,
|
||||||
|
0.15,
|
||||||
|
0.16,
|
||||||
|
0.17,
|
||||||
|
0.18,
|
||||||
|
0.19,
|
||||||
|
0.2,
|
||||||
|
0.21,
|
||||||
|
0.22,
|
||||||
|
0.23,
|
||||||
|
0.24,
|
||||||
|
0.25,
|
||||||
|
0.26,
|
||||||
|
0.27,
|
||||||
|
0.28,
|
||||||
|
0.29,
|
||||||
|
0.3,
|
||||||
|
0.31,
|
||||||
|
0.32,
|
||||||
|
0.33,
|
||||||
|
0.34,
|
||||||
|
0.35,
|
||||||
|
0.36,
|
||||||
|
0.37,
|
||||||
|
0.38,
|
||||||
|
0.39,
|
||||||
|
0.4,
|
||||||
|
0.41,
|
||||||
|
0.42,
|
||||||
|
0.43,
|
||||||
|
0.44,
|
||||||
|
0.45,
|
||||||
|
0.46,
|
||||||
|
0.47,
|
||||||
|
0.48,
|
||||||
|
0.49,
|
||||||
|
0.5,
|
||||||
|
0.51,
|
||||||
|
0.52,
|
||||||
|
0.53,
|
||||||
|
0.54,
|
||||||
|
0.55,
|
||||||
|
0.56,
|
||||||
|
0.57,
|
||||||
|
0.58,
|
||||||
|
0.59,
|
||||||
|
0.6,
|
||||||
|
0.61,
|
||||||
|
0.62,
|
||||||
|
0.63,
|
||||||
|
0.64,
|
||||||
|
0.65,
|
||||||
|
0.66,
|
||||||
|
0.67,
|
||||||
|
0.68,
|
||||||
|
0.69,
|
||||||
|
0.7,
|
||||||
|
0.71,
|
||||||
|
0.72,
|
||||||
|
0.73,
|
||||||
|
0.74,
|
||||||
|
0.75,
|
||||||
|
0.76,
|
||||||
|
0.77,
|
||||||
|
0.78,
|
||||||
|
0.79,
|
||||||
|
0.8,
|
||||||
|
0.81,
|
||||||
|
0.82,
|
||||||
|
0.83,
|
||||||
|
0.84,
|
||||||
|
0.85,
|
||||||
|
0.86,
|
||||||
|
0.87,
|
||||||
|
0.88,
|
||||||
|
0.89,
|
||||||
|
0.9,
|
||||||
|
0.91,
|
||||||
|
0.92,
|
||||||
|
0.93,
|
||||||
|
0.94,
|
||||||
|
0.95,
|
||||||
|
0.96,
|
||||||
|
0.97,
|
||||||
|
0.98,
|
||||||
|
0.99
|
||||||
|
],
|
||||||
|
"threshold_ece": 5.0,
|
||||||
|
"source": "/tmp/multi_60d.csv",
|
||||||
|
"note": "x=calibrated_confidence/100; new=interp(grid,y)"
|
||||||
|
},
|
||||||
|
"HT_OU05": {
|
||||||
|
"grid_min": 0.01,
|
||||||
|
"grid_max": 0.99,
|
||||||
|
"n": 3683,
|
||||||
|
"y": [
|
||||||
|
0.0833,
|
||||||
|
0.3333,
|
||||||
|
0.3333,
|
||||||
|
0.3333,
|
||||||
|
0.3394,
|
||||||
|
0.3636,
|
||||||
|
0.3636,
|
||||||
|
0.3636,
|
||||||
|
0.3636,
|
||||||
|
0.3636,
|
||||||
|
0.3636,
|
||||||
|
0.3636,
|
||||||
|
0.3636,
|
||||||
|
0.3727,
|
||||||
|
0.3955,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4,
|
||||||
|
0.4583,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6286,
|
||||||
|
0.6531,
|
||||||
|
0.672,
|
||||||
|
0.7143,
|
||||||
|
0.7262,
|
||||||
|
0.7262,
|
||||||
|
0.7312,
|
||||||
|
0.7406,
|
||||||
|
0.7655,
|
||||||
|
0.7655,
|
||||||
|
0.8495,
|
||||||
|
0.8495,
|
||||||
|
0.8495,
|
||||||
|
0.8495,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"HT_OU15": {
|
||||||
|
"grid_min": 0.01,
|
||||||
|
"grid_max": 0.99,
|
||||||
|
"n": 5200,
|
||||||
|
"y": [
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4118,
|
||||||
|
0.4521,
|
||||||
|
0.5385,
|
||||||
|
0.5385,
|
||||||
|
0.5385,
|
||||||
|
0.5848,
|
||||||
|
0.6142,
|
||||||
|
0.6142,
|
||||||
|
0.6142,
|
||||||
|
0.6245,
|
||||||
|
0.6245,
|
||||||
|
0.6245,
|
||||||
|
0.6262,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6275,
|
||||||
|
0.6452,
|
||||||
|
0.6842,
|
||||||
|
0.6842,
|
||||||
|
0.6842,
|
||||||
|
0.6842,
|
||||||
|
0.6842,
|
||||||
|
0.6842,
|
||||||
|
0.8077,
|
||||||
|
0.8077,
|
||||||
|
0.8077,
|
||||||
|
0.8077,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OU15": {
|
||||||
|
"grid_min": 0.01,
|
||||||
|
"grid_max": 0.99,
|
||||||
|
"n": 2724,
|
||||||
|
"y": [
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.2797,
|
||||||
|
0.4352,
|
||||||
|
0.6295,
|
||||||
|
0.7165,
|
||||||
|
0.7174,
|
||||||
|
0.7987,
|
||||||
|
0.8197,
|
||||||
|
0.8197,
|
||||||
|
0.8197,
|
||||||
|
0.8197,
|
||||||
|
0.8197,
|
||||||
|
0.8197,
|
||||||
|
0.9118,
|
||||||
|
0.9276,
|
||||||
|
0.9502,
|
||||||
|
0.9729,
|
||||||
|
0.9955,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OU35": {
|
||||||
|
"grid_min": 0.01,
|
||||||
|
"grid_max": 0.99,
|
||||||
|
"n": 4277,
|
||||||
|
"y": [
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.474,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.571,
|
||||||
|
0.6222,
|
||||||
|
0.6222,
|
||||||
|
0.6222,
|
||||||
|
0.6222,
|
||||||
|
0.6222,
|
||||||
|
0.7747,
|
||||||
|
0.7747,
|
||||||
|
0.7747,
|
||||||
|
0.7747,
|
||||||
|
0.7747,
|
||||||
|
0.7788,
|
||||||
|
0.8195,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.8333,
|
||||||
|
0.836,
|
||||||
|
0.8624,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889,
|
||||||
|
0.8889
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
"""Live-conditioned score projection (V38) — pure functions, no I/O.
|
||||||
|
|
||||||
|
Answers, DURING a match, questions like "1-0 at 80' — what is the REAL
|
||||||
|
probability the away team still scores?" by conditioning the same calibrated
|
||||||
|
market-anchored lambdas (V35/V36) on the current score and minute.
|
||||||
|
|
||||||
|
Mechanics — a minute-stepped Markov chain over remaining goals:
|
||||||
|
|
||||||
|
1. Pre-match lambdas come from the SAME source the score card uses
|
||||||
|
(de-vigged 1X2 + over2.5, models/score_matrix solvers) — one consistent
|
||||||
|
probability spine pre-match and in-play.
|
||||||
|
2. Each remaining minute contributes lambda_side x minute_share(t) goals,
|
||||||
|
where minute_share is the EMPIRICAL goal-time intensity curve measured
|
||||||
|
on 38,779 clean-timeline real-odds matches (1H share 44.4%, late-game
|
||||||
|
intensity rises, stoppage spikes at 45' and 90+').
|
||||||
|
3. Each minute's intensity is scaled by the MEASURED score-state
|
||||||
|
multiplier: trailing teams push (+9%, +17% after 70'), leading teams
|
||||||
|
shut up shop (-5%/-7%), 2+ ahead opens up. The chain updates the state
|
||||||
|
as virtual goals happen, so multipliers switch mid-projection exactly
|
||||||
|
like they do on the pitch.
|
||||||
|
|
||||||
|
All constants are fitted on the train window (matches older than the last 90
|
||||||
|
days); the held-out window validates calibration out-of-sample before any of
|
||||||
|
this reaches the screen.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
from models.score_matrix import split_lambdas, total_lambda_from_over25
|
||||||
|
|
||||||
|
MAX_MINUTE = 94 # 90 + folded stoppage
|
||||||
|
LATE_PHASE_FROM = 70 # measured multipliers switch here
|
||||||
|
MAX_EXTRA_GOALS = 7 # per side, absorbing cap for the chain
|
||||||
|
|
||||||
|
# Empirical goal-time intensity: share of a match's goals per 5-min bucket
|
||||||
|
# (0-5, ..., 90-94+). Measured on 105k goals; 45' and 90+' buckets carry the
|
||||||
|
# folded stoppage-time spikes.
|
||||||
|
INTENSITY_SHARES: Tuple[float, ...] = (
|
||||||
|
0.036, 0.045, 0.047, 0.047, 0.045, 0.046, 0.048, 0.049, 0.081,
|
||||||
|
0.048, 0.057, 0.055, 0.054, 0.053, 0.052, 0.053, 0.052, 0.056, 0.076,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Score-state goal-intensity multipliers, measured (actual/expected) by the
|
||||||
|
# scoring side's goal difference, split early (<70') / late (>=70').
|
||||||
|
_STATE_MULT_EARLY: Dict[int, float] = {-2: 1.095, -1: 1.045, 0: 0.966, 1: 0.952, 2: 1.011}
|
||||||
|
_STATE_MULT_LATE: Dict[int, float] = {-2: 1.123, -1: 1.174, 0: 1.015, 1: 0.930, 2: 1.011}
|
||||||
|
|
||||||
|
|
||||||
|
def _minute_share(minute: int) -> float:
|
||||||
|
"""Per-minute share of match-total goal intensity at `minute` (1-based)."""
|
||||||
|
b = min(len(INTENSITY_SHARES) - 1, max(0, (minute - 1) // 5))
|
||||||
|
return INTENSITY_SHARES[b] / 5.0
|
||||||
|
|
||||||
|
|
||||||
|
def state_multiplier(diff: int, minute: int) -> float:
|
||||||
|
"""Intensity multiplier for a side whose current goal difference is
|
||||||
|
`diff` (own − opponent), at `minute`."""
|
||||||
|
d = max(-2, min(2, diff))
|
||||||
|
table = _STATE_MULT_LATE if minute >= LATE_PHASE_FROM else _STATE_MULT_EARLY
|
||||||
|
return table[d]
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_minute(match_date_ms: Optional[int], now_ms: int) -> Optional[int]:
|
||||||
|
"""Approximate current match minute from kickoff time (no feed minute is
|
||||||
|
available: live_matches.substate carries none). Folds the ~15' half-time
|
||||||
|
break; accuracy is ±2-3 minutes which barely moves the projection."""
|
||||||
|
if not match_date_ms:
|
||||||
|
return None
|
||||||
|
elapsed = (now_ms - int(match_date_ms)) / 60000.0
|
||||||
|
if elapsed < 0:
|
||||||
|
return None
|
||||||
|
if elapsed <= 48: # first half (+stoppage)
|
||||||
|
minute = elapsed
|
||||||
|
elif elapsed <= 63: # half-time break window
|
||||||
|
minute = 46
|
||||||
|
else:
|
||||||
|
minute = elapsed - 15.0 # second half, break folded out
|
||||||
|
return int(max(1, min(MAX_MINUTE, minute)))
|
||||||
|
|
||||||
|
|
||||||
|
def _chain(
|
||||||
|
lam_h: float,
|
||||||
|
lam_a: float,
|
||||||
|
cur_h: int,
|
||||||
|
cur_a: int,
|
||||||
|
minute: int,
|
||||||
|
) -> Dict[Tuple[int, int], float]:
|
||||||
|
"""Distribution over (extra home goals, extra away goals) from `minute`
|
||||||
|
to full time, with state-dependent intensities."""
|
||||||
|
dist: Dict[Tuple[int, int], float] = {(0, 0): 1.0}
|
||||||
|
for t in range(minute, MAX_MINUTE + 1):
|
||||||
|
share = _minute_share(t)
|
||||||
|
nxt: Dict[Tuple[int, int], float] = {}
|
||||||
|
for (eh, ea), p in dist.items():
|
||||||
|
diff = (cur_h + eh) - (cur_a + ea)
|
||||||
|
ph = lam_h * share * state_multiplier(diff, t)
|
||||||
|
pa = lam_a * share * state_multiplier(-diff, t)
|
||||||
|
ph = min(ph, 0.30); pa = min(pa, 0.30)
|
||||||
|
stay = max(0.0, 1.0 - ph - pa)
|
||||||
|
nxt[(eh, ea)] = nxt.get((eh, ea), 0.0) + p * stay
|
||||||
|
if eh < MAX_EXTRA_GOALS:
|
||||||
|
nxt[(eh + 1, ea)] = nxt.get((eh + 1, ea), 0.0) + p * ph
|
||||||
|
else:
|
||||||
|
nxt[(eh, ea)] = nxt.get((eh, ea), 0.0) + p * ph
|
||||||
|
if ea < MAX_EXTRA_GOALS:
|
||||||
|
nxt[(eh, ea + 1)] = nxt.get((eh, ea + 1), 0.0) + p * pa
|
||||||
|
else:
|
||||||
|
nxt[(eh, ea)] = nxt.get((eh, ea), 0.0) + p * pa
|
||||||
|
dist = nxt
|
||||||
|
return dist
|
||||||
|
|
||||||
|
|
||||||
|
def build_live_projection(
|
||||||
|
p1: float,
|
||||||
|
px: float,
|
||||||
|
p2: float,
|
||||||
|
p_over25: float,
|
||||||
|
cur_h: int,
|
||||||
|
cur_a: int,
|
||||||
|
minute: int,
|
||||||
|
) -> Dict[str, object]:
|
||||||
|
"""Live projection from the anchored pre-match probabilities + the pitch
|
||||||
|
state. Returns honest, score/minute-aware probabilities.
|
||||||
|
|
||||||
|
(p1, px, p2) and p_over25 are the CALIBRATED (V35-anchored) numbers; the
|
||||||
|
same spine the pre-match cards display.
|
||||||
|
"""
|
||||||
|
minute = int(max(1, min(MAX_MINUTE, minute)))
|
||||||
|
cur_h = max(0, int(cur_h)); cur_a = max(0, int(cur_a))
|
||||||
|
total = total_lambda_from_over25(p_over25)
|
||||||
|
lam_h, lam_a = split_lambdas(total, p1, p2)
|
||||||
|
|
||||||
|
dist = _chain(lam_h, lam_a, cur_h, cur_a, minute)
|
||||||
|
|
||||||
|
p_home_win = p_draw = p_away_win = 0.0
|
||||||
|
p_home_scores = p_away_scores = 0.0
|
||||||
|
exp_goals = 0.0
|
||||||
|
scores: Dict[str, float] = {}
|
||||||
|
for (eh, ea), p in dist.items():
|
||||||
|
fh, fa = cur_h + eh, cur_a + ea
|
||||||
|
if fh > fa: p_home_win += p
|
||||||
|
elif fh == fa: p_draw += p
|
||||||
|
else: p_away_win += p
|
||||||
|
if eh > 0: p_home_scores += p
|
||||||
|
if ea > 0: p_away_scores += p
|
||||||
|
exp_goals += p * (eh + ea)
|
||||||
|
key = f"{min(fh,9)}-{min(fa,9)}"
|
||||||
|
scores[key] = scores.get(key, 0.0) + p
|
||||||
|
|
||||||
|
top = sorted(scores.items(), key=lambda kv: kv[1], reverse=True)[:5]
|
||||||
|
total_now = cur_h + cur_a
|
||||||
|
p_over25_live = sum(
|
||||||
|
p for (eh, ea), p in dist.items() if total_now + eh + ea >= 3
|
||||||
|
)
|
||||||
|
|
||||||
|
# "comeback": the side currently behind at least draws / currently level
|
||||||
|
# match does NOT stay level
|
||||||
|
if cur_h > cur_a:
|
||||||
|
p_comeback = p_draw + p_away_win
|
||||||
|
elif cur_a > cur_h:
|
||||||
|
p_comeback = p_draw + p_home_win
|
||||||
|
else:
|
||||||
|
p_comeback = p_home_win + p_away_win # deadlock breaks
|
||||||
|
|
||||||
|
return {
|
||||||
|
"minute": minute,
|
||||||
|
"current_score": f"{cur_h}-{cur_a}",
|
||||||
|
"probs": {
|
||||||
|
"1": round(p_home_win, 4),
|
||||||
|
"X": round(p_draw, 4),
|
||||||
|
"2": round(p_away_win, 4),
|
||||||
|
},
|
||||||
|
"p_home_scores_again": round(p_home_scores, 4),
|
||||||
|
"p_away_scores_again": round(p_away_scores, 4),
|
||||||
|
"p_comeback": round(p_comeback, 4),
|
||||||
|
"p_over25": round(p_over25_live, 4),
|
||||||
|
"expected_remaining_goals": round(exp_goals, 2),
|
||||||
|
"scenario_top5": [
|
||||||
|
{"score": s, "prob": round(p, 4)} for s, p in top
|
||||||
|
],
|
||||||
|
"calibration_source": "live_matrix_v38",
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
"""Market-anchored calibration (V35) — pure functions, no I/O.
|
||||||
|
|
||||||
|
WHY THIS EXISTS
|
||||||
|
---------------
|
||||||
|
The model's invented per-market probabilities were *measured* to be badly
|
||||||
|
overconfident. Grading the engine's own stored predictions against actual
|
||||||
|
results: it says ~50% where reality is ~25%, ~67% where reality is ~37%
|
||||||
|
(calibration error / ECE on the order of 25-30%). That mis-calibration is the
|
||||||
|
direct cause of the false "value" picks and the negative realised ROI.
|
||||||
|
|
||||||
|
The de-vigged market price, by contrast, is empirically near-perfectly
|
||||||
|
calibrated. Out-of-sample (correction fit on 2023-24, tested on 2025-26;
|
||||||
|
78k real-odds football matches) the de-vigged market's ECE was:
|
||||||
|
home 1.56% | draw 1.85% | away 1.49% | over2.5 1.79% | btts 1.38%
|
||||||
|
Adding one small, large-sample home-favourite correction cut MS-home ECE
|
||||||
|
from 1.56% -> 0.64%.
|
||||||
|
|
||||||
|
So for the DISPLAYED probabilities we anchor to the de-vigged market and apply
|
||||||
|
only that one proven correction. ~20-40x more calibrated than the model's
|
||||||
|
numbers, and fully transparent.
|
||||||
|
|
||||||
|
These functions are pure (stdlib only) so they can be unit-tested in isolation
|
||||||
|
without the DB or the heavy model stack.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
def devig(odds: List[Optional[float]]) -> Optional[List[float]]:
|
||||||
|
"""Vig-removed (fair) probabilities from a group of decimal odds.
|
||||||
|
|
||||||
|
``p_i = (1/odds_i) / Σ(1/odds_j)`` — normalising the raw implied
|
||||||
|
probabilities to sum to 1 removes the bookmaker margin.
|
||||||
|
|
||||||
|
Returns ``None`` when ANY leg is missing or non-real (``<= 1.01``). That is
|
||||||
|
deliberate: a market with a missing/placeholder leg has no real price, and
|
||||||
|
the product rule is to never fabricate numbers for a match without odds.
|
||||||
|
"""
|
||||||
|
if not odds or any(o is None or float(o) <= 1.01 for o in odds):
|
||||||
|
return None
|
||||||
|
inv = [1.0 / float(o) for o in odds]
|
||||||
|
total = sum(inv)
|
||||||
|
if total <= 0.0:
|
||||||
|
return None
|
||||||
|
return [x / total for x in inv]
|
||||||
|
|
||||||
|
|
||||||
|
# Home-favourite correction: measured (actual home-win rate − de-vigged implied)
|
||||||
|
# by implied-home band, out-of-sample on real-odds matches. Big home favourites
|
||||||
|
# win a few points MORE than the de-vigged price implies; underdogs are roughly
|
||||||
|
# unbiased. Values are deliberately conservative — universal and shrunk toward 0
|
||||||
|
# vs the raw tier-0 (soft-league) edge, because the bias is weaker in efficient
|
||||||
|
# top leagues. Applying these took MS-home OOS ECE 1.56% -> 0.64%.
|
||||||
|
#
|
||||||
|
# These static bands are the BUILT-IN FALLBACK. The live values come from the
|
||||||
|
# versioned artifact `config/market_anchor_corrections.json`, refreshed by
|
||||||
|
# `scripts/fit_anchor_corrections.py` (the guarded self-correction loop:
|
||||||
|
# measure on settled matches -> shrink/clip/min-sample gates -> out-of-sample
|
||||||
|
# acceptance -> write table). The engine only ever consumes the TABLE — the
|
||||||
|
# loop never modifies code.
|
||||||
|
_HOME_FAV_BANDS: Tuple[Tuple[float, float, float], ...] = (
|
||||||
|
(0.45, 0.55, 0.010),
|
||||||
|
(0.55, 0.65, 0.018),
|
||||||
|
(0.65, 0.75, 0.028),
|
||||||
|
(0.75, 1.01, 0.034),
|
||||||
|
)
|
||||||
|
|
||||||
|
_DEFAULT_CORRECTIONS_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)), "..", "config",
|
||||||
|
"market_anchor_corrections.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _corrections_path() -> str:
|
||||||
|
return os.environ.get(
|
||||||
|
"MARKET_ANCHOR_CORRECTIONS_PATH", _DEFAULT_CORRECTIONS_PATH
|
||||||
|
)
|
||||||
|
_corrections_lock = threading.Lock()
|
||||||
|
_corrections_cache: Optional[Dict[str, Any]] = None
|
||||||
|
_corrections_ts: float = 0.0
|
||||||
|
# Re-check sources at most every 10 minutes: the self-correction cron writes a
|
||||||
|
# new table to app_settings; running engines pick it up WITHOUT a restart.
|
||||||
|
_CORRECTIONS_TTL_S = 600.0
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_corrections(raw: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
parsed_table: Dict[str, Any] = {}
|
||||||
|
for key in ("ms_home", "ms_away"):
|
||||||
|
bands = raw.get("corrections", {}).get(key)
|
||||||
|
if not (isinstance(bands, list) and bands):
|
||||||
|
continue
|
||||||
|
parsed = []
|
||||||
|
for b in bands:
|
||||||
|
lo = float(b["lo"]); hi = float(b["hi"]); delta = float(b["delta"])
|
||||||
|
if not (0.0 <= lo < hi <= 1.01) or abs(delta) > 0.10:
|
||||||
|
raise ValueError(f"correction band out of range: {b}")
|
||||||
|
parsed.append((lo, hi, delta))
|
||||||
|
parsed_table[key] = tuple(parsed)
|
||||||
|
if not parsed_table:
|
||||||
|
return None
|
||||||
|
parsed_table["version"] = str(raw.get("version", "?"))
|
||||||
|
return parsed_table
|
||||||
|
|
||||||
|
|
||||||
|
def _db_corrections_raw() -> Optional[Dict[str, Any]]:
|
||||||
|
"""Fetch the correction artifact from app_settings (the deployment's shared
|
||||||
|
medium — the ai-engine container has no volume mounts, so a host-side cron
|
||||||
|
can only reach the running engine through the database). Guarded: any
|
||||||
|
failure → None, never breaks a prediction. Disable with MARKET_ANCHOR_DB=0."""
|
||||||
|
if os.environ.get("MARKET_ANCHOR_DB", "1") == "0":
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
import psycopg2 # local import: keeps module usable without DB deps
|
||||||
|
from data.db import get_clean_dsn
|
||||||
|
|
||||||
|
with psycopg2.connect(get_clean_dsn(), connect_timeout=3) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"SELECT value FROM app_settings"
|
||||||
|
" WHERE key = 'market_anchor_corrections'"
|
||||||
|
)
|
||||||
|
row = cur.fetchone()
|
||||||
|
if row and row[0]:
|
||||||
|
return json.loads(row[0])
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _load_corrections() -> Optional[Dict[str, Any]]:
|
||||||
|
"""Resolve the active correction table (thread-safe, TTL-cached).
|
||||||
|
|
||||||
|
Source order:
|
||||||
|
1. MARKET_ANCHOR_CORRECTIONS_PATH env file (tests/dev — file-only mode,
|
||||||
|
malformed → static fallback, DB and default file are NOT consulted)
|
||||||
|
2. app_settings DB row 'market_anchor_corrections' (production path —
|
||||||
|
refreshed by scripts/fit_anchor_corrections.py)
|
||||||
|
3. bundled config/market_anchor_corrections.json
|
||||||
|
4. None → built-in static fallback bands
|
||||||
|
"""
|
||||||
|
global _corrections_cache, _corrections_ts
|
||||||
|
now = time.time()
|
||||||
|
if now - _corrections_ts < _CORRECTIONS_TTL_S:
|
||||||
|
return _corrections_cache
|
||||||
|
with _corrections_lock:
|
||||||
|
if now - _corrections_ts < _CORRECTIONS_TTL_S:
|
||||||
|
return _corrections_cache
|
||||||
|
table: Optional[Dict[str, Any]] = None
|
||||||
|
env_path = os.environ.get("MARKET_ANCHOR_CORRECTIONS_PATH")
|
||||||
|
if env_path:
|
||||||
|
try:
|
||||||
|
with open(env_path, "r", encoding="utf-8") as fh:
|
||||||
|
table = _parse_corrections(json.load(fh))
|
||||||
|
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
|
||||||
|
table = None
|
||||||
|
else:
|
||||||
|
raw = _db_corrections_raw()
|
||||||
|
if raw is not None:
|
||||||
|
try:
|
||||||
|
table = _parse_corrections(raw)
|
||||||
|
except (ValueError, KeyError, TypeError):
|
||||||
|
table = None
|
||||||
|
if table is None:
|
||||||
|
try:
|
||||||
|
with open(_corrections_path(), "r", encoding="utf-8") as fh:
|
||||||
|
table = _parse_corrections(json.load(fh))
|
||||||
|
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
|
||||||
|
table = None
|
||||||
|
_corrections_cache = table
|
||||||
|
_corrections_ts = time.time()
|
||||||
|
return _corrections_cache
|
||||||
|
|
||||||
|
|
||||||
|
def reload_corrections() -> None:
|
||||||
|
"""Force re-read of the correction sources (used after a refresh/tests)."""
|
||||||
|
global _corrections_ts, _corrections_cache
|
||||||
|
with _corrections_lock:
|
||||||
|
_corrections_ts = 0.0
|
||||||
|
_corrections_cache = None
|
||||||
|
|
||||||
|
|
||||||
|
def home_favorite_delta(p_home: float) -> float:
|
||||||
|
"""Additive correction to the de-vigged home-win probability.
|
||||||
|
|
||||||
|
Band semantics: a fitted-artifact band OVERRIDES the static prior where it
|
||||||
|
exists (including an explicit delta of 0 — evidence of "no bias"). Where
|
||||||
|
the artifact is SILENT (a range that never passed the min-sample gate,
|
||||||
|
e.g. big favourites 0.75+), the static prior still applies — missing
|
||||||
|
evidence must not silently erase proven knowledge."""
|
||||||
|
table = _load_corrections()
|
||||||
|
if table and "ms_home" in table:
|
||||||
|
for lo, hi, delta in table["ms_home"]:
|
||||||
|
if lo <= p_home < hi:
|
||||||
|
return delta
|
||||||
|
for lo, hi, delta in _HOME_FAV_BANDS:
|
||||||
|
if lo <= p_home < hi:
|
||||||
|
return delta
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def away_favorite_delta(p_away: float) -> float:
|
||||||
|
"""Additive correction to the de-vigged away-win probability.
|
||||||
|
|
||||||
|
Scoreboard measurement (2026-06): away favourites also win a few points
|
||||||
|
MORE than the de-vigged price implies (+2.6..+4.2pt). Unlike the home
|
||||||
|
side there is NO built-in fallback — away corrections must be EARNED via
|
||||||
|
the fitted artifact (scripts/fit_anchor_corrections.py passing its
|
||||||
|
out-of-sample acceptance gate). No artifact → zero → prior behaviour."""
|
||||||
|
table = _load_corrections()
|
||||||
|
bands = table.get("ms_away", ()) if table else ()
|
||||||
|
for lo, hi, delta in bands:
|
||||||
|
if lo <= p_away < hi:
|
||||||
|
return delta
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def apply_corrections(
|
||||||
|
p1: float, px: float, p2: float
|
||||||
|
) -> Tuple[float, float, float]:
|
||||||
|
"""Apply favourite corrections to a 3-way (1, X, 2) vector.
|
||||||
|
|
||||||
|
In practice only one side can be a favourite (both ≥0.45 would leave no
|
||||||
|
room for the draw); if both bands somehow fire, the larger delta wins.
|
||||||
|
The other two outcomes are renormalised so the vector still sums to 1."""
|
||||||
|
d1 = home_favorite_delta(p1)
|
||||||
|
d2 = away_favorite_delta(p2)
|
||||||
|
if d1 <= 0.0 and d2 <= 0.0:
|
||||||
|
return p1, px, p2
|
||||||
|
if d1 >= d2:
|
||||||
|
return apply_home_correction(p1, px, p2)
|
||||||
|
p2n = min(0.98, p2 + d2)
|
||||||
|
remaining = 1.0 - p2n
|
||||||
|
rest = p1 + px
|
||||||
|
if rest <= 0.0:
|
||||||
|
return p1, px, p2n
|
||||||
|
return p1 / rest * remaining, px / rest * remaining, p2n
|
||||||
|
|
||||||
|
|
||||||
|
def apply_home_correction(
|
||||||
|
p1: float, px: float, p2: float
|
||||||
|
) -> Tuple[float, float, float]:
|
||||||
|
"""Apply the home-favourite delta to a 3-way (1, X, 2) probability vector,
|
||||||
|
renormalising draw/away so the three still sum to 1.0."""
|
||||||
|
delta = home_favorite_delta(p1)
|
||||||
|
if delta <= 0.0:
|
||||||
|
return p1, px, p2
|
||||||
|
p1n = min(0.98, p1 + delta)
|
||||||
|
remaining = 1.0 - p1n
|
||||||
|
rest = px + p2
|
||||||
|
if rest <= 0.0:
|
||||||
|
return p1n, px, p2
|
||||||
|
return p1n, px / rest * remaining, p2 / rest * remaining
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
"""Market-anchored score matrix (V36) — pure functions, no I/O.
|
||||||
|
|
||||||
|
WHY THIS EXISTS
|
||||||
|
---------------
|
||||||
|
The engine's displayed score predictions (`score_prediction`, `scenario_top5`)
|
||||||
|
come from the model's invented xG, so they can contradict the calibrated
|
||||||
|
market-anchored probabilities shown right next to them (V35). Example seen in
|
||||||
|
production: MS card says home 78% while the score card's distribution implies
|
||||||
|
something else entirely.
|
||||||
|
|
||||||
|
This module derives the FULL scoreline distribution from the SAME calibrated
|
||||||
|
(de-vigged) market probabilities that the V35 market anchor displays:
|
||||||
|
|
||||||
|
1. Solve total-goals lambda T from the calibrated P(over 2.5)
|
||||||
|
(total goals ~ Poisson(T): P(N>=3) = 1 - e^-T (1 + T + T^2/2)).
|
||||||
|
2. Split T into (lambda_home, lambda_away) so the independent-Poisson
|
||||||
|
matrix's home/away win gap matches the calibrated 1X2.
|
||||||
|
3. Build the score matrix, then IPF-scale the three outcome regions
|
||||||
|
(home-win cells, draw cells, away-win cells) so they sum EXACTLY to the
|
||||||
|
calibrated (p1, px, pX2) — guaranteeing the score card and the MS card
|
||||||
|
can never disagree again.
|
||||||
|
4. Half-time matrix: same machinery with lambdas scaled by the measured
|
||||||
|
first-half goal share, optionally IPF'd to the anchored HT 1X2.
|
||||||
|
|
||||||
|
All stdlib (math only) → unit-testable in isolation, no model/DB deps.
|
||||||
|
|
||||||
|
Validated on 63,681 real-odds matches (2025-26, out-of-sample constants):
|
||||||
|
see tests + the calibration session notes. Honest ceiling reminder: even a
|
||||||
|
perfect correct-score predictor only hits the modal score ~12-15% of the time;
|
||||||
|
the value here is honest, consistent probabilities — not certainty.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# Measured on 63,681 real-odds matches (2025-26): share of full-time goals
|
||||||
|
# scored in the first half, per side (home 0.4440, away 0.4428).
|
||||||
|
HT_GOAL_SHARE_HOME = 0.44
|
||||||
|
HT_GOAL_SHARE_AWAY = 0.44
|
||||||
|
|
||||||
|
MAX_GOALS = 10 # matrix is (0..10)x(0..10); tail mass beyond is negligible
|
||||||
|
|
||||||
|
|
||||||
|
def _pois_pmf(lam: float, k: int) -> float:
|
||||||
|
return math.exp(-lam) * lam**k / math.factorial(k)
|
||||||
|
|
||||||
|
|
||||||
|
def total_lambda_from_over25(p_over25: float) -> float:
|
||||||
|
"""Solve T such that P(Poisson(T) >= 3) == p_over25, by bisection."""
|
||||||
|
p = min(max(p_over25, 0.01), 0.99)
|
||||||
|
|
||||||
|
def p_over(t: float) -> float:
|
||||||
|
return 1.0 - math.exp(-t) * (1.0 + t + t * t / 2.0)
|
||||||
|
|
||||||
|
lo, hi = 0.05, 8.0
|
||||||
|
for _ in range(60):
|
||||||
|
mid = (lo + hi) / 2.0
|
||||||
|
if p_over(mid) < p:
|
||||||
|
lo = mid
|
||||||
|
else:
|
||||||
|
hi = mid
|
||||||
|
return (lo + hi) / 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def _raw_matrix(lh: float, la: float) -> List[List[float]]:
|
||||||
|
ph = [_pois_pmf(lh, i) for i in range(MAX_GOALS + 1)]
|
||||||
|
pa = [_pois_pmf(la, j) for j in range(MAX_GOALS + 1)]
|
||||||
|
return [[ph[i] * pa[j] for j in range(MAX_GOALS + 1)] for i in range(MAX_GOALS + 1)]
|
||||||
|
|
||||||
|
|
||||||
|
def _outcome_sums(mat: List[List[float]]) -> Tuple[float, float, float]:
|
||||||
|
w = d = l = 0.0
|
||||||
|
for i in range(MAX_GOALS + 1):
|
||||||
|
for j in range(MAX_GOALS + 1):
|
||||||
|
if i > j:
|
||||||
|
w += mat[i][j]
|
||||||
|
elif i == j:
|
||||||
|
d += mat[i][j]
|
||||||
|
else:
|
||||||
|
l += mat[i][j]
|
||||||
|
return w, d, l
|
||||||
|
|
||||||
|
|
||||||
|
def split_lambdas(total: float, p1: float, p2: float) -> Tuple[float, float]:
|
||||||
|
"""Split total lambda into (home, away) so the matrix's win-prob gap
|
||||||
|
matches the calibrated 1X2 gap, by bisection on the home share."""
|
||||||
|
target_gap = p1 - p2
|
||||||
|
lo, hi = 0.10, 0.90
|
||||||
|
for _ in range(40):
|
||||||
|
s = (lo + hi) / 2.0
|
||||||
|
w, _, l = _outcome_sums(_raw_matrix(total * s, total * (1.0 - s)))
|
||||||
|
if (w - l) < target_gap:
|
||||||
|
lo = s
|
||||||
|
else:
|
||||||
|
hi = s
|
||||||
|
s = (lo + hi) / 2.0
|
||||||
|
return total * s, total * (1.0 - s)
|
||||||
|
|
||||||
|
|
||||||
|
def ipf_to_outcomes(
|
||||||
|
mat: List[List[float]], p1: float, px: float, p2: float
|
||||||
|
) -> List[List[float]]:
|
||||||
|
"""Scale the home-win / draw / away-win regions so each sums EXACTLY to the
|
||||||
|
calibrated (p1, px, p2). This is what makes the score card mathematically
|
||||||
|
consistent with the displayed MS probabilities."""
|
||||||
|
w, d, l = _outcome_sums(mat)
|
||||||
|
if min(w, d, l) <= 0.0:
|
||||||
|
return mat
|
||||||
|
fw, fd, fl = p1 / w, px / d, p2 / l
|
||||||
|
out = [[0.0] * (MAX_GOALS + 1) for _ in range(MAX_GOALS + 1)]
|
||||||
|
for i in range(MAX_GOALS + 1):
|
||||||
|
for j in range(MAX_GOALS + 1):
|
||||||
|
f = fw if i > j else fd if i == j else fl
|
||||||
|
out[i][j] = mat[i][j] * f
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def top_scores(mat: List[List[float]], n: int = 5) -> List[Dict[str, object]]:
|
||||||
|
cells = [
|
||||||
|
(mat[i][j], i, j)
|
||||||
|
for i in range(MAX_GOALS + 1)
|
||||||
|
for j in range(MAX_GOALS + 1)
|
||||||
|
]
|
||||||
|
cells.sort(reverse=True)
|
||||||
|
return [
|
||||||
|
{"score": f"{i}-{j}", "prob": round(p, 4)}
|
||||||
|
for p, i, j in cells[:n]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def build_calibrated_score_package(
|
||||||
|
p1: float,
|
||||||
|
px: float,
|
||||||
|
p2: float,
|
||||||
|
p_over25: float,
|
||||||
|
ht_probs: Optional[Tuple[float, float, float]] = None,
|
||||||
|
) -> Dict[str, object]:
|
||||||
|
"""Full calibrated score card from the V35-anchored probabilities.
|
||||||
|
|
||||||
|
Returns {ft, ht, xg_home, xg_away, xg_total, scenario_top5, ht_top}.
|
||||||
|
xg_* here are MARKET-implied goal expectations (the lambdas), so every
|
||||||
|
number on the card comes from one consistent source.
|
||||||
|
"""
|
||||||
|
total = total_lambda_from_over25(p_over25)
|
||||||
|
lh, la = split_lambdas(total, p1, p2)
|
||||||
|
ft_mat = ipf_to_outcomes(_raw_matrix(lh, la), p1, px, p2)
|
||||||
|
ft_top = top_scores(ft_mat, 5)
|
||||||
|
|
||||||
|
lh_ht, la_ht = lh * HT_GOAL_SHARE_HOME, la * HT_GOAL_SHARE_AWAY
|
||||||
|
ht_mat = _raw_matrix(lh_ht, la_ht)
|
||||||
|
if ht_probs is not None:
|
||||||
|
ht_mat = ipf_to_outcomes(ht_mat, *ht_probs)
|
||||||
|
ht_top = top_scores(ht_mat, 3)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"ft": str(ft_top[0]["score"]) if ft_top else None,
|
||||||
|
"ht": str(ht_top[0]["score"]) if ht_top else None,
|
||||||
|
"xg_home": round(lh, 2),
|
||||||
|
"xg_away": round(la, 2),
|
||||||
|
"xg_total": round(lh + la, 2),
|
||||||
|
"scenario_top5": ft_top,
|
||||||
|
"ht_top": ht_top,
|
||||||
|
"calibration_source": "market_anchor_v36_score",
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# V29 Data-Driven Optimization Report
|
||||||
|
## Based on 7,000-match Diagnostic Backtest (2026-05-27)
|
||||||
|
|
||||||
|
### Before (V28-Pro-Max)
|
||||||
|
- **4,134 settled BET-action picks**
|
||||||
|
- Hit rate: 54.9%
|
||||||
|
- Unit profit: -132.68
|
||||||
|
- Staked: 849.50
|
||||||
|
- **ROI: -15.6%**
|
||||||
|
|
||||||
|
### Root Cause Analysis
|
||||||
|
|
||||||
|
#### 1. Value Sniper Threshold Too Loose (CRITICAL)
|
||||||
|
```python
|
||||||
|
# OLD: ev_edge >= 0.008 or calibrated_conf >= 55.0
|
||||||
|
# This made 100% of bets qualify as "value sniper", bypassing ALL betting brain vetoes
|
||||||
|
```
|
||||||
|
- 4,134/4,134 bets (100%) had `is_value_sniper = True`
|
||||||
|
- Hard vetoes (negative_ev, market_muted, low_reliability) were NEVER enforced
|
||||||
|
|
||||||
|
#### 2. 89% of Bets Had Negative EV Edge
|
||||||
|
- n=3,688 with ev_edge < 0: ROI = -16.1%
|
||||||
|
- The model was systematically pricing below market, meaning every bet carried negative expected value
|
||||||
|
|
||||||
|
#### 3. OU25 Market Unprofitable in ALL Configurations
|
||||||
|
- n=1,563 bets, -17.1% ROI
|
||||||
|
- Even with ev>=5% + rel>=0.55: n=27, -36.9% ROI
|
||||||
|
- Grid search found NO profitable filter combination
|
||||||
|
|
||||||
|
#### 4. BTTS Market Marginal
|
||||||
|
- n=1,456 bets, -15.4% ROI
|
||||||
|
- Only profitable with ev>=5%: n=15, +12.9% (but tiny sample)
|
||||||
|
|
||||||
|
### Grid Search Results (Top Profitable Combos)
|
||||||
|
|
||||||
|
| Market | EV Min | Rel Min | V27 | n | Hit% | ROI |
|
||||||
|
|--------|--------|---------|-----|---|------|-----|
|
||||||
|
| MS | >=5% | >=0.55 | AGREE | 42 | 59.5% | **+10.4%** |
|
||||||
|
| MS | >=5% | >=0.55 | ANY | 52 | 59.6% | **+8.6%** |
|
||||||
|
| MS | >=3% | >=0.55 | ANY | 69 | 56.5% | **+4.0%** |
|
||||||
|
| BTTS | >=5% | >=0.70 | ANY | 15 | 60.0% | **+12.9%** |
|
||||||
|
| MS | >=5% | >=0.00 | ANY | 113 | 55.8% | **-0.7%** |
|
||||||
|
|
||||||
|
### Changes Applied (V29)
|
||||||
|
|
||||||
|
#### market_board.py
|
||||||
|
```python
|
||||||
|
# Tightened from: ev >= 0.008 OR conf >= 55.0
|
||||||
|
# To: ALL three must be true
|
||||||
|
is_value_sniper = ev_edge >= 0.05 and calibrated_conf >= 60.0 and odds_rel >= 0.55
|
||||||
|
```
|
||||||
|
|
||||||
|
#### betting_brain.py
|
||||||
|
1. **MIN_BET_SCORE**: 72.0 -> 62.0 (hard vetoes now do the filtering)
|
||||||
|
2. **MIN_WATCH_SCORE**: 62.0 -> 52.0
|
||||||
|
3. **MUTED_MARKETS**: `{"BTTS"}` -> `{"OU25", "DC", "OU35"}`
|
||||||
|
4. **MARKET_OPTIMAL_FILTERS**:
|
||||||
|
- MS: min_edge=0.03, min_reliability=0.55, require_v27_agree=False
|
||||||
|
- BTTS: min_edge=0.05, min_reliability=0.70 (strict envelope)
|
||||||
|
5. **Hard vetoes no longer bypassed by sniper**:
|
||||||
|
- `negative_ev_edge` (ev < 0)
|
||||||
|
- `ev_edge_too_high_trap` (ev >= 0.20)
|
||||||
|
- `market_muted_by_backtest`
|
||||||
|
- `low_reliability_league_hard_block` (rel < 0.30)
|
||||||
|
- Per-market envelope checks
|
||||||
|
|
||||||
|
### Expected Performance (Simulated on 7K backtest)
|
||||||
|
- **65 bets** out of 7,000 matches (0.9% selectivity)
|
||||||
|
- Hit rate: 56.9%
|
||||||
|
- **ROI: +6.8%** (from -15.6%)
|
||||||
|
- MS dominates: n=64, ROI=+8.0%
|
||||||
|
- Consistent: April +14.0%, May +4.9%
|
||||||
|
|
||||||
|
### Trade-off
|
||||||
|
The system becomes very selective (fewer bets per day) but each bet carries genuine positive expected value. Quality over quantity.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
match_id,match_date,league_id,score_home,score_away,ht_score_home,ht_score_away,market,pick,odds,stake_units,playable,won,unit_profit,raw_confidence,calibrated_confidence,play_score,ev_edge,bet_grade,is_value_sniper,bb_score,bb_action,bb_vetoes,bb_issues,bb_positives,bb_model_prob,bb_implied_prob,bb_model_market_gap,bb_divergence,bb_trap_market,v27_consensus,data_quality_score,data_quality_flags,risk_level,odds_reliability,htft_reversal_prob,htft_top_pick,league_name,is_cup,model_version,decision_reason
|
||||||
|
5iam9c9dw3ggz3y1ohr9uh53o,2026-05-24,8nbwkj392b0xzssqpw9jwmzdn,0,0,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
8c90p2ft4zxjdck8wlgq1a61g,2026-05-24,a9vrdkelbgif0gtu3wxsr75xo,2,2,0,0,OU25,Üst,1.41,0.2,True,True,0.082,59.5,65.4,58.4,-0.1557,B,True,26.4,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5955,0.7092,-0.1137,0.0339,True,DISAGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.6618,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
9ljz1grea3a8jajif4e9b7bpw,2026-05-24,2wolc27r8z03itcvwp43e38c5,1,1,1,1,BTTS,KG Var,1.62,0.2,True,True,0.124,53.7,69.9,48.4,-0.1476,B,True,1.6,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.6173,-0.0802,,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5592,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
1r7iq0nhg2b674jpcm92ragpg,2026-05-24,1zp1du9n4rj36p1ss9zbxtqfb,4,0,1,0,BTTS,KG Var,1.89,0.2,True,False,-0.2,53.2,69.9,52.9,-0.0832,B,True,0.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5317,0.5291,0.0026,,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5353,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
70dgok3yq76g076vemaps0178,2026-05-24,6lwpjhktjhl9g7x2w7njmzva6,2,1,1,0,BTTS,KG Var,1.74,0.2,True,True,0.148,51.9,68.4,60.4,-0.1255,B,True,15.7,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5192,0.5747,-0.0555,,True,AGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.6164,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
72q9d4uimmby6g6bor26taz9w,2026-05-24,6jgwiu2gq3dllmrwt45pfdn2z,2,0,2,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.555,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,LOW,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
67dd2t7043kv0yw1zj1buwdg4,2026-05-24,8n9w0n3i9kk05echhtmstn6o9,1,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.698,missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
ejdwfph35q57phtfz7jr8st1w,2026-05-24,8ey0ww2zsosdmwr8ehsorh6t7,0,2,0,0,BTTS,KG Var,1.82,0.2,True,False,-0.2,53.2,69.9,63.5,-0.1097,B,True,12.1,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5317,0.5495,-0.0178,,True,DISAGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.51,,X/X,,False,v28-pro-max,betting_brain_approved
|
||||||
|
68y8tlfnilw5trs1oqi4dhfkk,2026-05-24,8n9w0n3i9kk05echhtmstn6o9,2,2,1,1,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,DISAGREE,0.535,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
68fghiwdtwspk1m0ft5mspzx0,2026-05-24,8n9w0n3i9kk05echhtmstn6o9,1,2,1,2,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,DISAGREE,0.602,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
a1kqq0ggywfl6sl4srntxy2hg,2026-05-24,3ww12jab49q8q8mk9avdwjqgk,1,1,1,0,OU25,Üst,1.62,0.2,True,False,-0.2,61.4,65.4,60.6,-0.148,B,True,27.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.6173,-0.0029,0.0854,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5961,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
a1vpp4i6t61v7qm3dfy6iuj2s,2026-05-24,3ww12jab49q8q8mk9avdwjqgk,2,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.74,missing_full_ms_odds;live_match_pre_match_features;ai_features_inferred_from_history,LOW,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
28rk26zqah60qj7h78qh5beac,2026-05-24,663a54fmymndjeev47qm7d3nf,2,2,2,1,OU25,Üst,1.39,0.2,True,True,0.078,61.1,65.4,57.5,-0.2039,B,True,13.3,BET,,low_reliability_league;inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6107,0.7194,-0.1087,0.0577,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,0.3522,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
a26stt54g5ju5ecodpcbcqxw4,2026-05-24,3ww12jab49q8q8mk9avdwjqgk,2,2,1,1,BTTS,KG Var,2.05,0.2,True,True,0.21,53.7,69.9,70.6,0.0354,B,True,33.5,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;usable_historical_sample,0.5371,0.4878,0.0493,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5961,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
318rrnl9b1vlenjd9hwahpvro,2026-05-24,9z5643nd06afqu01ea2wt8y4g,1,0,0,0,DC,1X,1.12,0.0,False,True,0.0,73.5,73.5,72.9,-0.0553,PASS,True,41.8,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features,value_sniper_override;v25_v27_aligned;triple_value_confirmed;usable_historical_sample,0.7351,0.8929,-0.1578,0.031,False,AGREE,0.51,lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8734,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
|
||||||
|
65tlfs3m6sc70261z37i90jys,2026-05-24,89ovpy1rarewwzqvi30bfdr8b,4,3,1,1,OU25,Üst,1.36,0.2,True,True,0.072,60.2,65.4,53.5,-0.2107,B,True,24.8,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6018,0.7353,-0.1335,0.0982,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.7068,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
ei6nilo5074tnb17kvv37oy6s,2026-05-24,3j81qr7yc4gdnakfwnxf95ovh,1,1,1,1,MS,2,1.11,0.0,False,False,0.0,75.5,90.9,66.9,-0.0497,PASS,True,0.0,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features;v25_v27_soft_disagreement;triple_value_not_confirmed,value_sniper_override;strong_historical_sample,0.7554,0.9009,-0.1455,0.204,False,AGREE,0.51,lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8771,0.0,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
|
||||||
|
cx8nb7w3nmell38mta1umh2qc,2026-05-24,54c65mhi143utomzvvv3q2avh,0,2,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.726,missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
53348iqniod61z7xurb4tx250,2026-05-24,477yyajzheg2z8u7uick0e13e,2,0,0,0,OU25,Üst,1.93,0.2,True,False,-0.2,59.5,65.4,71.3,0.0414,B,True,45.8,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5955,0.5181,0.0774,0.1084,False,AGREE,0.74,lineup_probable_not_confirmed;ai_features_inferred_from_history,MEDIUM,0.4706,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
2hw717s7fxi2v2w53kdsplhqs,2026-05-24,8najqkluatpaxvqws78b9s17c,1,1,0,0,DC,12,1.24,0.0,False,False,0.0,70.0,70.0,61.2,-0.12,PASS,True,0.0,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,value_sniper_override;v25_v27_aligned;strong_historical_sample,0.7001,0.8065,-0.1064,0.0075,True,AGREE,0.51,lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,HIGH,0.5082,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
|
||||||
|
1q1s55dy4d4z4gs34qk6vx9n8,2026-05-24,cegl2ivkc25blcatxp4jmk1ec,1,0,1,0,OU25,Üst,2.03,0.2,True,False,-0.2,50.0,57.7,54.3,0.0546,B,True,35.7,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5,0.4926,0.0074,0.0555,False,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5993,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
648u8zd49cwcxpspmvinlmexg,2026-05-24,1eruend45vd20g9hbrpiggs5u,1,0,0,0,MS,1,1.32,0.2,True,True,0.064,63.1,65.5,55.7,-0.0828,B,True,39.4,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6313,0.7576,-0.1263,0.0683,False,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8083,0.0,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
1psnufak57w8dfs9e5cvbmgwk,2026-05-24,cegl2ivkc25blcatxp4jmk1ec,3,0,2,0,BTTS,KG Var,1.94,0.2,True,False,-0.2,53.7,69.9,53.1,-0.0765,B,True,5.3,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.5155,0.0216,,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5993,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
921kqaviappxt0w1kfmq1ek2c,2026-05-24,byu00jvt1j6csyv4y1lkt2fm2,1,0,1,0,DC,X2,1.18,0.0,False,False,0.0,75.8,75.8,74.9,-0.1267,PASS,True,0.0,WATCH_NO_VALUE,odds_below_minimum,inferred_statistical_features;v25_v27_soft_disagreement;trap_market_market_overpriced;triple_value_not_confirmed;engine_consensus_disagree,base_model_playable;value_sniper_override;strong_historical_sample,0.7584,0.8475,-0.0891,0.1783,True,DISAGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.5359,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
|
||||||
|
3m11hvh2fzailt3ykd0uhzz84,2026-05-24,54c65mhi143utomzvvv3q2avh,0,0,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.591,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
7kvvf6blnps2xk15100ccdedw,2026-05-24,4zwgbb66rif2spcoeeol2motx,5,0,3,0,BTTS,KG Var,1.33,0.2,True,False,-0.2,57.1,69.9,62.3,-0.1512,B,True,29.1,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5707,0.7519,-0.1812,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.6995,,1/1,,False,v28-pro-max,betting_brain_approved
|
||||||
|
7liir8zj32o7m2udr7cknb8d0,2026-05-24,4zwgbb66rif2spcoeeol2motx,3,0,2,0,OU25,Üst,1.33,0.2,True,True,0.066,61.4,65.4,58.3,-0.1437,B,True,27.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.7519,-0.1375,0.0279,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.6995,,1/1,,False,v28-pro-max,betting_brain_approved
|
||||||
|
7l74ilyz7olljclexvn8tbjtg,2026-05-24,4zwgbb66rif2spcoeeol2motx,5,1,4,0,BTTS,KG Var,1.55,0.2,True,True,0.11,57.1,69.9,68.2,-0.0873,B,True,32.7,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5707,0.6452,-0.0745,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.6995,,1/1,,False,v28-pro-max,betting_brain_approved
|
||||||
|
8f6gex4eh119d2hh9y2zb5clw,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,2,0,2,0,OU25,Üst,1.49,0.2,True,False,-0.2,66.5,65.4,77.9,0.0081,B,True,50.1,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6651,0.6711,-0.006,0.1144,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.5033,,1/1,,False,v28-pro-max,betting_brain_approved
|
||||||
|
8ee7ipt4u6kyk6baueedsdafo,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,0,2,0,2,BTTS,KG Var,1.69,0.2,True,False,-0.2,54.3,69.9,64.7,-0.0792,B,True,27.6,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5433,0.5917,-0.0484,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,2/2,,False,v28-pro-max,betting_brain_approved
|
||||||
|
8fydg367drpc25r1bobxqj3f8,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,3,1,2,1,OU25,Üst,1.61,0.2,True,True,0.122,50.1,57.7,50.7,-0.1072,B,True,35.0,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5009,0.6211,-0.1202,0.0553,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
8fkdhce1peguwgnsunwoln3f8,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,0,2,0,2,OU25,Üst,1.24,0.0,False,False,0.0,61.4,65.4,61.1,-0.1548,PASS,True,16.8,WATCH_NO_VALUE,odds_below_minimum,inferred_statistical_features;triple_value_not_confirmed;historical_sample_too_low,base_model_playable;value_sniper_override;v25_v27_aligned,0.6144,0.8065,-0.1921,0.0942,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.5033,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
|
||||||
|
9g5hqtjja6ceqhkpghwmoy6ms,2026-05-24,2y8bntiif3a9y6gtmauv30gt,2,0,1,0,OU25,Üst,1.71,0.2,True,False,-0.2,50.1,57.7,52.6,-0.0794,B,True,36.5,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5009,0.5848,-0.0839,0.0481,False,DISAGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.4782,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
8h6429zr5ijqcxc8gjxygjtw4,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,3,0,1,0,MS,1,1.33,0.2,True,True,0.066,66.1,65.5,66.7,-0.1155,B,True,0.0,BET,,inferred_statistical_features;v25_v27_soft_disagreement;trap_market_market_overpriced;triple_value_not_confirmed;htft_reversal_prob_minor=0.11,base_model_playable;value_sniper_override;strong_historical_sample,0.6614,0.7519,-0.0905,0.2583,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,0.0814,1/1,,False,v28-pro-max,betting_brain_approved
|
||||||
|
77knm2ibdtb7akzrbltwz7axg,2026-05-24,bly7ema5au6j40i0grhl0pnub,1,1,1,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.726,missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
8es4680yd87gtmomg2jk3isyc,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,0,1,0,0,OU25,Üst,1.53,0.2,True,False,-0.2,59.5,65.4,67.8,-0.0259,B,True,42.4,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5955,0.6536,-0.0581,0.0713,False,DISAGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,2/2,,False,v28-pro-max,betting_brain_approved
|
||||||
|
8dmcz3k1u4ze53nvrsoz7eoes,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,1,1,1,0,BTTS,KG Var,1.26,0.0,False,True,0.0,54.9,69.9,57.6,-0.2005,PASS,True,3.3,WATCH_NO_VALUE,odds_below_minimum,inferred_statistical_features;triple_value_not_confirmed;historical_sample_too_low,base_model_playable;value_sniper_override,0.5488,0.7937,-0.2449,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.5033,,1/1,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
|
||||||
|
8gcbai6m7v7o8piqfram4qe50,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,3,1,2,0,HTFT,1/1,4.59,0.0,False,True,0.0,27.3,27.3,24.6,0.1657,PASS,True,0.0,REJECT,calibrated_confidence_too_low;play_score_too_low;volatile_market_requires_exceptional_evidence,inferred_statistical_features;historical_sample_too_low,base_model_playable,0.2734,0.2179,0.0555,,False,DISAGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,1/1,,False,v28-pro-max,betting_brain_no_safe_pick
|
||||||
|
3azy3enp78au0zfugc3l1yf4k,2026-05-24,54c65mhi143utomzvvv3q2avh,2,0,1,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.532,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
1d2fb7bt5f8xy5on24w1kj1g4,2026-05-24,54c65mhi143utomzvvv3q2avh,1,0,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.532,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,LOW,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
pw01xm8v3jlz13fpi3zq0ftg,2026-05-24,3umprqta6ipyann6qjjh07biz,1,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
mjo9k4zr1x884vjlwea2y1hw,2026-05-24,3umprqta6ipyann6qjjh07biz,1,0,1,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
8d8fm7wli7tfx8hm9w5l8nuhg,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,1,1,1,0,BTTS,KG Var,1.72,0.2,True,True,0.144,53.7,69.9,65.2,-0.0712,B,True,28.0,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.5814,-0.0443,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
oqsq3f0kvic8xfed8dp302z8,2026-05-24,3umprqta6ipyann6qjjh07biz,3,2,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
o7tn4si7fxvq9c2mg0xs48wk,2026-05-24,3umprqta6ipyann6qjjh07biz,0,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
|
||||||
|
eh9jfegscokidyczxfq691990,2026-05-24,3j81qr7yc4gdnakfwnxf95ovh,2,3,0,1,OU25,Üst,1.44,0.2,True,True,0.088,50.1,57.7,32.9,-0.2537,B,True,17.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5009,0.6944,-0.1935,0.0596,True,AGREE,0.51,lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8771,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
dkhhkbwnxwl47e8hybv89mwb8,2026-05-24,5jd0k2txwnq69frs79eulba8j,1,2,0,1,OU25,Üst,1.23,0.0,False,True,0.0,61.4,65.4,61.2,-0.1185,PASS,True,11.4,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features;triple_value_not_confirmed,value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.813,-0.1986,0.0179,False,AGREE,0.74,ai_features_inferred_from_history,LOW,0.9233,,1/1,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
|
||||||
|
1lknqdz9vmb3hnqu144zkkefo,2026-05-24,1r097lpxe0xn03ihb7wi98kao,1,0,1,0,BTTS,KG Var,1.78,0.2,True,False,-0.2,50.0,61.7,55.6,-0.088,B,True,29.3,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5,0.5618,-0.0618,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.7391,,1/1,,False,v28-pro-max,betting_brain_approved
|
||||||
|
3oazp9kfbyyiatn246k4to6xg,2026-05-24,9ynnnx1qmkizq1o3qr3v0nsuk,1,2,0,1,BTTS,KG Var,1.36,0.2,True,True,0.072,53.7,69.9,61.2,-0.1571,B,True,33.7,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.7353,-0.1982,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.554,,2/2,,False,v28-pro-max,betting_brain_approved
|
||||||
|
8cr8t6qh0r6g0mv6ftq0ic1sk,2026-05-24,a9vrdkelbgif0gtu3wxsr75xo,2,1,0,1,OU25,Üst,1.46,0.2,True,True,0.092,61.4,65.4,68.1,-0.0182,B,True,47.8,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.6849,-0.0705,0.0535,False,AGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.6618,,,,False,v28-pro-max,betting_brain_approved
|
||||||
|
@@ -0,0 +1,220 @@
|
|||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"days": 3,
|
||||||
|
"max_matches": 50,
|
||||||
|
"start": null,
|
||||||
|
"end": null,
|
||||||
|
"progress_interval": 50
|
||||||
|
},
|
||||||
|
"aggregate": {
|
||||||
|
"overall": {
|
||||||
|
"n_total": 50,
|
||||||
|
"n_playable_settled": 27,
|
||||||
|
"wins": 15,
|
||||||
|
"losses": 12,
|
||||||
|
"hit_rate_pct": 55.56,
|
||||||
|
"unit_profit": -0.862,
|
||||||
|
"staked": 5.4,
|
||||||
|
"roi_pct": -15.96
|
||||||
|
},
|
||||||
|
"by_market": {
|
||||||
|
"OU25": {
|
||||||
|
"n_total": 13,
|
||||||
|
"n_playable_settled": 13,
|
||||||
|
"wins": 7,
|
||||||
|
"losses": 6,
|
||||||
|
"hit_rate_pct": 53.85,
|
||||||
|
"unit_profit": -0.6,
|
||||||
|
"staked": 2.6,
|
||||||
|
"roi_pct": -23.08
|
||||||
|
},
|
||||||
|
"BTTS": {
|
||||||
|
"n_total": 12,
|
||||||
|
"n_playable_settled": 12,
|
||||||
|
"wins": 6,
|
||||||
|
"losses": 6,
|
||||||
|
"hit_rate_pct": 50.0,
|
||||||
|
"unit_profit": -0.392,
|
||||||
|
"staked": 2.4,
|
||||||
|
"roi_pct": -16.33
|
||||||
|
},
|
||||||
|
"MS": {
|
||||||
|
"n_total": 2,
|
||||||
|
"n_playable_settled": 2,
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 0,
|
||||||
|
"hit_rate_pct": 100.0,
|
||||||
|
"unit_profit": 0.13,
|
||||||
|
"staked": 0.4,
|
||||||
|
"roi_pct": 32.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_confidence": {
|
||||||
|
"65-70": {
|
||||||
|
"n_total": 22,
|
||||||
|
"n_playable_settled": 22,
|
||||||
|
"wins": 13,
|
||||||
|
"losses": 9,
|
||||||
|
"hit_rate_pct": 59.09,
|
||||||
|
"unit_profit": -0.472,
|
||||||
|
"staked": 4.4,
|
||||||
|
"roi_pct": -10.73
|
||||||
|
},
|
||||||
|
"55-60": {
|
||||||
|
"n_total": 4,
|
||||||
|
"n_playable_settled": 4,
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 2,
|
||||||
|
"hit_rate_pct": 50.0,
|
||||||
|
"unit_profit": -0.19,
|
||||||
|
"staked": 0.8,
|
||||||
|
"roi_pct": -23.75
|
||||||
|
},
|
||||||
|
"60-65": {
|
||||||
|
"n_total": 1,
|
||||||
|
"n_playable_settled": 1,
|
||||||
|
"wins": 0,
|
||||||
|
"losses": 1,
|
||||||
|
"hit_rate_pct": 0.0,
|
||||||
|
"unit_profit": -0.2,
|
||||||
|
"staked": 0.2,
|
||||||
|
"roi_pct": -100.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_odds": {
|
||||||
|
"1.3-1.5": {
|
||||||
|
"n_total": 11,
|
||||||
|
"n_playable_settled": 11,
|
||||||
|
"wins": 9,
|
||||||
|
"losses": 2,
|
||||||
|
"hit_rate_pct": 81.82,
|
||||||
|
"unit_profit": 0.28,
|
||||||
|
"staked": 2.2,
|
||||||
|
"roi_pct": 12.73
|
||||||
|
},
|
||||||
|
"1.5-1.8": {
|
||||||
|
"n_total": 10,
|
||||||
|
"n_playable_settled": 10,
|
||||||
|
"wins": 5,
|
||||||
|
"losses": 5,
|
||||||
|
"hit_rate_pct": 50.0,
|
||||||
|
"unit_profit": -0.352,
|
||||||
|
"staked": 2.0,
|
||||||
|
"roi_pct": -17.6
|
||||||
|
},
|
||||||
|
"1.8-2.2": {
|
||||||
|
"n_total": 6,
|
||||||
|
"n_playable_settled": 6,
|
||||||
|
"wins": 1,
|
||||||
|
"losses": 5,
|
||||||
|
"hit_rate_pct": 16.67,
|
||||||
|
"unit_profit": -0.79,
|
||||||
|
"staked": 1.2,
|
||||||
|
"roi_pct": -65.83
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_grade": {
|
||||||
|
"B": {
|
||||||
|
"n_total": 27,
|
||||||
|
"n_playable_settled": 27,
|
||||||
|
"wins": 15,
|
||||||
|
"losses": 12,
|
||||||
|
"hit_rate_pct": 55.56,
|
||||||
|
"unit_profit": -0.862,
|
||||||
|
"staked": 5.4,
|
||||||
|
"roi_pct": -15.96
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_competition": {
|
||||||
|
"league": {
|
||||||
|
"n_total": 27,
|
||||||
|
"n_playable_settled": 27,
|
||||||
|
"wins": 15,
|
||||||
|
"losses": 12,
|
||||||
|
"hit_rate_pct": 55.56,
|
||||||
|
"unit_profit": -0.862,
|
||||||
|
"staked": 5.4,
|
||||||
|
"roi_pct": -15.96
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loss_diagnostics": {
|
||||||
|
"n_losses": 12,
|
||||||
|
"total_loss_units": -2.4,
|
||||||
|
"patterns": {
|
||||||
|
"high_htft_reversal_prob (>=0.20)": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"cup_match": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"low_league_reliability (<0.45)": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"v27_disagree": [
|
||||||
|
3,
|
||||||
|
25.0
|
||||||
|
],
|
||||||
|
"trap_market_flagged": [
|
||||||
|
4,
|
||||||
|
33.33
|
||||||
|
],
|
||||||
|
"low_calibrated_conf (<55)": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"high_odds_underdog (>=2.5)": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"low_data_quality (<0.55)": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"high_risk_level": [
|
||||||
|
3,
|
||||||
|
25.0
|
||||||
|
],
|
||||||
|
"inferred_features": [
|
||||||
|
12,
|
||||||
|
100.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"by_market": [
|
||||||
|
[
|
||||||
|
"BTTS",
|
||||||
|
6
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"OU25",
|
||||||
|
6
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"by_league": [
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
12
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"top_bb_issues_in_losses": [
|
||||||
|
[
|
||||||
|
"inferred_statistical_features",
|
||||||
|
12
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"triple_value_not_confirmed",
|
||||||
|
12
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"trap_market_market_overpriced",
|
||||||
|
4
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"top_bb_vetoes_in_losses": []
|
||||||
|
},
|
||||||
|
"recommendations": [],
|
||||||
|
"errors_sample": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
==============================================================================
|
||||||
|
DIAGNOSTIC BACKTEST REPORT
|
||||||
|
==============================================================================
|
||||||
|
Generated: 2026-05-25T02:44:37
|
||||||
|
Sample window: start=-3d, end=now
|
||||||
|
Max matches: 50
|
||||||
|
Excluded days: ['2026-04-29', '2026-05-03']
|
||||||
|
|
||||||
|
OVERALL
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
n_total : 50
|
||||||
|
n_playable_settled : 27
|
||||||
|
wins : 15
|
||||||
|
losses : 12
|
||||||
|
hit_rate_pct : 55.56
|
||||||
|
unit_profit : -0.862
|
||||||
|
staked : 5.4
|
||||||
|
roi_pct : -15.96
|
||||||
|
|
||||||
|
PER MARKET
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
market n hit% profit roi%
|
||||||
|
OU25 13 53.85 -0.6 -23.08
|
||||||
|
BTTS 12 50.0 -0.392 -16.33
|
||||||
|
MS 2 100.0 0.13 32.5
|
||||||
|
|
||||||
|
PER CALIBRATED CONFIDENCE BAND
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
band n hit% roi%
|
||||||
|
55-60 4 50.0 -23.75
|
||||||
|
60-65 1 0.0 -100.0
|
||||||
|
65-70 22 59.09 -10.73
|
||||||
|
|
||||||
|
PER ODDS BAND
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
band n hit% roi%
|
||||||
|
1.3-1.5 11 81.82 12.73
|
||||||
|
1.5-1.8 10 50.0 -17.6
|
||||||
|
1.8-2.2 6 16.67 -65.83
|
||||||
|
|
||||||
|
LEAGUE vs CUP
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
league n= 27 hit=55.56% roi=-15.96%
|
||||||
|
|
||||||
|
LOSS DIAGNOSTICS
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
total losses: 12
|
||||||
|
total lost units: -2.4
|
||||||
|
By market: [('BTTS', 6), ('OU25', 6)]
|
||||||
|
Loss patterns (count, % of losses):
|
||||||
|
high_htft_reversal_prob (>=0.20) 0 (0.0%)
|
||||||
|
cup_match 0 (0.0%)
|
||||||
|
low_league_reliability (<0.45) 0 (0.0%)
|
||||||
|
v27_disagree 3 (25.0%)
|
||||||
|
trap_market_flagged 4 (33.33%)
|
||||||
|
low_calibrated_conf (<55) 0 (0.0%)
|
||||||
|
high_odds_underdog (>=2.5) 0 (0.0%)
|
||||||
|
low_data_quality (<0.55) 0 (0.0%)
|
||||||
|
high_risk_level 3 (25.0%)
|
||||||
|
inferred_features 12 (100.0%)
|
||||||
|
Top betting_brain issues seen in losses:
|
||||||
|
inferred_statistical_features 12
|
||||||
|
triple_value_not_confirmed 12
|
||||||
|
trap_market_market_overpriced 4
|
||||||
|
Top betting_brain vetoes (in losses — i.e. veto fired but bet still went through value-sniper override):
|
||||||
|
|
||||||
|
RECOMMENDATIONS
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
(none surfaced — sample too small or no clear pattern)
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
{
|
||||||
|
"args": {
|
||||||
|
"days": 14,
|
||||||
|
"max_matches": 1000,
|
||||||
|
"start": null,
|
||||||
|
"end": null,
|
||||||
|
"progress_interval": 50
|
||||||
|
},
|
||||||
|
"aggregate": {
|
||||||
|
"overall": {
|
||||||
|
"n_total": 1000,
|
||||||
|
"n_playable_settled": 524,
|
||||||
|
"wins": 287,
|
||||||
|
"losses": 237,
|
||||||
|
"hit_rate_pct": 54.77,
|
||||||
|
"unit_profit": -17.897,
|
||||||
|
"staked": 107.0,
|
||||||
|
"roi_pct": -16.73
|
||||||
|
},
|
||||||
|
"by_market": {
|
||||||
|
"OU25": {
|
||||||
|
"n_total": 236,
|
||||||
|
"n_playable_settled": 236,
|
||||||
|
"wins": 134,
|
||||||
|
"losses": 102,
|
||||||
|
"hit_rate_pct": 56.78,
|
||||||
|
"unit_profit": -6.271,
|
||||||
|
"staked": 48.5,
|
||||||
|
"roi_pct": -12.93
|
||||||
|
},
|
||||||
|
"BTTS": {
|
||||||
|
"n_total": 205,
|
||||||
|
"n_playable_settled": 205,
|
||||||
|
"wins": 105,
|
||||||
|
"losses": 100,
|
||||||
|
"hit_rate_pct": 51.22,
|
||||||
|
"unit_profit": -8.89,
|
||||||
|
"staked": 41.1,
|
||||||
|
"roi_pct": -21.63
|
||||||
|
},
|
||||||
|
"MS": {
|
||||||
|
"n_total": 76,
|
||||||
|
"n_playable_settled": 76,
|
||||||
|
"wins": 44,
|
||||||
|
"losses": 32,
|
||||||
|
"hit_rate_pct": 57.89,
|
||||||
|
"unit_profit": -2.396,
|
||||||
|
"staked": 16.0,
|
||||||
|
"roi_pct": -14.98
|
||||||
|
},
|
||||||
|
"OU35": {
|
||||||
|
"n_total": 3,
|
||||||
|
"n_playable_settled": 3,
|
||||||
|
"wins": 0,
|
||||||
|
"losses": 3,
|
||||||
|
"hit_rate_pct": 0.0,
|
||||||
|
"unit_profit": -0.6,
|
||||||
|
"staked": 0.6,
|
||||||
|
"roi_pct": -100.0
|
||||||
|
},
|
||||||
|
"DC": {
|
||||||
|
"n_total": 4,
|
||||||
|
"n_playable_settled": 4,
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 0,
|
||||||
|
"hit_rate_pct": 100.0,
|
||||||
|
"unit_profit": 0.26,
|
||||||
|
"staked": 0.8,
|
||||||
|
"roi_pct": 32.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_confidence": {
|
||||||
|
"65-70": {
|
||||||
|
"n_total": 420,
|
||||||
|
"n_playable_settled": 420,
|
||||||
|
"wins": 233,
|
||||||
|
"losses": 187,
|
||||||
|
"hit_rate_pct": 55.48,
|
||||||
|
"unit_profit": -14.057,
|
||||||
|
"staked": 85.1,
|
||||||
|
"roi_pct": -16.52
|
||||||
|
},
|
||||||
|
"60-65": {
|
||||||
|
"n_total": 33,
|
||||||
|
"n_playable_settled": 33,
|
||||||
|
"wins": 16,
|
||||||
|
"losses": 17,
|
||||||
|
"hit_rate_pct": 48.48,
|
||||||
|
"unit_profit": -1.61,
|
||||||
|
"staked": 6.6,
|
||||||
|
"roi_pct": -24.39
|
||||||
|
},
|
||||||
|
"55-60": {
|
||||||
|
"n_total": 52,
|
||||||
|
"n_playable_settled": 52,
|
||||||
|
"wins": 28,
|
||||||
|
"losses": 24,
|
||||||
|
"hit_rate_pct": 53.85,
|
||||||
|
"unit_profit": -0.668,
|
||||||
|
"staked": 10.5,
|
||||||
|
"roi_pct": -6.36
|
||||||
|
},
|
||||||
|
"50-55": {
|
||||||
|
"n_total": 5,
|
||||||
|
"n_playable_settled": 5,
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 3,
|
||||||
|
"hit_rate_pct": 40.0,
|
||||||
|
"unit_profit": -0.64,
|
||||||
|
"staked": 1.3,
|
||||||
|
"roi_pct": -49.23
|
||||||
|
},
|
||||||
|
"45-50": {
|
||||||
|
"n_total": 8,
|
||||||
|
"n_playable_settled": 8,
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 4,
|
||||||
|
"hit_rate_pct": 50.0,
|
||||||
|
"unit_profit": -0.382,
|
||||||
|
"staked": 1.9,
|
||||||
|
"roi_pct": -20.11
|
||||||
|
},
|
||||||
|
"70-80": {
|
||||||
|
"n_total": 6,
|
||||||
|
"n_playable_settled": 6,
|
||||||
|
"wins": 4,
|
||||||
|
"losses": 2,
|
||||||
|
"hit_rate_pct": 66.67,
|
||||||
|
"unit_profit": -0.54,
|
||||||
|
"staked": 1.6,
|
||||||
|
"roi_pct": -33.75
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_odds": {
|
||||||
|
"1.3-1.5": {
|
||||||
|
"n_total": 241,
|
||||||
|
"n_playable_settled": 241,
|
||||||
|
"wins": 148,
|
||||||
|
"losses": 93,
|
||||||
|
"hit_rate_pct": 61.41,
|
||||||
|
"unit_profit": -7.408,
|
||||||
|
"staked": 49.0,
|
||||||
|
"roi_pct": -15.12
|
||||||
|
},
|
||||||
|
"1.5-1.8": {
|
||||||
|
"n_total": 221,
|
||||||
|
"n_playable_settled": 221,
|
||||||
|
"wins": 115,
|
||||||
|
"losses": 106,
|
||||||
|
"hit_rate_pct": 52.04,
|
||||||
|
"unit_profit": -6.926,
|
||||||
|
"staked": 44.3,
|
||||||
|
"roi_pct": -15.63
|
||||||
|
},
|
||||||
|
"1.8-2.2": {
|
||||||
|
"n_total": 56,
|
||||||
|
"n_playable_settled": 56,
|
||||||
|
"wins": 23,
|
||||||
|
"losses": 33,
|
||||||
|
"hit_rate_pct": 41.07,
|
||||||
|
"unit_profit": -2.789,
|
||||||
|
"staked": 12.2,
|
||||||
|
"roi_pct": -22.86
|
||||||
|
},
|
||||||
|
"2.2-3.0": {
|
||||||
|
"n_total": 5,
|
||||||
|
"n_playable_settled": 5,
|
||||||
|
"wins": 1,
|
||||||
|
"losses": 4,
|
||||||
|
"hit_rate_pct": 20.0,
|
||||||
|
"unit_profit": -0.574,
|
||||||
|
"staked": 1.3,
|
||||||
|
"roi_pct": -44.15
|
||||||
|
},
|
||||||
|
"3.0-5.0": {
|
||||||
|
"n_total": 1,
|
||||||
|
"n_playable_settled": 1,
|
||||||
|
"wins": 0,
|
||||||
|
"losses": 1,
|
||||||
|
"hit_rate_pct": 0.0,
|
||||||
|
"unit_profit": -0.2,
|
||||||
|
"staked": 0.2,
|
||||||
|
"roi_pct": -100.0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_grade": {
|
||||||
|
"B": {
|
||||||
|
"n_total": 518,
|
||||||
|
"n_playable_settled": 518,
|
||||||
|
"wins": 285,
|
||||||
|
"losses": 233,
|
||||||
|
"hit_rate_pct": 55.02,
|
||||||
|
"unit_profit": -16.931,
|
||||||
|
"staked": 105.3,
|
||||||
|
"roi_pct": -16.08
|
||||||
|
},
|
||||||
|
"A": {
|
||||||
|
"n_total": 6,
|
||||||
|
"n_playable_settled": 6,
|
||||||
|
"wins": 2,
|
||||||
|
"losses": 4,
|
||||||
|
"hit_rate_pct": 33.33,
|
||||||
|
"unit_profit": -0.966,
|
||||||
|
"staked": 1.7,
|
||||||
|
"roi_pct": -56.82
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"by_competition": {
|
||||||
|
"league": {
|
||||||
|
"n_total": 524,
|
||||||
|
"n_playable_settled": 524,
|
||||||
|
"wins": 287,
|
||||||
|
"losses": 237,
|
||||||
|
"hit_rate_pct": 54.77,
|
||||||
|
"unit_profit": -17.897,
|
||||||
|
"staked": 107.0,
|
||||||
|
"roi_pct": -16.73
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loss_diagnostics": {
|
||||||
|
"n_losses": 237,
|
||||||
|
"total_loss_units": -48.7,
|
||||||
|
"patterns": {
|
||||||
|
"high_htft_reversal_prob (>=0.20)": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"cup_match": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"low_league_reliability (<0.45)": [
|
||||||
|
42,
|
||||||
|
17.72
|
||||||
|
],
|
||||||
|
"v27_disagree": [
|
||||||
|
60,
|
||||||
|
25.32
|
||||||
|
],
|
||||||
|
"trap_market_flagged": [
|
||||||
|
81,
|
||||||
|
34.18
|
||||||
|
],
|
||||||
|
"low_calibrated_conf (<55)": [
|
||||||
|
7,
|
||||||
|
2.95
|
||||||
|
],
|
||||||
|
"high_odds_underdog (>=2.5)": [
|
||||||
|
4,
|
||||||
|
1.69
|
||||||
|
],
|
||||||
|
"low_data_quality (<0.55)": [
|
||||||
|
40,
|
||||||
|
16.88
|
||||||
|
],
|
||||||
|
"high_risk_level": [
|
||||||
|
20,
|
||||||
|
8.44
|
||||||
|
],
|
||||||
|
"inferred_features": [
|
||||||
|
0,
|
||||||
|
0.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"by_market": [
|
||||||
|
[
|
||||||
|
"OU25",
|
||||||
|
102
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"BTTS",
|
||||||
|
100
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"MS",
|
||||||
|
32
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"OU35",
|
||||||
|
3
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"by_league": [
|
||||||
|
[
|
||||||
|
null,
|
||||||
|
237
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"top_bb_issues_in_losses": [
|
||||||
|
[
|
||||||
|
"triple_value_not_confirmed",
|
||||||
|
230
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"trap_market_market_overpriced",
|
||||||
|
81
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"low_reliability_league",
|
||||||
|
40
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"v25_v27_soft_disagreement",
|
||||||
|
10
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"engine_consensus_disagree",
|
||||||
|
5
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"historical_sample_too_low",
|
||||||
|
3
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"very_low_reliability_league",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"htft_reversal_prob_minor=0.13",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"top_bb_vetoes_in_losses": []
|
||||||
|
},
|
||||||
|
"recommendations": [
|
||||||
|
{
|
||||||
|
"type": "raise_confidence_threshold",
|
||||||
|
"confidence_band": "65-70",
|
||||||
|
"evidence": "n=420, roi=-16.52%",
|
||||||
|
"suggested_fix": "Raise MIN_BET_SCORE or market_min_conf above 65"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"errors_sample": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
==============================================================================
|
||||||
|
DIAGNOSTIC BACKTEST REPORT
|
||||||
|
==============================================================================
|
||||||
|
Generated: 2026-05-25T03:56:49
|
||||||
|
Sample window: start=-14d, end=now
|
||||||
|
Max matches: 1000
|
||||||
|
Excluded days: ['2026-04-29', '2026-05-03']
|
||||||
|
|
||||||
|
OVERALL
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
n_total : 1000
|
||||||
|
n_playable_settled : 524
|
||||||
|
wins : 287
|
||||||
|
losses : 237
|
||||||
|
hit_rate_pct : 54.77
|
||||||
|
unit_profit : -17.897
|
||||||
|
staked : 107.0
|
||||||
|
roi_pct : -16.73
|
||||||
|
|
||||||
|
PER MARKET
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
market n hit% profit roi%
|
||||||
|
OU25 236 56.78 -6.271 -12.93
|
||||||
|
BTTS 205 51.22 -8.89 -21.63
|
||||||
|
MS 76 57.89 -2.396 -14.98
|
||||||
|
DC 4 100.0 0.26 32.5
|
||||||
|
OU35 3 0.0 -0.6 -100.0
|
||||||
|
|
||||||
|
PER CALIBRATED CONFIDENCE BAND
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
band n hit% roi%
|
||||||
|
45-50 8 50.0 -20.11
|
||||||
|
50-55 5 40.0 -49.23
|
||||||
|
55-60 52 53.85 -6.36
|
||||||
|
60-65 33 48.48 -24.39
|
||||||
|
65-70 420 55.48 -16.52
|
||||||
|
70-80 6 66.67 -33.75
|
||||||
|
|
||||||
|
PER ODDS BAND
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
band n hit% roi%
|
||||||
|
1.3-1.5 241 61.41 -15.12
|
||||||
|
1.5-1.8 221 52.04 -15.63
|
||||||
|
1.8-2.2 56 41.07 -22.86
|
||||||
|
2.2-3.0 5 20.0 -44.15
|
||||||
|
3.0-5.0 1 0.0 -100.0
|
||||||
|
|
||||||
|
LEAGUE vs CUP
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
league n= 524 hit=54.77% roi=-16.73%
|
||||||
|
|
||||||
|
LOSS DIAGNOSTICS
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
total losses: 237
|
||||||
|
total lost units: -48.7
|
||||||
|
By market: [('OU25', 102), ('BTTS', 100), ('MS', 32), ('OU35', 3)]
|
||||||
|
Loss patterns (count, % of losses):
|
||||||
|
high_htft_reversal_prob (>=0.20) 0 (0.0%)
|
||||||
|
cup_match 0 (0.0%)
|
||||||
|
low_league_reliability (<0.45) 42 (17.72%)
|
||||||
|
v27_disagree 60 (25.32%)
|
||||||
|
trap_market_flagged 81 (34.18%)
|
||||||
|
low_calibrated_conf (<55) 7 (2.95%)
|
||||||
|
high_odds_underdog (>=2.5) 4 (1.69%)
|
||||||
|
low_data_quality (<0.55) 40 (16.88%)
|
||||||
|
high_risk_level 20 (8.44%)
|
||||||
|
inferred_features 0 (0.0%)
|
||||||
|
Top betting_brain issues seen in losses:
|
||||||
|
triple_value_not_confirmed 230
|
||||||
|
trap_market_market_overpriced 81
|
||||||
|
low_reliability_league 40
|
||||||
|
v25_v27_soft_disagreement 10
|
||||||
|
engine_consensus_disagree 5
|
||||||
|
historical_sample_too_low 3
|
||||||
|
very_low_reliability_league 2
|
||||||
|
htft_reversal_prob_minor=0.13 1
|
||||||
|
Top betting_brain vetoes (in losses — i.e. veto fired but bet still went through value-sniper override):
|
||||||
|
|
||||||
|
RECOMMENDATIONS
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
• [raise_confidence_threshold]
|
||||||
|
confidence_band: 65-70
|
||||||
|
evidence: n=420, roi=-16.52%
|
||||||
|
suggested_fix: Raise MIN_BET_SCORE or market_min_conf above 65
|
||||||
|
|
||||||
|
==============================================================================
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"BTTS": {
|
||||||
|
"min_calibrated_confidence": 65,
|
||||||
|
"min_ev_edge": -1.0,
|
||||||
|
"max_ev_edge": 0.1,
|
||||||
|
"min_odds": 1.4,
|
||||||
|
"max_odds": 10.0,
|
||||||
|
"min_odds_reliability": 0.55,
|
||||||
|
"require_v27_agree": true,
|
||||||
|
"expected_n_bets": 54,
|
||||||
|
"expected_hit_pct": 55.56,
|
||||||
|
"expected_roi_pct": -10.96
|
||||||
|
},
|
||||||
|
"MS": {
|
||||||
|
"min_calibrated_confidence": 0,
|
||||||
|
"min_ev_edge": -0.05,
|
||||||
|
"max_ev_edge": 0.15,
|
||||||
|
"min_odds": 1.2,
|
||||||
|
"max_odds": 10.0,
|
||||||
|
"min_odds_reliability": 0.0,
|
||||||
|
"require_v27_agree": true,
|
||||||
|
"expected_n_bets": 21,
|
||||||
|
"expected_hit_pct": 61.9,
|
||||||
|
"expected_roi_pct": 8.23
|
||||||
|
},
|
||||||
|
"OU25": {
|
||||||
|
"min_calibrated_confidence": 0,
|
||||||
|
"min_ev_edge": -1.0,
|
||||||
|
"max_ev_edge": 0.15,
|
||||||
|
"min_odds": 1.8,
|
||||||
|
"max_odds": 10.0,
|
||||||
|
"min_odds_reliability": 0.0,
|
||||||
|
"require_v27_agree": false,
|
||||||
|
"expected_n_bets": 20,
|
||||||
|
"expected_hit_pct": 65.0,
|
||||||
|
"expected_roi_pct": 28.91
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
"""
|
||||||
|
Deep root-cause analysis on diagnostic_backtest CSV.
|
||||||
|
Tests specific hypotheses with hard numbers and proposes actionable
|
||||||
|
filter rules with estimated impact (units saved, ROI shift).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys, os, glob
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
REPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "reports")
|
||||||
|
|
||||||
|
def latest_csv():
|
||||||
|
files = sorted(glob.glob(os.path.join(REPORTS_DIR, "diagnostic_backtest_*.csv")),
|
||||||
|
key=os.path.getmtime, reverse=True)
|
||||||
|
return files[0] if files else None
|
||||||
|
|
||||||
|
def fmt_pct(x):
|
||||||
|
return f"{x:>6.2f}%" if pd.notna(x) else " ----"
|
||||||
|
|
||||||
|
def cell(df, label, mask):
|
||||||
|
sub = df[mask]
|
||||||
|
n = len(sub)
|
||||||
|
if n == 0:
|
||||||
|
return f" {label:<60} n=0"
|
||||||
|
wins = (sub["won"] == True).sum()
|
||||||
|
losses = (sub["won"] == False).sum()
|
||||||
|
settled = wins + losses
|
||||||
|
hr = 100.0 * wins / settled if settled else 0
|
||||||
|
profit = sub["unit_profit"].sum()
|
||||||
|
staked = sub["stake_units"].sum()
|
||||||
|
roi = 100.0 * profit / staked if staked else 0
|
||||||
|
return (f" {label:<60} n={n:>4} hit={hr:>6.2f}% "
|
||||||
|
f"profit={profit:>+7.2f}u roi={roi:>+7.2f}%")
|
||||||
|
|
||||||
|
def hypothesis_block(title, rows):
|
||||||
|
print(f"\n{'─' * 78}")
|
||||||
|
print(f" {title}")
|
||||||
|
print(f"{'─' * 78}")
|
||||||
|
for row in rows:
|
||||||
|
print(row)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
csv_path = latest_csv()
|
||||||
|
if not csv_path:
|
||||||
|
print("No backtest CSV found")
|
||||||
|
return
|
||||||
|
print(f"Reading {csv_path}")
|
||||||
|
df = pd.read_csv(csv_path)
|
||||||
|
print(f"Loaded {len(df)} rows")
|
||||||
|
|
||||||
|
# Filter only playable + settled
|
||||||
|
pdf = df[(df["playable"] == True) & (df["won"].notna())].copy()
|
||||||
|
pdf["won"] = pdf["won"].astype(bool)
|
||||||
|
print(f"Playable + settled: {len(pdf)}")
|
||||||
|
|
||||||
|
overall_hr = (pdf["won"].sum() / len(pdf)) * 100
|
||||||
|
overall_roi = 100.0 * pdf["unit_profit"].sum() / pdf["stake_units"].sum()
|
||||||
|
print(f"\nOVERALL: hit={overall_hr:.2f}% roi={overall_roi:.2f}%")
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H1: TRIPLE VALUE CONFIRMATION
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
triple_confirmed_mask = ~pdf["bb_issues"].fillna("").str.contains(
|
||||||
|
"triple_value_not_confirmed", na=False
|
||||||
|
)
|
||||||
|
hypothesis_block(
|
||||||
|
"H1: TRIPLE VALUE CONFIRMED vs NOT CONFIRMED",
|
||||||
|
[
|
||||||
|
cell(pdf, "triple_value CONFIRMED", triple_confirmed_mask),
|
||||||
|
cell(pdf, "triple_value NOT CONFIRMED", ~triple_confirmed_mask),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H2: TRAP MARKET FLAG
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
trap_mask = pdf["bb_trap_market"] == True
|
||||||
|
hypothesis_block(
|
||||||
|
"H2: TRAP MARKET FLAG (model says band rate < implied → market overpriced)",
|
||||||
|
[
|
||||||
|
cell(pdf, "trap_market_flag = TRUE (model warned)", trap_mask),
|
||||||
|
cell(pdf, "trap_market_flag = FALSE", ~trap_mask),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H3: V25/V27 CONSENSUS
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
agree_mask = pdf["v27_consensus"] == "AGREE"
|
||||||
|
disagree_mask = pdf["v27_consensus"] == "DISAGREE"
|
||||||
|
hypothesis_block(
|
||||||
|
"H3: V25 ↔ V27 CONSENSUS",
|
||||||
|
[
|
||||||
|
cell(pdf, "AGREE", agree_mask),
|
||||||
|
cell(pdf, "DISAGREE", disagree_mask),
|
||||||
|
cell(pdf, "neither/null", ~(agree_mask | disagree_mask)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H4: ODDS RELIABILITY (league quality)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
pdf["rel_band"] = pd.cut(
|
||||||
|
pdf["odds_reliability"].fillna(0.35),
|
||||||
|
[0, 0.30, 0.45, 0.55, 1.0],
|
||||||
|
labels=["<0.30 verylow", "0.30-0.45 low", "0.45-0.55 mid", ">=0.55 high"]
|
||||||
|
)
|
||||||
|
hypothesis_block(
|
||||||
|
"H4: LEAGUE ODDS RELIABILITY",
|
||||||
|
[cell(pdf, str(b), pdf["rel_band"] == b) for b in pdf["rel_band"].cat.categories]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H5: CALIBRATOR IMPACT (raw vs calibrated)
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
pdf["calib_delta"] = pdf["calibrated_confidence"] - pdf["raw_confidence"]
|
||||||
|
pdf["delta_band"] = pd.cut(
|
||||||
|
pdf["calib_delta"].fillna(0),
|
||||||
|
[-100, -10, -3, 3, 10, 100],
|
||||||
|
labels=["cal<<raw (-10+)", "cal<raw (-3..-10)", "≈equal (±3)",
|
||||||
|
"cal>raw (3..10)", "cal>>raw (+10+)"]
|
||||||
|
)
|
||||||
|
hypothesis_block(
|
||||||
|
"H5: CALIBRATOR DELTA (calibrated_conf - raw_conf)",
|
||||||
|
[cell(pdf, str(b), pdf["delta_band"] == b) for b in pdf["delta_band"].cat.categories]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H6: EV EDGE
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
pdf["edge_band"] = pd.cut(
|
||||||
|
pdf["ev_edge"].fillna(0),
|
||||||
|
[-10, -0.05, 0.0, 0.05, 0.10, 0.20, 10],
|
||||||
|
labels=["edge<-5%", "-5%-0%", "0-5%", "5-10%", "10-20%", ">20%"]
|
||||||
|
)
|
||||||
|
hypothesis_block(
|
||||||
|
"H6: EV EDGE (model_prob - implied_prob)",
|
||||||
|
[cell(pdf, str(b), pdf["edge_band"] == b) for b in pdf["edge_band"].cat.categories]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H7: ODDS x MARKET cross
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
pdf["odds_band"] = pd.cut(
|
||||||
|
pdf["odds"].fillna(0),
|
||||||
|
[0, 1.30, 1.50, 1.80, 2.20, 3.00, 100],
|
||||||
|
labels=["<1.30", "1.30-1.50", "1.50-1.80", "1.80-2.20", "2.20-3.00", ">3.00"]
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"\n{'─' * 78}")
|
||||||
|
print(f" H7: ODDS BAND × MARKET (per cell hit% / roi% / n)")
|
||||||
|
print(f"{'─' * 78}")
|
||||||
|
pivot_n = pdf.pivot_table(index="market", columns="odds_band",
|
||||||
|
values="match_id", aggfunc="count", fill_value=0,
|
||||||
|
observed=False)
|
||||||
|
pivot_roi = pdf.pivot_table(index="market", columns="odds_band",
|
||||||
|
values="unit_profit", aggfunc="sum", fill_value=0,
|
||||||
|
observed=False)
|
||||||
|
pivot_stake = pdf.pivot_table(index="market", columns="odds_band",
|
||||||
|
values="stake_units", aggfunc="sum", fill_value=0,
|
||||||
|
observed=False)
|
||||||
|
pivot_roi_pct = (100.0 * pivot_roi / pivot_stake.replace(0, np.nan)).round(1)
|
||||||
|
print("\n Bet count per cell:")
|
||||||
|
print(pivot_n.to_string())
|
||||||
|
print("\n ROI% per cell:")
|
||||||
|
print(pivot_roi_pct.to_string())
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
# H8: COMBINED FILTER SIMULATION
|
||||||
|
# ─────────────────────────────────────────────────────────────────────
|
||||||
|
print(f"\n{'─' * 78}")
|
||||||
|
print(" H8: COMBINED FILTER SIMULATION (what if we add rules)")
|
||||||
|
print(f"{'─' * 78}")
|
||||||
|
|
||||||
|
def simulate(filter_name, keep_mask):
|
||||||
|
kept = pdf[keep_mask]
|
||||||
|
rejected = pdf[~keep_mask]
|
||||||
|
if len(kept) == 0:
|
||||||
|
return f" {filter_name:<55} → 0 bet remain"
|
||||||
|
kept_hr = 100.0 * kept["won"].sum() / len(kept)
|
||||||
|
kept_profit = kept["unit_profit"].sum()
|
||||||
|
kept_staked = kept["stake_units"].sum()
|
||||||
|
kept_roi = 100.0 * kept_profit / kept_staked if kept_staked else 0
|
||||||
|
saved = -rejected["unit_profit"].sum() # money we WOULD HAVE LOST
|
||||||
|
return (f" {filter_name:<55} keep={len(kept):>3} hit={kept_hr:>5.1f}% "
|
||||||
|
f"roi={kept_roi:>+6.2f}% saved={saved:>+6.2f}u")
|
||||||
|
|
||||||
|
print(simulate("BASELINE (no extra filter)", pd.Series([True] * len(pdf), index=pdf.index)))
|
||||||
|
print(simulate("REJECT triple_value_not_confirmed",
|
||||||
|
~pdf["bb_issues"].fillna("").str.contains("triple_value_not_confirmed")))
|
||||||
|
print(simulate("REJECT trap_market_flag",
|
||||||
|
~(pdf["bb_trap_market"] == True)))
|
||||||
|
print(simulate("REJECT v27 DISAGREE",
|
||||||
|
pdf["v27_consensus"] != "DISAGREE"))
|
||||||
|
print(simulate("REJECT odds_reliability < 0.45",
|
||||||
|
pdf["odds_reliability"].fillna(1.0) >= 0.45))
|
||||||
|
print(simulate("REJECT odds in 1.80-2.20",
|
||||||
|
(pdf["odds"].fillna(0) < 1.80) | (pdf["odds"].fillna(0) >= 2.20)))
|
||||||
|
print(simulate("REJECT ev_edge < 0",
|
||||||
|
pdf["ev_edge"].fillna(0) >= 0))
|
||||||
|
print(simulate("REJECT ev_edge < 0.05",
|
||||||
|
pdf["ev_edge"].fillna(0) >= 0.05))
|
||||||
|
print()
|
||||||
|
print(" COMBINED rules:")
|
||||||
|
# Stack 1: drop triple_not_confirmed + trap_market + DISAGREE
|
||||||
|
s1 = (
|
||||||
|
~pdf["bb_issues"].fillna("").str.contains("triple_value_not_confirmed")
|
||||||
|
& ~(pdf["bb_trap_market"] == True)
|
||||||
|
& (pdf["v27_consensus"] != "DISAGREE")
|
||||||
|
)
|
||||||
|
print(simulate("STACK1: !triple_not_conf & !trap & !disagree", s1))
|
||||||
|
# Stack 2: + edge>=0
|
||||||
|
s2 = s1 & (pdf["ev_edge"].fillna(0) >= 0)
|
||||||
|
print(simulate("STACK2: STACK1 + edge >= 0", s2))
|
||||||
|
# Stack 3: + reliability>=0.45
|
||||||
|
s3 = s2 & (pdf["odds_reliability"].fillna(1.0) >= 0.45)
|
||||||
|
print(simulate("STACK3: STACK2 + reliability >= 0.45", s3))
|
||||||
|
# Stack 4: + odds outside 1.80-2.20
|
||||||
|
s4 = s3 & ((pdf["odds"].fillna(0) < 1.80) | (pdf["odds"].fillna(0) >= 2.20))
|
||||||
|
print(simulate("STACK4: STACK3 + odds NOT in 1.80-2.20", s4))
|
||||||
|
|
||||||
|
print(f"\n{'─' * 78}")
|
||||||
|
print("DONE.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
Analyze Match v2 — the per-match multi-market value board + disciplined pick.
|
||||||
|
===========================================================================
|
||||||
|
Answers "for ONE match, show every bet type's probability + model signal +
|
||||||
|
market-vs-model value, and pick the right bet." Leak-free models.
|
||||||
|
|
||||||
|
KEY HONEST RULE (proven by multi_market_edge.py): compute & SHOW value for all
|
||||||
|
markets, but only MS (1X2) carries real, fold-consistent model edge. In OU/HT/
|
||||||
|
BTTS the market is efficient — a big model-vs-market gap there is the MODEL'S
|
||||||
|
ERROR, not value. So non-MS rows are INFO-ONLY; only an MS value bet in the
|
||||||
|
favourite band is STAKED.
|
||||||
|
|
||||||
|
Demo: trains all market models on the first 85% of history, then prints the full
|
||||||
|
board for sample matches in the unseen last 15% (with what actually happened).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/analyze_match_v2.py --n 6
|
||||||
|
python scripts/analyze_match_v2.py --match <match_id>
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, os, sys
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
META = {"match_id","home_team_id","away_team_id","league_id","mst_utc",
|
||||||
|
"score_home","score_away","ht_score_home","ht_score_away"}
|
||||||
|
LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals",
|
||||||
|
"squad_diff","home_squad_quality","away_squad_quality",
|
||||||
|
"referee_home_bias","referee_avg_goals"}
|
||||||
|
STAKE_LO, STAKE_HI = 1.5, 2.4 # MS favourite band that staking is allowed in
|
||||||
|
STAKE_MARGIN = 0.03
|
||||||
|
|
||||||
|
def ou(line): return lambda sh,sa,hh,ha: (0 if (sh+sa) > line else 1)
|
||||||
|
def htou(line): return lambda sh,sa,hh,ha: (None if np.isnan(hh) else (0 if (hh+ha) > line else 1))
|
||||||
|
MARKETS = {
|
||||||
|
"MS": ("multi", ["odds_ms_h","odds_ms_d","odds_ms_a"], ["1","X","2"],
|
||||||
|
lambda sh,sa,hh,ha: 0 if sh>sa else (1 if sh==sa else 2)),
|
||||||
|
"OU25": ("binary",["odds_ou25_o","odds_ou25_u"], ["2.5Üst","2.5Alt"], ou(2.5)),
|
||||||
|
"OU15": ("binary",["odds_ou15_o","odds_ou15_u"], ["1.5Üst","1.5Alt"], ou(1.5)),
|
||||||
|
"OU35": ("binary",["odds_ou35_o","odds_ou35_u"], ["3.5Üst","3.5Alt"], ou(3.5)),
|
||||||
|
"BTTS": ("binary",["odds_btts_y","odds_btts_n"], ["KG Var","KG Yok"],
|
||||||
|
lambda sh,sa,hh,ha: 0 if (sh>0 and sa>0) else 1),
|
||||||
|
"HT": ("multi", ["odds_ht_ms_h","odds_ht_ms_d","odds_ht_ms_a"], ["İY1","İYX","İY2"],
|
||||||
|
lambda sh,sa,hh,ha: None if np.isnan(hh) else (0 if hh>ha else (1 if hh==ha else 2))),
|
||||||
|
"HT_OU15": ("binary",["odds_ht_ou15_o","odds_ht_ou15_u"], ["İY1.5Üst","İY1.5Alt"], htou(1.5)),
|
||||||
|
}
|
||||||
|
STAKED_MARKETS = {"MS"} # only these are bet; rest are info-only
|
||||||
|
PM = {"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05,"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
PB = {"objective":"binary:logistic","max_depth":5,"eta":0.05,"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--n", type=int, default=6, help="how many sample matches")
|
||||||
|
ap.add_argument("--match", help="specific match_id")
|
||||||
|
ap.add_argument("--estimators", type=int, default=250)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
df = pd.read_csv(CSV, low_memory=False).sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh = pd.to_numeric(df["score_home"],errors="coerce"); sa = pd.to_numeric(df["score_away"],errors="coerce")
|
||||||
|
ok = sh.notna()&sa.notna(); df = df[ok].reset_index(drop=True)
|
||||||
|
SH=sh[ok.values].values.astype(float); SA=sa[ok.values].values.astype(float)
|
||||||
|
HH=pd.to_numeric(df["ht_score_home"],errors="coerce").values.astype(float)
|
||||||
|
HA=pd.to_numeric(df["ht_score_away"],errors="coerce").values.astype(float)
|
||||||
|
feats=[c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY]
|
||||||
|
X=df[feats].apply(pd.to_numeric,errors="coerce").fillna(0.0).values
|
||||||
|
N=len(df); cut=int(N*0.85)
|
||||||
|
print(f"Training {len(MARKETS)} leak-free market models on {cut:,} matches ...")
|
||||||
|
|
||||||
|
models={}
|
||||||
|
for m,(kind,ocols,picks,tfn) in MARKETS.items():
|
||||||
|
if not all(c in df.columns for c in ocols): continue
|
||||||
|
truth=np.array([tfn(SH[i],SA[i],HH[i],HA[i]) for i in range(cut)],dtype=object)
|
||||||
|
valid=np.array([v is not None for v in truth])
|
||||||
|
if kind=="multi":
|
||||||
|
b=xgb.train(PM,xgb.DMatrix(X[:cut][valid],label=truth[valid].astype(int)),num_boost_round=args.estimators)
|
||||||
|
else:
|
||||||
|
b=xgb.train(PB,xgb.DMatrix(X[:cut][valid],label=(truth[valid].astype(int)==0).astype(int)),num_boost_round=args.estimators)
|
||||||
|
models[m]=(kind,ocols,picks,tfn,b)
|
||||||
|
|
||||||
|
# choose matches from holdout
|
||||||
|
hold = df.iloc[cut:].reset_index(drop=True)
|
||||||
|
if args.match:
|
||||||
|
sel_idx = df.index[df["match_id"].astype(str)==str(args.match)].tolist()
|
||||||
|
rows = [(i,) for i in sel_idx]
|
||||||
|
base = df
|
||||||
|
else:
|
||||||
|
pick_pos = np.linspace(0, len(hold)-1, args.n, dtype=int)
|
||||||
|
rows = [(cut+p,) for p in pick_pos]
|
||||||
|
base = df
|
||||||
|
|
||||||
|
for (gi,) in rows:
|
||||||
|
r = base.iloc[gi]
|
||||||
|
xrow = X[gi:gi+1]
|
||||||
|
sh_,sa_,hh_,ha_ = SH[gi],SA[gi],HH[gi],HA[gi]
|
||||||
|
ht = f"{int(hh_)}-{int(ha_)}" if not np.isnan(hh_) else "?"
|
||||||
|
print("\n"+"="*72)
|
||||||
|
print(f"MATCH {r['match_id']} | elo H{r.get('home_overall_elo','?'):.0f} vs A{r.get('away_overall_elo','?'):.0f}"
|
||||||
|
f" | ACTUAL {int(sh_)}-{int(sa_)} (HT {ht})")
|
||||||
|
print(f" {'market':<8}{'pick':<10}{'model%':>8}{'impl%':>7}{'edge':>7}{'odds':>7} flag result")
|
||||||
|
print(" "+"-"*64)
|
||||||
|
best_ms=None
|
||||||
|
for m,(kind,ocols,picks,tfn,b) in models.items():
|
||||||
|
if kind=="multi":
|
||||||
|
P=b.predict(xgb.DMatrix(xrow))[0]
|
||||||
|
else:
|
||||||
|
p=float(b.predict(xgb.DMatrix(xrow))[0]); P=np.array([p,1-p])
|
||||||
|
O=pd.to_numeric(r[ocols],errors="coerce").fillna(0.0).values
|
||||||
|
truth=tfn(sh_,sa_,hh_,ha_)
|
||||||
|
for k in range(len(picks)):
|
||||||
|
o=O[k]
|
||||||
|
if o<=1.0: continue
|
||||||
|
imp=1.0/o; edge=P[k]-imp
|
||||||
|
res = "—" if truth is None else ("WON" if truth==k else "lost")
|
||||||
|
staked = (m in STAKED_MARKETS) and edge>STAKE_MARGIN and STAKE_LO<=o<STAKE_HI
|
||||||
|
flag = "★BET" if staked else ("val" if edge>STAKE_MARGIN else "")
|
||||||
|
print(f" {m:<8}{picks[k]:<10}{100*P[k]:>7.1f}{100*imp:>7.1f}{100*edge:>+7.1f}{o:>7.2f} {flag:<5} {res}")
|
||||||
|
if staked and (best_ms is None or edge>best_ms[0]):
|
||||||
|
best_ms=(edge,m,picks[k],o,res)
|
||||||
|
print(" "+"-"*64)
|
||||||
|
if best_ms:
|
||||||
|
e,m,p,o,res = best_ms
|
||||||
|
print(f" >>> STAKE: {m} {p} @ {o:.2f} (edge +{100*e:.1f}%, favourite band) -> {res}")
|
||||||
|
else:
|
||||||
|
print(f" >>> NO STAKE: no MS value in favourite band. (Other markets info-only —")
|
||||||
|
print(f" their 'value' is model error in efficient markets; do NOT chase it.)")
|
||||||
|
print("\nNOTE: only MS staked (proven edge). All markets shown for transparency.")
|
||||||
|
print("Forward-validate with CLV before real money. Static CSV odds may overstate edge.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
"""
|
||||||
|
Betting Policy — the honest, leak-free strategy the data actually supports.
|
||||||
|
==========================================================================
|
||||||
|
Everything else in this repo bet UNDERDOGS (odds 6-7.5) and lost (-43.7% live).
|
||||||
|
The data says the opposite: the only positive, fold-consistent, model-driven
|
||||||
|
signal is MILD FAVOURITES the model rates above the market price.
|
||||||
|
|
||||||
|
POLICY (MS / 1X2 only):
|
||||||
|
* leak-free model (drops the result-encoding features, see LEAKY)
|
||||||
|
* bet the model's single biggest value edge (model_prob - implied) ...
|
||||||
|
* ONLY if the picked side's odds are in [--lo, --hi] (favourite band)
|
||||||
|
* ONLY if that edge > --margin
|
||||||
|
* flat 1u stake, one bet per match, never a longshot, never a parlay.
|
||||||
|
|
||||||
|
Walk-forward, no leakage. Reports the policy ROI, fold consistency, drawdown,
|
||||||
|
and the model-free baseline (blind favourite) so you can see the model's lift.
|
||||||
|
|
||||||
|
⚠️ HONEST CAVEAT: CSV odds are a static capture, not the verified obtainable
|
||||||
|
closing line. A small backtest edge here is a LEAD, not a guarantee. Forward
|
||||||
|
paper-trade with real CLV (capture_closing_odds.py) before risking money.
|
||||||
|
|
||||||
|
Usage: python scripts/betting_policy.py --lo 1.5 --hi 2.2 --margin 0.0 --folds 8
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, os, sys
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
META = {"match_id","home_team_id","away_team_id","league_id","mst_utc",
|
||||||
|
"score_home","score_away","ht_score_home","ht_score_away"}
|
||||||
|
LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals",
|
||||||
|
"squad_diff","home_squad_quality","away_squad_quality",
|
||||||
|
"referee_home_bias","referee_avg_goals"}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--lo", type=float, default=1.5)
|
||||||
|
ap.add_argument("--hi", type=float, default=2.2)
|
||||||
|
ap.add_argument("--margin", type=float, default=0.0)
|
||||||
|
ap.add_argument("--folds", type=int, default=8)
|
||||||
|
ap.add_argument("--estimators", type=int, default=250)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
df = pd.read_csv(CSV, low_memory=False).sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh = pd.to_numeric(df["score_home"], errors="coerce")
|
||||||
|
sa = pd.to_numeric(df["score_away"], errors="coerce")
|
||||||
|
ok = sh.notna() & sa.notna()
|
||||||
|
df, sh, sa = df[ok].reset_index(drop=True), sh[ok.values].values, sa[ok.values].values
|
||||||
|
y = np.where(sh > sa, 0, np.where(sh == sa, 1, 2))
|
||||||
|
O = df[["odds_ms_h","odds_ms_d","odds_ms_a"]].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
feats = [c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY]
|
||||||
|
X = df[feats].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
|
||||||
|
n = len(df); start = int(n*0.5)
|
||||||
|
bounds = np.linspace(start, n, args.folds+1, dtype=int)
|
||||||
|
params = {"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05,
|
||||||
|
"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
|
||||||
|
print(f"POLICY: favourite band [{args.lo},{args.hi}] margin {args.margin} "
|
||||||
|
f"leak-free feats={len(feats)} folds={args.folds}\n")
|
||||||
|
all_pnl=[]; fold_rows=[]; base_pnl=[]
|
||||||
|
for fi in range(args.folds):
|
||||||
|
te0,te1 = bounds[fi], bounds[fi+1]
|
||||||
|
if te1-te0 < 50: continue
|
||||||
|
bst = xgb.train(params, xgb.DMatrix(X[:te0], label=y[:te0]), num_boost_round=args.estimators)
|
||||||
|
P = bst.predict(xgb.DMatrix(X[te0:te1]))
|
||||||
|
yte, Ote = y[te0:te1], O[te0:te1]
|
||||||
|
implied = np.where(Ote>1.0, 1.0/Ote, np.nan)
|
||||||
|
edge = np.where(np.isnan(implied), -9.0, P-implied)
|
||||||
|
pick = edge.argmax(1); pe = edge[np.arange(len(yte)),pick]; po = Ote[np.arange(len(yte)),pick]
|
||||||
|
bet = (pe>args.margin) & (po>=args.lo) & (po<args.hi)
|
||||||
|
win = (pick==yte)&bet
|
||||||
|
pnl = np.where(win, po-1.0, -1.0)[bet]
|
||||||
|
# model-free baseline: blind favourite in same band
|
||||||
|
fav=Ote.argmin(1); fo=Ote[np.arange(len(yte)),fav]
|
||||||
|
bmask=(fo>=args.lo)&(fo<args.hi)&(Ote>1.0).all(1)
|
||||||
|
bpnl=np.where(fav[bmask]==yte[bmask], fo[bmask]-1.0, -1.0)
|
||||||
|
roi = 100*pnl.sum()/len(pnl) if len(pnl) else float('nan')
|
||||||
|
broi= 100*bpnl.sum()/len(bpnl) if len(bpnl) else float('nan')
|
||||||
|
fold_rows.append((fi, len(pnl), 100*win.sum()/max(bet.sum(),1), roi, broi))
|
||||||
|
all_pnl.extend(pnl.tolist()); base_pnl.extend(bpnl.tolist())
|
||||||
|
print(f" fold {fi}: policy_bets={len(pnl):>4} hit={100*win.sum()/max(bet.sum(),1):>5.1f}% "
|
||||||
|
f"ROI={roi:>7.2f}% | baseline(blind fav) ROI={broi:>7.2f}%")
|
||||||
|
|
||||||
|
a=np.array(all_pnl); b=np.array(base_pnl)
|
||||||
|
print("\n"+"="*70)
|
||||||
|
print("AGGREGATE")
|
||||||
|
print("="*70)
|
||||||
|
if len(a):
|
||||||
|
cum=np.cumsum(a); peak=np.maximum.accumulate(cum); dd=(cum-peak).min()
|
||||||
|
folds_pos=sum(1 for r in fold_rows if r[3]>0)
|
||||||
|
print(f" POLICY: bets={len(a):>5} hit={100*(a>0).mean():.1f}% "
|
||||||
|
f"ROI={100*a.mean():+.2f}% net={a.sum():+.1f}u maxDD={dd:.1f}u "
|
||||||
|
f"folds+={folds_pos}/{len(fold_rows)}")
|
||||||
|
if len(b):
|
||||||
|
print(f" BASELINE: bets={len(b):>5} hit={100*(b>0).mean():.1f}% "
|
||||||
|
f"ROI={100*b.mean():+.2f}% (blind favourite, same band)")
|
||||||
|
if len(a):
|
||||||
|
print(f"\n MODEL LIFT over blind favourite: "
|
||||||
|
f"{100*a.mean()-100*b.mean():+.1f} percentage points")
|
||||||
|
print("\nREAD: a believable system has ROI>0, folds+ near full, tolerable maxDD,")
|
||||||
|
print("and clearly beats the blind-favourite baseline. Even then it's a LEAD —")
|
||||||
|
print("forward paper-trade with real CLV before staking real money.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""
|
||||||
|
Calibration Report — are the model's probabilities "kusursuz"?
|
||||||
|
=============================================================
|
||||||
|
"Flawless probability" has a precise technical meaning: CALIBRATION. When the
|
||||||
|
model says 60%, the event must happen ~60% of the time. This measures exactly
|
||||||
|
that for the leak-free MS (1X2) model, and shows how much isotonic calibration
|
||||||
|
improves it.
|
||||||
|
|
||||||
|
Metrics:
|
||||||
|
* Reliability table: bin predicted prob -> avg predicted vs ACTUAL frequency.
|
||||||
|
Calibrated = avg_pred ≈ actual in every bin (gap ≈ 0).
|
||||||
|
* ECE (Expected Calibration Error): weighted mean |pred - actual|. Lower=better.
|
||||||
|
* Brier score, Log-loss: overall probability accuracy. Lower=better.
|
||||||
|
|
||||||
|
Time-split (no leakage): train 70% -> fit isotonic on next 15% -> test last 15%.
|
||||||
|
|
||||||
|
Usage: python scripts/calibration_report.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import os, sys
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
from sklearn.isotonic import IsotonicRegression
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
META = {"match_id","home_team_id","away_team_id","league_id","mst_utc",
|
||||||
|
"score_home","score_away","ht_score_home","ht_score_away"}
|
||||||
|
LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals",
|
||||||
|
"squad_diff","home_squad_quality","away_squad_quality",
|
||||||
|
"referee_home_bias","referee_avg_goals"}
|
||||||
|
PARAMS = {"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05,
|
||||||
|
"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
|
||||||
|
|
||||||
|
def reliability(probs, y, nbins=10):
|
||||||
|
"""Pool one-vs-rest predictions; bin by predicted prob; compare to actual freq."""
|
||||||
|
P = probs.reshape(-1)
|
||||||
|
hit = np.zeros((len(y), probs.shape[1]))
|
||||||
|
hit[np.arange(len(y)), y] = 1.0
|
||||||
|
H = hit.reshape(-1)
|
||||||
|
edges = np.linspace(0, 1, nbins + 1)
|
||||||
|
rows, ece, N = [], 0.0, len(P)
|
||||||
|
for i in range(nbins):
|
||||||
|
lo, hi = edges[i], edges[i+1]
|
||||||
|
m = (P >= lo) & (P < hi) if i < nbins-1 else (P >= lo) & (P <= hi)
|
||||||
|
if m.sum() == 0:
|
||||||
|
continue
|
||||||
|
ap, af, n = P[m].mean(), H[m].mean(), int(m.sum())
|
||||||
|
rows.append((f"{int(lo*100)}-{int(hi*100)}%", n, ap, af, af-ap))
|
||||||
|
ece += (n / N) * abs(ap - af)
|
||||||
|
return rows, ece
|
||||||
|
|
||||||
|
|
||||||
|
def brier(probs, y):
|
||||||
|
oh = np.zeros_like(probs); oh[np.arange(len(y)), y] = 1.0
|
||||||
|
return float(np.mean(np.sum((probs - oh) ** 2, axis=1)))
|
||||||
|
|
||||||
|
|
||||||
|
def logloss(probs, y):
|
||||||
|
p = np.clip(probs[np.arange(len(y)), y], 1e-9, 1)
|
||||||
|
return float(-np.mean(np.log(p)))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
df = pd.read_csv(CSV, low_memory=False).sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh = pd.to_numeric(df["score_home"], errors="coerce")
|
||||||
|
sa = pd.to_numeric(df["score_away"], errors="coerce")
|
||||||
|
ok = sh.notna() & sa.notna()
|
||||||
|
df, sh, sa = df[ok].reset_index(drop=True), sh[ok.values].values, sa[ok.values].values
|
||||||
|
y = np.where(sh > sa, 0, np.where(sh == sa, 1, 2))
|
||||||
|
feats = [c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY]
|
||||||
|
X = df[feats].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
|
||||||
|
n = len(df); a, b = int(n*0.70), int(n*0.85)
|
||||||
|
Xtr, ytr = X[:a], y[:a]
|
||||||
|
Xca, yca = X[a:b], y[a:b]
|
||||||
|
Xte, yte = X[b:], y[b:]
|
||||||
|
print(f"{n:,} matches | train {len(ytr):,} / calib {len(yca):,} / test {len(yte):,} (time-split)")
|
||||||
|
|
||||||
|
bst = xgb.train(PARAMS, xgb.DMatrix(Xtr, label=ytr), num_boost_round=300)
|
||||||
|
raw_ca = bst.predict(xgb.DMatrix(Xca))
|
||||||
|
raw_te = bst.predict(xgb.DMatrix(Xte))
|
||||||
|
|
||||||
|
# isotonic per class (fit on calib), apply to test, renormalize
|
||||||
|
isos = []
|
||||||
|
for k in range(3):
|
||||||
|
ir = IsotonicRegression(out_of_bounds="clip", y_min=0, y_max=1)
|
||||||
|
ir.fit(raw_ca[:, k], (yca == k).astype(float))
|
||||||
|
isos.append(ir)
|
||||||
|
cal_te = np.column_stack([isos[k].predict(raw_te[:, k]) for k in range(3)])
|
||||||
|
cal_te = np.clip(cal_te, 1e-6, 1)
|
||||||
|
cal_te = cal_te / cal_te.sum(axis=1, keepdims=True)
|
||||||
|
|
||||||
|
for name, P in (("RAW (kalibrasyonsuz)", raw_te), ("ISOTONIC KALİBRELİ", cal_te)):
|
||||||
|
rows, ece = reliability(P, yte)
|
||||||
|
print(f"\n{'='*64}\n{name}\n{'='*64}")
|
||||||
|
print(f" {'tahmin bandı':<12}{'n':>7}{'ort.tahmin':>12}{'gerçek':>9}{'fark':>8}")
|
||||||
|
for band, nn, ap, af, gap in rows:
|
||||||
|
print(f" {band:<12}{nn:>7}{100*ap:>11.1f}%{100*af:>8.1f}%{100*gap:>+7.1f}")
|
||||||
|
print(f" ECE={100*ece:.2f}% Brier={brier(P,yte):.4f} LogLoss={logloss(P,yte):.4f}")
|
||||||
|
|
||||||
|
print("\nOKUMA: 'fark' ≈ 0 ise olasılıklar KUSURSUZ (söylediği %X gerçekten %X).")
|
||||||
|
print("ECE/Brier/LogLoss düştüyse kalibrasyon işe yaradı. Bu kalibre olasılıklar,")
|
||||||
|
print("maçın olası sonuçlarını dürüstçe gösterir — kayıp-minimizasyonun temeli budur.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
"""Calibration scoreboard — "dediğimiz vs olan" karnesi.
|
||||||
|
|
||||||
|
Measures, on settled real-odds matches, how honest the DISPLAYED numbers are:
|
||||||
|
|
||||||
|
1. ANCHORED PIPELINE (what V35 shows): per market (MS 1/X/2, OU2.5, BTTS)
|
||||||
|
reliability buckets — mean stated probability vs actual frequency,
|
||||||
|
plus ECE / Brier per market.
|
||||||
|
2. SCORE CARD (V36): modal-score hit vs stated modal probability, top-5
|
||||||
|
coverage, HT modal hit.
|
||||||
|
3. STORED RUNS: prediction_runs settled per engine_version (the
|
||||||
|
`.sim-finished` buckets — the user's manual finished-match tests — are
|
||||||
|
reported separately and never mixed into the live karne).
|
||||||
|
|
||||||
|
It recomputes the anchored numbers with the SAME modules the engine ships
|
||||||
|
(models/market_anchor.py + models/score_matrix.py), so the scoreboard always
|
||||||
|
grades current pipeline math, not a copy of it.
|
||||||
|
|
||||||
|
DB: uses DATABASE_URL (data/db.py). Reads are gentle: a server-side cursor
|
||||||
|
over an indexed, date-bounded join — never aggregate-scans the giant odds
|
||||||
|
tables (prod runs on a Raspberry Pi).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/calibration_scoreboard.py [--days 365] [--buckets 10]
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
import psycopg2 # noqa: E402
|
||||||
|
from psycopg2.extras import RealDictCursor # noqa: E402
|
||||||
|
|
||||||
|
from data.db import get_clean_dsn # noqa: E402
|
||||||
|
from models.market_anchor import apply_corrections # noqa: E402
|
||||||
|
from models.score_matrix import build_calibrated_score_package # noqa: E402
|
||||||
|
|
||||||
|
REAL_ODDS_MIN_OVERROUND = 0.05 # the user's hard rule: no real odds -> excluded
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_settled_matches(days: int) -> List[Dict[str, Any]]:
|
||||||
|
"""Finished, real-odds matches with stored de-vigged implied probs."""
|
||||||
|
since_ms = int((time.time() - days * 86400) * 1000)
|
||||||
|
sql = """
|
||||||
|
SELECT f.implied_home, f.implied_draw, f.implied_away,
|
||||||
|
f.implied_over25, f.implied_btts_yes, f.odds_overround,
|
||||||
|
m.score_home, m.score_away, m.ht_score_home, m.ht_score_away
|
||||||
|
FROM football_ai_features f
|
||||||
|
JOIN matches m ON m.id = f.match_id
|
||||||
|
WHERE m.sport = 'football'
|
||||||
|
AND m.winner IN ('home', 'away', 'draw')
|
||||||
|
AND m.score_home IS NOT NULL
|
||||||
|
AND f.odds_overround > %s
|
||||||
|
AND m.mst_utc >= %s
|
||||||
|
"""
|
||||||
|
rows: List[Dict[str, Any]] = []
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SET statement_timeout = '120s'")
|
||||||
|
# server-side (named) cursor: streams gently instead of one big fetch
|
||||||
|
with conn.cursor("scoreboard_stream", cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.itersize = 5000
|
||||||
|
cur.execute(sql, (REAL_ODDS_MIN_OVERROUND, since_ms))
|
||||||
|
for r in cur:
|
||||||
|
rows.append(dict(r))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def _anchored_probs(row: Dict[str, Any]) -> Optional[Tuple[float, float, float]]:
|
||||||
|
"""The MS vector the V35 pipeline would display (devig is already done in
|
||||||
|
the stored features; apply the active home-favourite correction)."""
|
||||||
|
try:
|
||||||
|
p1 = float(row["implied_home"]); px = float(row["implied_draw"]); p2 = float(row["implied_away"])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
if not (0.0 < p1 < 1.0 and 0.0 < px < 1.0 and 0.0 < p2 < 1.0):
|
||||||
|
return None
|
||||||
|
if abs(p1 + px + p2 - 1.0) > 0.02: # not a clean de-vigged vector
|
||||||
|
return None
|
||||||
|
return apply_corrections(p1, px, p2)
|
||||||
|
|
||||||
|
|
||||||
|
class Reliability:
|
||||||
|
"""Accumulates (stated probability, outcome) pairs into buckets."""
|
||||||
|
|
||||||
|
def __init__(self, n_buckets: int) -> None:
|
||||||
|
self.n_buckets = n_buckets
|
||||||
|
self.n = defaultdict(int)
|
||||||
|
self.sum_p = defaultdict(float)
|
||||||
|
self.sum_y = defaultdict(int)
|
||||||
|
|
||||||
|
def add(self, p: float, hit: bool) -> None:
|
||||||
|
b = min(self.n_buckets - 1, int(p * self.n_buckets))
|
||||||
|
self.n[b] += 1
|
||||||
|
self.sum_p[b] += p
|
||||||
|
self.sum_y[b] += 1 if hit else 0
|
||||||
|
|
||||||
|
def report(self, title: str) -> Tuple[float, float]:
|
||||||
|
total = sum(self.n.values())
|
||||||
|
if not total:
|
||||||
|
print(f"\n== {title}: no data ==")
|
||||||
|
return 0.0, 0.0
|
||||||
|
ece = 0.0
|
||||||
|
brier_num = 0.0
|
||||||
|
print(f"\n== {title} (n={total}) ==")
|
||||||
|
print(f"{'band':>10} {'n':>8} {'said%':>8} {'actual%':>8} {'gap_pt':>7}")
|
||||||
|
for b in sorted(self.n):
|
||||||
|
n = self.n[b]
|
||||||
|
said = self.sum_p[b] / n
|
||||||
|
act = self.sum_y[b] / n
|
||||||
|
ece += n * abs(said - act)
|
||||||
|
print(f"{b / self.n_buckets:>5.2f}-{(b + 1) / self.n_buckets:<4.2f} "
|
||||||
|
f"{n:>8} {100 * said:>8.1f} {100 * act:>8.1f} {100 * (act - said):>7.1f}")
|
||||||
|
ece /= total
|
||||||
|
# Brier from bucket stats is approximate; recompute exactly elsewhere
|
||||||
|
# if needed. ECE is the headline honesty metric here.
|
||||||
|
print(f"{'ECE':>10}: {100 * ece:.2f}%")
|
||||||
|
return ece, brier_num
|
||||||
|
|
||||||
|
|
||||||
|
def grade_pipeline(rows: List[Dict[str, Any]], n_buckets: int) -> None:
|
||||||
|
ms1 = Reliability(n_buckets); msx = Reliability(n_buckets); ms2 = Reliability(n_buckets)
|
||||||
|
ou = Reliability(n_buckets); btts = Reliability(n_buckets)
|
||||||
|
top1 = top5 = ht1 = 0
|
||||||
|
stated_modal = 0.0
|
||||||
|
n_score = 0
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
anch = _anchored_probs(r)
|
||||||
|
sh, sa = int(r["score_home"]), int(r["score_away"])
|
||||||
|
winner = "home" if sh > sa else "away" if sa > sh else "draw"
|
||||||
|
if anch is not None:
|
||||||
|
p1, px, p2 = anch
|
||||||
|
ms1.add(p1, winner == "home")
|
||||||
|
msx.add(px, winner == "draw")
|
||||||
|
ms2.add(p2, winner == "away")
|
||||||
|
# exactly-0.5 values are DEFAULT FILL for matches without a real OU/BTTS
|
||||||
|
# market (measured: 15,993 of 78k OU rows) — never grade or use them.
|
||||||
|
try:
|
||||||
|
po = float(r["implied_over25"])
|
||||||
|
if po == 0.5 or not (0.05 < po < 0.95):
|
||||||
|
po = None
|
||||||
|
else:
|
||||||
|
ou.add(po, sh + sa >= 3)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
po = None
|
||||||
|
try:
|
||||||
|
pb = float(r["implied_btts_yes"])
|
||||||
|
if pb != 0.5 and 0.05 < pb < 0.95:
|
||||||
|
btts.add(pb, sh > 0 and sa > 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# V36 score card (sampled fully — pure math, no I/O)
|
||||||
|
if anch is not None and po is not None and 0.05 < po < 0.95:
|
||||||
|
pkg = build_calibrated_score_package(*anch, po)
|
||||||
|
actual = f"{min(sh, 10)}-{min(sa, 10)}"
|
||||||
|
n_score += 1
|
||||||
|
stated_modal += float(pkg["scenario_top5"][0]["prob"])
|
||||||
|
if pkg["ft"] == actual:
|
||||||
|
top1 += 1
|
||||||
|
if actual in [d["score"] for d in pkg["scenario_top5"]]:
|
||||||
|
top5 += 1
|
||||||
|
hh, ha = r.get("ht_score_home"), r.get("ht_score_away")
|
||||||
|
if hh is not None and ha is not None and pkg["ht"] == f"{min(int(hh),10)}-{min(int(ha),10)}":
|
||||||
|
ht1 += 1
|
||||||
|
|
||||||
|
ms1.report("MS ev (1) — anchored pipeline")
|
||||||
|
msx.report("MS beraberlik (X) — anchored pipeline")
|
||||||
|
ms2.report("MS deplasman (2) — anchored pipeline")
|
||||||
|
ou.report("Ust/Alt 2.5 (over) — devig")
|
||||||
|
btts.report("KG Var — devig")
|
||||||
|
|
||||||
|
if n_score:
|
||||||
|
print(f"\n== V36 skor karti (n={n_score}) ==")
|
||||||
|
print(f" modal skor isabeti : {100 * top1 / n_score:.1f}% (soylenen: {100 * stated_modal / n_score:.1f}%)")
|
||||||
|
print(f" top-5 kapsama : {100 * top5 / n_score:.1f}%")
|
||||||
|
print(f" IY modal isabeti : {100 * ht1 / n_score:.1f}%")
|
||||||
|
|
||||||
|
|
||||||
|
def grade_stored_runs() -> None:
|
||||||
|
"""Settle prediction_runs main_pick stated probabilities per engine_version.
|
||||||
|
`.sim-finished` buckets (manual finished-match tests) report separately."""
|
||||||
|
sql = """
|
||||||
|
SELECT pr.engine_version,
|
||||||
|
pr.payload_summary->'main_pick'->>'market' AS market,
|
||||||
|
pr.payload_summary->'main_pick'->>'pick' AS pick,
|
||||||
|
COALESCE((pr.payload_summary->'main_pick'->>'calibrated_probability')::float,
|
||||||
|
(pr.payload_summary->'main_pick'->>'probability')::float) AS p,
|
||||||
|
m.score_home AS sh, m.score_away AS sa, m.winner AS w
|
||||||
|
FROM prediction_runs pr
|
||||||
|
JOIN matches m ON m.id = pr.match_id
|
||||||
|
WHERE m.score_home IS NOT NULL
|
||||||
|
AND jsonb_typeof(pr.payload_summary->'main_pick') = 'object'
|
||||||
|
"""
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SET statement_timeout = '60s'")
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(sql)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
def settle(market: str, pick: str, sh: int, sa: int, w: str) -> Optional[bool]:
|
||||||
|
total = sh + sa
|
||||||
|
pick_u = (pick or "").upper()
|
||||||
|
over = "UST" in pick_u.replace("Ü", "U") or "OVER" in pick_u
|
||||||
|
if market == "MS":
|
||||||
|
return {"1": w == "home", "X": w == "draw", "2": w == "away"}.get(pick)
|
||||||
|
if market in ("OU15", "OU25", "OU35"):
|
||||||
|
line = {"OU15": 1.5, "OU25": 2.5, "OU35": 3.5}[market]
|
||||||
|
return total > line if over else total < line
|
||||||
|
if market == "BTTS":
|
||||||
|
yes = "VAR" in pick_u or "YES" in pick_u
|
||||||
|
return (sh > 0 and sa > 0) if yes else not (sh > 0 and sa > 0)
|
||||||
|
return None
|
||||||
|
|
||||||
|
stats: Dict[str, List[Tuple[float, bool]]] = defaultdict(list)
|
||||||
|
for r in rows:
|
||||||
|
if r["p"] is None:
|
||||||
|
continue
|
||||||
|
hit = settle(str(r["market"]), str(r["pick"]), int(r["sh"]), int(r["sa"]), str(r["w"]))
|
||||||
|
if hit is None:
|
||||||
|
continue
|
||||||
|
stats[str(r["engine_version"])].append((float(r["p"]), bool(hit)))
|
||||||
|
|
||||||
|
print("\n== prediction_runs karnesi (main_pick, soylenen vs olan) ==")
|
||||||
|
print(f"{'engine_version':<34} {'n':>5} {'said%':>8} {'actual%':>8}")
|
||||||
|
for ver in sorted(stats):
|
||||||
|
pairs = stats[ver]
|
||||||
|
n = len(pairs)
|
||||||
|
said = sum(p for p, _ in pairs) / n
|
||||||
|
act = sum(1 for _, h in pairs if h) / n
|
||||||
|
tag = " <- test kovasi" if ver.endswith(".sim-finished") else ""
|
||||||
|
print(f"{ver:<34} {n:>5} {100 * said:>8.1f} {100 * act:>8.1f}{tag}")
|
||||||
|
if not stats:
|
||||||
|
print(" (settle edilebilir kayit yok)")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--days", type=int, default=365, help="lookback window (days)")
|
||||||
|
ap.add_argument("--buckets", type=int, default=10)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
t0 = time.time()
|
||||||
|
rows = _fetch_settled_matches(args.days)
|
||||||
|
print(f"settled real-odds matches loaded: {len(rows)} (last {args.days} days, "
|
||||||
|
f"{time.time() - t0:.1f}s)")
|
||||||
|
if rows:
|
||||||
|
grade_pipeline(rows, args.buckets)
|
||||||
|
grade_stored_runs()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
Capture Closing Odds — snapshot #2 of the minimal 2-snapshot CLV system.
|
||||||
|
=======================================================================
|
||||||
|
WHY: CLV (closing line value) is the only reliable proof of betting edge.
|
||||||
|
This codebase never captured it: odds are stored as a single static snapshot
|
||||||
|
and `odds_history` is empty. But the live sync (DataFetcherTask CRON 1) DOES
|
||||||
|
refresh `live_matches.odds` every 15 min before kickoff, and prediction_runs
|
||||||
|
already store the bet-time odds blob (odds_snapshot.odds, source=live_match).
|
||||||
|
|
||||||
|
This script supplies the missing half: just before kickoff it copies the
|
||||||
|
*current* live odds blob onto the match's latest prediction_run as
|
||||||
|
`odds_snapshot.closing_odds`. Later, CLV per bet = bet-time pick odds vs
|
||||||
|
closing pick odds (computed in live_scoreboard.py once enough data exists).
|
||||||
|
|
||||||
|
Run it every ~15 min (e.g. alongside the existing sync, or its own cron):
|
||||||
|
python scripts/capture_closing_odds.py # default 25-min window
|
||||||
|
python scripts/capture_closing_odds.py --window-min 20 --dry-run
|
||||||
|
|
||||||
|
Structure-agnostic: stores the whole live odds blob; no pick parsing here.
|
||||||
|
Idempotent: skips runs that already have closing_odds. Only ADDS a JSON key,
|
||||||
|
never deletes. Safe to run repeatedly.
|
||||||
|
|
||||||
|
⚠️ Needs one supervised test run against a live DB with upcoming matches
|
||||||
|
before scheduling (DB was down at authoring time).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
|
||||||
|
sys.path.insert(0, AI_ENGINE_DIR)
|
||||||
|
|
||||||
|
from data.db import get_clean_dsn # noqa: E402
|
||||||
|
import psycopg2 # noqa: E402
|
||||||
|
from psycopg2.extras import RealDictCursor # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--window-min", type=int, default=25,
|
||||||
|
help="Capture matches kicking off within the next N minutes (default 25)")
|
||||||
|
ap.add_argument("--grace-min", type=int, default=10,
|
||||||
|
help="Also include matches that kicked off up to N min ago (default 10)")
|
||||||
|
ap.add_argument("--dry-run", action="store_true",
|
||||||
|
help="Report what would be captured without writing")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
lo_ms = now_ms - args.grace_min * 60 * 1000
|
||||||
|
hi_ms = now_ms + args.window_min * 60 * 1000
|
||||||
|
|
||||||
|
captured = skipped = no_run = 0
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as conn:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
# Upcoming/just-started live matches that still hold pre-kickoff odds.
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, mst_utc, odds
|
||||||
|
FROM live_matches
|
||||||
|
WHERE odds IS NOT NULL
|
||||||
|
AND mst_utc BETWEEN %s AND %s
|
||||||
|
ORDER BY mst_utc ASC
|
||||||
|
""",
|
||||||
|
(lo_ms, hi_ms),
|
||||||
|
)
|
||||||
|
matches = cur.fetchall()
|
||||||
|
print(f"[capture_closing_odds] window={args.window_min}m grace={args.grace_min}m "
|
||||||
|
f"upcoming_with_odds={len(matches)} dry_run={args.dry_run}")
|
||||||
|
|
||||||
|
for m in matches:
|
||||||
|
mid = m["id"]
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT id, odds_snapshot
|
||||||
|
FROM prediction_runs
|
||||||
|
WHERE match_id = %s
|
||||||
|
ORDER BY generated_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
""",
|
||||||
|
(mid,),
|
||||||
|
)
|
||||||
|
run = cur.fetchone()
|
||||||
|
if not run:
|
||||||
|
no_run += 1
|
||||||
|
continue
|
||||||
|
snap = run["odds_snapshot"] or {}
|
||||||
|
if isinstance(snap, str):
|
||||||
|
try:
|
||||||
|
snap = json.loads(snap)
|
||||||
|
except Exception:
|
||||||
|
snap = {}
|
||||||
|
if snap.get("closing_odds") is not None:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
patch = {
|
||||||
|
"closing_odds": m["odds"],
|
||||||
|
"closing_captured_at": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"closing_mst_utc": m["mst_utc"],
|
||||||
|
"closing_source": "live_match",
|
||||||
|
}
|
||||||
|
if args.dry_run:
|
||||||
|
captured += 1
|
||||||
|
print(f" would capture match={mid} run_id={run['id']} mst_utc={m['mst_utc']}")
|
||||||
|
continue
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
UPDATE prediction_runs
|
||||||
|
SET odds_snapshot = COALESCE(odds_snapshot, '{}'::jsonb) || %s::jsonb
|
||||||
|
WHERE id = %s
|
||||||
|
""",
|
||||||
|
(json.dumps(patch, default=str), run["id"]),
|
||||||
|
)
|
||||||
|
captured += 1
|
||||||
|
if not args.dry_run:
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
print(f"[capture_closing_odds] captured={captured} already_had={skipped} "
|
||||||
|
f"no_prediction_run={no_run}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
"""
|
||||||
|
CLV Report — the single most important edge metric.
|
||||||
|
===================================================
|
||||||
|
Closing Line Value = did we bet at better odds than the market's closing line?
|
||||||
|
Consistently positive CLV is the only reliable proof of a real betting edge;
|
||||||
|
negative CLV means no edge, regardless of short-term wins/losses.
|
||||||
|
|
||||||
|
This codebase stores the BET-TIME odds for ~92% of runs (prediction_runs.
|
||||||
|
odds_snapshot.source = 'live_match' with the live odds blob, and the pick's
|
||||||
|
odds in payload main_pick.odds). For the closing line we use, in order:
|
||||||
|
1. odds_snapshot.closing_odds (captured by capture_closing_odds.py, forward)
|
||||||
|
2. odd_selections current value (the static near-final capture — a proxy)
|
||||||
|
|
||||||
|
CLV per bet = bet_odds / closing_odds - 1 (positive = beat the close = good).
|
||||||
|
|
||||||
|
Read-only. SELECT only.
|
||||||
|
Usage:
|
||||||
|
python scripts/clv_report.py
|
||||||
|
python scripts/clv_report.py --staked-only
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Dict, Optional, Tuple
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
|
||||||
|
sys.path.insert(0, AI_ENGINE_DIR)
|
||||||
|
|
||||||
|
from data.db import get_clean_dsn # noqa: E402
|
||||||
|
import psycopg2 # noqa: E402
|
||||||
|
from psycopg2.extras import RealDictCursor # noqa: E402
|
||||||
|
|
||||||
|
# market code -> (Turkish odds-category name, pick-normalizer -> selection key)
|
||||||
|
OU_CATS = {"OU05": "0,5 Alt/Üst", "OU15": "1,5 Alt/Üst", "OU25": "2,5 Alt/Üst",
|
||||||
|
"OU35": "3,5 Alt/Üst", "OU45": "4,5 Alt/Üst"}
|
||||||
|
|
||||||
|
|
||||||
|
def _f(x: Any, d: Optional[float] = None) -> Optional[float]:
|
||||||
|
try:
|
||||||
|
return float(x) if x is not None else d
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(j: Any) -> Dict[str, Any]:
|
||||||
|
if isinstance(j, str):
|
||||||
|
try:
|
||||||
|
return json.loads(j)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return j or {}
|
||||||
|
|
||||||
|
|
||||||
|
def map_pick(market: str, pick: str) -> Optional[Tuple[str, str]]:
|
||||||
|
"""Return (category_name, selection_key) for the live-odds JSON / odd_selections."""
|
||||||
|
m = (market or "").upper()
|
||||||
|
p = (pick or "").strip()
|
||||||
|
pl = p.casefold()
|
||||||
|
if m in ("MS", "ML", "1X2"):
|
||||||
|
return ("Maç Sonucu", p if p in ("1", "X", "2") else None) if p in ("1", "X", "2") else None
|
||||||
|
if m == "HT":
|
||||||
|
return ("1. Yarı Sonucu", p) if p in ("1", "X", "2") else None
|
||||||
|
if m in OU_CATS:
|
||||||
|
if "üst" in pl or "ust" in pl or "over" in pl:
|
||||||
|
return (OU_CATS[m], "Üst")
|
||||||
|
if "alt" in pl or "under" in pl:
|
||||||
|
return (OU_CATS[m], "Alt")
|
||||||
|
return None
|
||||||
|
if m == "DC":
|
||||||
|
key = p.upper().replace(" ", "").replace("/", "-")
|
||||||
|
norm = {"1X": "1-X", "X1": "1-X", "X2": "X-2", "2X": "X-2",
|
||||||
|
"12": "1-2", "21": "1-2", "1-X": "1-X", "X-2": "X-2", "1-2": "1-2"}.get(key)
|
||||||
|
return ("Çifte Şans", norm) if norm else None
|
||||||
|
if m == "BTTS":
|
||||||
|
if "var" in pl or "yes" in pl:
|
||||||
|
return ("Karşılıklı Gol", "Var")
|
||||||
|
if "yok" in pl or "no" in pl:
|
||||||
|
return ("Karşılıklı Gol", "Yok")
|
||||||
|
return None
|
||||||
|
if m == "OE":
|
||||||
|
if "tek" in pl or "odd" in pl:
|
||||||
|
return ("Tek/Çift", "Tek")
|
||||||
|
if "çift" in pl or "cift" in pl or "even" in pl:
|
||||||
|
return ("Tek/Çift", "Çift")
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def closing_from_blob(blob: Any, cat: str, sel: str) -> Optional[float]:
|
||||||
|
blob = _parse(blob)
|
||||||
|
cat_map = blob.get(cat) if isinstance(blob, dict) else None
|
||||||
|
if isinstance(cat_map, dict):
|
||||||
|
return _f(cat_map.get(sel))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--staked-only", action="store_true",
|
||||||
|
help="Only playable/staked bets (default: all picks with a mappable market)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
rows_out = []
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as conn:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT match_id, engine_version, odds_snapshot, payload_summary,
|
||||||
|
eventual_outcome, unit_profit
|
||||||
|
FROM prediction_runs
|
||||||
|
WHERE odds_snapshot->>'source' = 'live_match'
|
||||||
|
ORDER BY generated_at ASC
|
||||||
|
""")
|
||||||
|
runs = cur.fetchall()
|
||||||
|
|
||||||
|
for r in runs:
|
||||||
|
snap = _parse(r["odds_snapshot"])
|
||||||
|
ps = _parse(r["payload_summary"])
|
||||||
|
mp = ps.get("main_pick") or {}
|
||||||
|
market = mp.get("market")
|
||||||
|
pick = mp.get("pick")
|
||||||
|
bet_odds = _f(mp.get("odds"))
|
||||||
|
playable = bool(mp.get("playable"))
|
||||||
|
if args.staked_only and not playable:
|
||||||
|
continue
|
||||||
|
if not market or not pick or not bet_odds or bet_odds <= 1.0:
|
||||||
|
continue
|
||||||
|
mapped = map_pick(market, pick)
|
||||||
|
if not mapped or not mapped[1]:
|
||||||
|
continue
|
||||||
|
cat, sel = mapped
|
||||||
|
|
||||||
|
# closing line: prefer captured closing_odds, else static odd_selections
|
||||||
|
closing = closing_from_blob(snap.get("closing_odds"), cat, sel)
|
||||||
|
src = "captured"
|
||||||
|
if closing is None:
|
||||||
|
cur.execute("""
|
||||||
|
SELECT os.odd_value FROM odd_categories oc
|
||||||
|
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
|
||||||
|
WHERE oc.match_id = %s AND oc.name = %s AND os.name = %s
|
||||||
|
LIMIT 1
|
||||||
|
""", (r["match_id"], cat, sel))
|
||||||
|
row = cur.fetchone()
|
||||||
|
closing = _f(row["odd_value"]) if row else None
|
||||||
|
src = "static_proxy"
|
||||||
|
if closing is None or closing <= 1.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
clv = bet_odds / closing - 1.0
|
||||||
|
rows_out.append({
|
||||||
|
"market": market, "playable": playable,
|
||||||
|
"bet_odds": bet_odds, "closing": closing, "clv": clv,
|
||||||
|
"src": src, "profit": _f(r["unit_profit"], 0.0) or 0.0,
|
||||||
|
"settled": r["eventual_outcome"] is not None
|
||||||
|
and not str(r["eventual_outcome"]).startswith("NO_BET"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if not rows_out:
|
||||||
|
print("No mappable runs with both bet-time and closing odds found.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def agg(rs):
|
||||||
|
n = len(rs)
|
||||||
|
clvs = [x["clv"] for x in rs]
|
||||||
|
pos = sum(1 for c in clvs if c > 0)
|
||||||
|
return {
|
||||||
|
"n": n,
|
||||||
|
"mean_clv_pct": round(100.0 * sum(clvs) / n, 2),
|
||||||
|
"pct_positive": round(100.0 * pos / n, 1),
|
||||||
|
"captured": sum(1 for x in rs if x["src"] == "captured"),
|
||||||
|
}
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("CLV REPORT — did we beat the closing line? (the edge compass)")
|
||||||
|
print("=" * 70)
|
||||||
|
o = agg(rows_out)
|
||||||
|
print(f"runs analyzed: {o['n']} (closing source: {o['captured']} captured, "
|
||||||
|
f"{o['n'] - o['captured']} static-proxy)")
|
||||||
|
print(f"\nOVERALL mean CLV: {o['mean_clv_pct']}% "
|
||||||
|
f"bets beating close: {o['pct_positive']}%")
|
||||||
|
print(" (positive mean CLV = real edge; ~0 or negative = no edge)\n")
|
||||||
|
|
||||||
|
staked = [x for x in rows_out if x["playable"]]
|
||||||
|
if staked:
|
||||||
|
s = agg(staked)
|
||||||
|
print(f"STAKED only: n={s['n']} mean CLV={s['mean_clv_pct']}% "
|
||||||
|
f"beating close={s['pct_positive']}%\n")
|
||||||
|
|
||||||
|
print("BY MARKET")
|
||||||
|
by_m = defaultdict(list)
|
||||||
|
for x in rows_out:
|
||||||
|
by_m[x["market"]].append(x)
|
||||||
|
for m, rs in sorted(by_m.items(), key=lambda kv: -len(kv[1])):
|
||||||
|
a = agg(rs)
|
||||||
|
print(f" {m:<8} n={a['n']:>4} mean CLV={a['mean_clv_pct']:>7}% "
|
||||||
|
f"beating close={a['pct_positive']:>5}%")
|
||||||
|
|
||||||
|
# CLV vs outcome sanity: do positive-CLV bets actually win more / lose less?
|
||||||
|
print("\nCLV vs realized P/L (settled staked)")
|
||||||
|
ss = [x for x in rows_out if x["playable"] and x["settled"]]
|
||||||
|
if ss:
|
||||||
|
posc = [x for x in ss if x["clv"] > 0]
|
||||||
|
negc = [x for x in ss if x["clv"] <= 0]
|
||||||
|
for label, grp in (("CLV>0", posc), ("CLV<=0", negc)):
|
||||||
|
if grp:
|
||||||
|
pr = sum(x["profit"] for x in grp)
|
||||||
|
print(f" {label:<7} n={len(grp):>3} profit={pr:>7.2f}u "
|
||||||
|
f"ROI(flat1u)={round(100*pr/len(grp),1)}%")
|
||||||
|
print("=" * 70)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""
|
||||||
|
Compare two diagnostic_backtest CSV outputs side-by-side.
|
||||||
|
Used to validate that a filter change actually improved ROI vs the
|
||||||
|
baseline run — and to detect overfitting (in-sample success but
|
||||||
|
out-of-sample collapse).
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/compare_backtests.py <baseline.csv> <validation.csv>
|
||||||
|
python scripts/compare_backtests.py (auto-picks 2 most recent CSVs)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys, os, glob
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
REPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "reports")
|
||||||
|
|
||||||
|
|
||||||
|
def load(path: str) -> pd.DataFrame:
|
||||||
|
df = pd.read_csv(path)
|
||||||
|
df["won_bool"] = df["won"].map(
|
||||||
|
{True: True, False: False, "True": True, "False": False, 1: True, 0: False}
|
||||||
|
)
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def stats(df: pd.DataFrame, mask=None) -> Dict:
|
||||||
|
if mask is not None:
|
||||||
|
df = df[mask]
|
||||||
|
playable = df[(df["playable"] == True) & (df["won_bool"].notna())]
|
||||||
|
if len(playable) == 0:
|
||||||
|
return {"n_total": len(df), "n_playable": 0, "hit": 0, "profit": 0,
|
||||||
|
"staked": 0, "roi": 0}
|
||||||
|
wins = playable["won_bool"].sum()
|
||||||
|
profit = playable["unit_profit"].sum()
|
||||||
|
staked = playable["stake_units"].sum()
|
||||||
|
return {
|
||||||
|
"n_total": int(len(df)),
|
||||||
|
"n_playable": int(len(playable)),
|
||||||
|
"wins": int(wins),
|
||||||
|
"losses": int(len(playable) - wins),
|
||||||
|
"hit": round(100.0 * wins / len(playable), 2),
|
||||||
|
"profit": round(profit, 2),
|
||||||
|
"staked": round(staked, 2),
|
||||||
|
"roi": round(100.0 * profit / staked, 2) if staked else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def line(label: str, a: Dict, b: Dict, suffix: str = ""):
|
||||||
|
fields = ["n_total", "n_playable", "hit", "profit", "staked", "roi"]
|
||||||
|
parts = [f"{label:<28}"]
|
||||||
|
for f in fields:
|
||||||
|
va = a.get(f, "-")
|
||||||
|
vb = b.get(f, "-")
|
||||||
|
parts.append(f"{f}: {str(va):>8} → {str(vb):>8}")
|
||||||
|
print(" " + " | ".join(parts) + suffix)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) == 3:
|
||||||
|
a_path, b_path = sys.argv[1], sys.argv[2]
|
||||||
|
else:
|
||||||
|
files = sorted(glob.glob(os.path.join(REPORTS_DIR, "diagnostic_backtest_*.csv")),
|
||||||
|
key=os.path.getmtime, reverse=True)
|
||||||
|
if len(files) < 2:
|
||||||
|
print("Need at least 2 backtest CSVs in reports/. Pass paths manually.")
|
||||||
|
return
|
||||||
|
b_path, a_path = files[0], files[1] # newest first as "validation"
|
||||||
|
|
||||||
|
print(f"Baseline A: {os.path.basename(a_path)}")
|
||||||
|
print(f"Validation B: {os.path.basename(b_path)}")
|
||||||
|
|
||||||
|
a = load(a_path)
|
||||||
|
b = load(b_path)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 100}")
|
||||||
|
print(f" OVERALL")
|
||||||
|
print(f"{'=' * 100}")
|
||||||
|
line("ALL", stats(a), stats(b))
|
||||||
|
|
||||||
|
print(f"\n{'─' * 100}")
|
||||||
|
print(f" PER MARKET")
|
||||||
|
print(f"{'─' * 100}")
|
||||||
|
markets = sorted(set(a["market"].dropna().unique()) | set(b["market"].dropna().unique()))
|
||||||
|
for m in markets:
|
||||||
|
line(f"market={m}",
|
||||||
|
stats(a, a["market"] == m),
|
||||||
|
stats(b, b["market"] == m))
|
||||||
|
|
||||||
|
# New veto family check — did MUTED_MARKETS actually mute?
|
||||||
|
print(f"\n{'─' * 100}")
|
||||||
|
print(f" NEW VETO IMPACT (look for new veto names in betting_brain.vetoes)")
|
||||||
|
print(f"{'─' * 100}")
|
||||||
|
new_vetoes = ["market_muted_by_backtest", "negative_ev_edge", "ev_edge_too_high_trap",
|
||||||
|
"outside_envelope_edge_low", "outside_envelope_edge_high",
|
||||||
|
"outside_envelope_odds_low", "outside_envelope_v27_must_agree"]
|
||||||
|
for veto in new_vetoes:
|
||||||
|
a_hits = a["bb_vetoes"].fillna("").str.contains(veto).sum()
|
||||||
|
b_hits = b["bb_vetoes"].fillna("").str.contains(veto).sum()
|
||||||
|
print(f" {veto:<45} A={a_hits:>4} B={b_hits:>4}")
|
||||||
|
|
||||||
|
# Top issue tags
|
||||||
|
print(f"\n{'─' * 100}")
|
||||||
|
print(f" BTTS MUTE CHECK — should be ~0 playable in validation")
|
||||||
|
print(f"{'─' * 100}")
|
||||||
|
a_btts_play = ((a["market"] == "BTTS") & (a["playable"] == True)).sum()
|
||||||
|
b_btts_play = ((b["market"] == "BTTS") & (b["playable"] == True)).sum()
|
||||||
|
print(f" BTTS playable bets: A={a_btts_play} → B={b_btts_play} "
|
||||||
|
f"(should be 0 in B if MUTE works)")
|
||||||
|
|
||||||
|
# Verdict
|
||||||
|
print(f"\n{'=' * 100}")
|
||||||
|
a_s = stats(a)
|
||||||
|
b_s = stats(b)
|
||||||
|
roi_delta = b_s["roi"] - a_s["roi"]
|
||||||
|
if b_s["n_playable"] < 20:
|
||||||
|
verdict = "TOO FEW BETS — sample insufficient"
|
||||||
|
elif roi_delta > 5 and b_s["roi"] > 0:
|
||||||
|
verdict = "✅ FILTERS WORK — ROI improved AND positive"
|
||||||
|
elif roi_delta > 5:
|
||||||
|
verdict = "🟡 PARTIAL — ROI improved but still negative"
|
||||||
|
elif roi_delta > 0:
|
||||||
|
verdict = "🟡 SLIGHT IMPROVEMENT"
|
||||||
|
elif roi_delta < -5:
|
||||||
|
verdict = "❌ OVERFITTING — validation ROI collapsed"
|
||||||
|
else:
|
||||||
|
verdict = "❌ NO MATERIAL CHANGE"
|
||||||
|
print(f" VERDICT: {verdict}")
|
||||||
|
print(f" ROI: {a_s['roi']}% → {b_s['roi']}% (Δ {roi_delta:+.2f}pp)")
|
||||||
|
print(f"{'=' * 100}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,738 @@
|
|||||||
|
"""
|
||||||
|
Diagnostic Backtest
|
||||||
|
===================
|
||||||
|
Run the full V28 orchestrator (in-process — no HTTP) on a window of completed
|
||||||
|
matches, capture the recommendation + key signal features + the actual outcome,
|
||||||
|
and produce a *diagnostic* report: not just "what was the hit rate" but
|
||||||
|
"which feature clusters drive the losing bets".
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
- reports/diagnostic_backtest_YYYYMMDD.csv (per-bet detail)
|
||||||
|
- reports/diagnostic_backtest_YYYYMMDD.json (aggregate metrics)
|
||||||
|
- reports/diagnostic_backtest_YYYYMMDD.txt (human-readable summary)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/diagnostic_backtest.py --days 14 --max-matches 2000
|
||||||
|
python scripts/diagnostic_backtest.py --start 2026-05-10 --end 2026-05-24
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
from collections import defaultdict, Counter
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
|
||||||
|
# Path bootstrap so we can import the ai-engine package from anywhere
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
|
||||||
|
sys.path.insert(0, AI_ENGINE_DIR)
|
||||||
|
|
||||||
|
from data.db import get_clean_dsn
|
||||||
|
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||||
|
|
||||||
|
REPORTS_DIR = os.path.join(AI_ENGINE_DIR, "reports")
|
||||||
|
os.makedirs(REPORTS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
# Days with confirmed feeder gaps — exclude from sample
|
||||||
|
EXCLUDED_DATES = {"2026-05-03", "2026-04-29"}
|
||||||
|
|
||||||
|
|
||||||
|
# ── Outcome resolution ────────────────────────────────────────────────
|
||||||
|
def _norm_pick(pick: Optional[str]) -> str:
|
||||||
|
return str(pick or "").strip().casefold()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_outcome(market: str, pick: str, sh: int, sa: int,
|
||||||
|
htsh: Optional[int], htsa: Optional[int]) -> Optional[bool]:
|
||||||
|
"""Mirror of prediction-settlement.market-resolver.ts (TS side).
|
||||||
|
Returns True/False on settle, None if cannot resolve."""
|
||||||
|
m = (market or "").upper().replace(" ", "").replace("-", "_")
|
||||||
|
p = _norm_pick(pick)
|
||||||
|
|
||||||
|
if m in ("MS", "ML", "1X2"):
|
||||||
|
outcome = "1" if sh > sa else "2" if sa > sh else "x"
|
||||||
|
return p in {outcome, outcome.upper(), outcome.lower(), "0" if outcome == "x" else outcome}
|
||||||
|
|
||||||
|
if m in ("HT", "IY"):
|
||||||
|
if htsh is None or htsa is None:
|
||||||
|
return None
|
||||||
|
outcome = "1" if htsh > htsa else "2" if htsa > htsh else "x"
|
||||||
|
return p in {outcome, "0" if outcome == "x" else outcome}
|
||||||
|
|
||||||
|
if m in ("OU05", "OU15", "OU25", "OU35", "OU45", "TOTAL"):
|
||||||
|
line = {"OU05": 0.5, "OU15": 1.5, "OU25": 2.5, "OU35": 3.5,
|
||||||
|
"OU45": 4.5, "TOTAL": 2.5}[m]
|
||||||
|
total = sh + sa
|
||||||
|
if total == line:
|
||||||
|
return None
|
||||||
|
is_over = total > line
|
||||||
|
if "over" in p or "üst" in p or "ust" in p:
|
||||||
|
return is_over
|
||||||
|
if "alt" in p or "under" in p:
|
||||||
|
return not is_over
|
||||||
|
return None
|
||||||
|
|
||||||
|
if m in ("OU05_HT", "OU15_HT", "OU25_HT", "HT_OU05", "HT_OU15", "HT_OU25"):
|
||||||
|
if htsh is None or htsa is None:
|
||||||
|
return None
|
||||||
|
line = {"OU05_HT": 0.5, "OU15_HT": 1.5, "OU25_HT": 2.5,
|
||||||
|
"HT_OU05": 0.5, "HT_OU15": 1.5, "HT_OU25": 2.5}[m]
|
||||||
|
total = htsh + htsa
|
||||||
|
if total == line:
|
||||||
|
return None
|
||||||
|
is_over = total > line
|
||||||
|
if "over" in p or "üst" in p or "ust" in p:
|
||||||
|
return is_over
|
||||||
|
if "alt" in p or "under" in p:
|
||||||
|
return not is_over
|
||||||
|
return None
|
||||||
|
|
||||||
|
if m in ("BTTS", "KG"):
|
||||||
|
both = sh > 0 and sa > 0
|
||||||
|
if "yes" in p or "var" in p:
|
||||||
|
return both
|
||||||
|
if "no" in p or "yok" in p:
|
||||||
|
return not both
|
||||||
|
return None
|
||||||
|
|
||||||
|
if m in ("HTFT", "IYMS"):
|
||||||
|
if htsh is None or htsa is None or "/" not in p:
|
||||||
|
return None
|
||||||
|
ht_p, ft_p = p.split("/", 1)
|
||||||
|
ht_actual = "1" if htsh > htsa else "2" if htsa > htsh else "x"
|
||||||
|
ft_actual = "1" if sh > sa else "2" if sa > sh else "x"
|
||||||
|
return ht_p.strip() == ht_actual and ft_p.strip() == ft_actual
|
||||||
|
|
||||||
|
if m in ("DC", "CIFTE_SANS"):
|
||||||
|
ft = "1" if sh > sa else "2" if sa > sh else "X"
|
||||||
|
raw = p.upper().replace("-", "").replace("/", "")
|
||||||
|
if raw in ("1X", "X1"):
|
||||||
|
pair = ["1", "X"]
|
||||||
|
elif raw in ("X2", "2X"):
|
||||||
|
pair = ["X", "2"]
|
||||||
|
elif raw in ("12", "21"):
|
||||||
|
pair = ["1", "2"]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
return ft in pair
|
||||||
|
|
||||||
|
if m in ("OE", "TEKCIFT"):
|
||||||
|
is_odd = (sh + sa) % 2 == 1
|
||||||
|
if "tek" in p or "odd" in p:
|
||||||
|
return is_odd
|
||||||
|
if "cift" in p or "çift" in p or "even" in p:
|
||||||
|
return not is_odd
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def compute_unit_profit(won: Optional[bool], stake: float, odds: Optional[float]) -> float:
|
||||||
|
if won is None:
|
||||||
|
return 0.0
|
||||||
|
if not won:
|
||||||
|
return -abs(stake) if stake else -1.0
|
||||||
|
if not odds or odds <= 1.0:
|
||||||
|
return 0.0
|
||||||
|
return round(stake * (odds - 1.0), 4)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Data fetch ────────────────────────────────────────────────────────
|
||||||
|
def fetch_match_window(args) -> List[Dict]:
|
||||||
|
dsn = get_clean_dsn()
|
||||||
|
if "?schema=" in dsn:
|
||||||
|
dsn = dsn.split("?schema=")[0]
|
||||||
|
|
||||||
|
if args.start and args.end:
|
||||||
|
start = datetime.strptime(args.start, "%Y-%m-%d")
|
||||||
|
end = datetime.strptime(args.end, "%Y-%m-%d") + timedelta(days=1)
|
||||||
|
else:
|
||||||
|
end = datetime.now(timezone.utc).replace(tzinfo=None)
|
||||||
|
start = end - timedelta(days=args.days)
|
||||||
|
|
||||||
|
start_ms = int(start.timestamp() * 1000)
|
||||||
|
end_ms = int(end.timestamp() * 1000)
|
||||||
|
|
||||||
|
excluded = sorted(EXCLUDED_DATES)
|
||||||
|
excluded_clause = ""
|
||||||
|
if excluded:
|
||||||
|
ex_csv = ",".join(f"'{d}'" for d in excluded)
|
||||||
|
excluded_clause = (
|
||||||
|
f" AND to_timestamp(mst_utc/1000)::date "
|
||||||
|
f"NOT IN ({ex_csv})"
|
||||||
|
)
|
||||||
|
|
||||||
|
with psycopg2.connect(dsn) as conn:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(
|
||||||
|
f"""
|
||||||
|
SELECT id AS match_id,
|
||||||
|
score_home, score_away,
|
||||||
|
ht_score_home, ht_score_away,
|
||||||
|
league_id,
|
||||||
|
to_timestamp(mst_utc/1000)::date AS match_date
|
||||||
|
FROM matches
|
||||||
|
WHERE sport='football'
|
||||||
|
AND status='FT'
|
||||||
|
AND score_home IS NOT NULL
|
||||||
|
AND score_away IS NOT NULL
|
||||||
|
AND mst_utc >= %s
|
||||||
|
AND mst_utc < %s
|
||||||
|
{excluded_clause}
|
||||||
|
ORDER BY mst_utc DESC
|
||||||
|
LIMIT %s
|
||||||
|
""",
|
||||||
|
(start_ms, end_ms, args.max_matches),
|
||||||
|
)
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
# ── Per-bet capture ───────────────────────────────────────────────────
|
||||||
|
def capture_bet_row(match: Dict, package: Dict) -> Dict[str, Any]:
|
||||||
|
"""Distill orchestrator response + ground truth into one analytic row."""
|
||||||
|
main = package.get("main_pick") or {}
|
||||||
|
bb = main.get("betting_brain") or {}
|
||||||
|
advice = package.get("bet_advice") or {}
|
||||||
|
v27 = package.get("v27_engine") or {}
|
||||||
|
triple = (v27.get("triple_value") or {})
|
||||||
|
risk = package.get("risk") or {}
|
||||||
|
quality = package.get("data_quality") or {}
|
||||||
|
htft_payload = ((package.get("market_board") or {}).get("HTFT") or {})
|
||||||
|
htft_probs = htft_payload.get("probs") or {}
|
||||||
|
|
||||||
|
sh, sa = match["score_home"], match["score_away"]
|
||||||
|
htsh, htsa = match["ht_score_home"], match["ht_score_away"]
|
||||||
|
|
||||||
|
market = main.get("market")
|
||||||
|
pick = main.get("pick")
|
||||||
|
odds_val = _f(main.get("odds"))
|
||||||
|
stake = _f(main.get("stake_units"), 1.0)
|
||||||
|
playable = bool(main.get("playable")) and bool(advice.get("playable"))
|
||||||
|
|
||||||
|
won = resolve_outcome(market, pick, sh, sa, htsh, htsa) if market and pick else None
|
||||||
|
profit = compute_unit_profit(won, stake, odds_val) if playable else 0.0
|
||||||
|
|
||||||
|
# Reversal context (only meaningful for MS picks)
|
||||||
|
rev_prob = None
|
||||||
|
if market == "MS" and pick in ("1", "2"):
|
||||||
|
if pick == "1":
|
||||||
|
rev_prob = _f(htft_probs.get("1/2"), 0.0) + _f(htft_probs.get("1/X"), 0.0)
|
||||||
|
else:
|
||||||
|
rev_prob = _f(htft_probs.get("2/1"), 0.0) + _f(htft_probs.get("2/X"), 0.0)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"match_id": match["match_id"],
|
||||||
|
"match_date": str(match["match_date"]),
|
||||||
|
"league_id": match.get("league_id"),
|
||||||
|
"score_home": sh,
|
||||||
|
"score_away": sa,
|
||||||
|
"ht_score_home": htsh,
|
||||||
|
"ht_score_away": htsa,
|
||||||
|
"market": market,
|
||||||
|
"pick": pick,
|
||||||
|
"odds": odds_val,
|
||||||
|
"stake_units": stake,
|
||||||
|
"playable": playable,
|
||||||
|
"won": won,
|
||||||
|
"unit_profit": profit,
|
||||||
|
"raw_confidence": _f(main.get("raw_confidence")),
|
||||||
|
"calibrated_confidence": _f(main.get("calibrated_confidence")),
|
||||||
|
"play_score": _f(main.get("play_score")),
|
||||||
|
"ev_edge": _f(main.get("ev_edge")),
|
||||||
|
"bet_grade": main.get("bet_grade"),
|
||||||
|
"is_value_sniper": bool(main.get("is_value_sniper")),
|
||||||
|
"bb_score": _f(bb.get("score")),
|
||||||
|
"bb_action": bb.get("action"),
|
||||||
|
"bb_vetoes": ";".join(bb.get("vetoes") or []),
|
||||||
|
"bb_issues": ";".join(bb.get("issues") or []),
|
||||||
|
"bb_positives": ";".join(bb.get("positives") or []),
|
||||||
|
"bb_model_prob": _f(bb.get("model_prob")),
|
||||||
|
"bb_implied_prob": _f(bb.get("implied_prob")),
|
||||||
|
"bb_model_market_gap": _f(bb.get("model_market_gap")),
|
||||||
|
"bb_divergence": _f(bb.get("divergence")),
|
||||||
|
"bb_trap_market": bool(bb.get("trap_market_flag")),
|
||||||
|
"v27_consensus": v27.get("consensus"),
|
||||||
|
"data_quality_score": _f(quality.get("score")),
|
||||||
|
"data_quality_flags": ";".join(quality.get("flags") or []),
|
||||||
|
"risk_level": (risk.get("level") if isinstance(risk, dict) else None),
|
||||||
|
"odds_reliability": _f(main.get("odds_reliability")),
|
||||||
|
"htft_reversal_prob": rev_prob,
|
||||||
|
"htft_top_pick": _argmax(htft_probs),
|
||||||
|
"league_name": (package.get("match_info") or {}).get("league_name"),
|
||||||
|
"is_cup": _is_cup((package.get("match_info") or {}).get("league_name") or ""),
|
||||||
|
"model_version": package.get("model_version"),
|
||||||
|
"decision_reason": main.get("pick_reason") or advice.get("reason"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _f(x: Any, default: Optional[float] = None) -> Optional[float]:
|
||||||
|
try:
|
||||||
|
return float(x) if x is not None else default
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def _argmax(d: Dict[str, Any]) -> Optional[str]:
|
||||||
|
best, val = None, -1.0
|
||||||
|
for k, v in d.items():
|
||||||
|
fv = _f(v, 0.0) or 0.0
|
||||||
|
if fv > val:
|
||||||
|
best, val = k, fv
|
||||||
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
_CUP_KEYWORDS = ("kupa", "cup", "coupe", "copa", "coppa", "pokal", "trophy",
|
||||||
|
"shield", "ziraat", "süper kupa", "super cup", "beker", "taça", "taca")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_cup(name: str) -> bool:
|
||||||
|
n = (name or "").lower()
|
||||||
|
return any(kw in n for kw in _CUP_KEYWORDS)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Aggregation helpers ────────────────────────────────────────────────
|
||||||
|
def _bucket(value: Optional[float], edges: List[float]) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
for i, edge in enumerate(edges):
|
||||||
|
if value < edge:
|
||||||
|
if i == 0:
|
||||||
|
return f"<{edge}"
|
||||||
|
return f"{edges[i-1]}-{edge}"
|
||||||
|
return f">={edges[-1]}"
|
||||||
|
|
||||||
|
|
||||||
|
def _summary_stats(rows: List[Dict]) -> Dict[str, Any]:
|
||||||
|
if not rows:
|
||||||
|
return {"n": 0}
|
||||||
|
settled = [r for r in rows if r["playable"] and r["won"] is not None]
|
||||||
|
won = sum(1 for r in settled if r["won"])
|
||||||
|
lost = sum(1 for r in settled if not r["won"])
|
||||||
|
profit = sum(float(r["unit_profit"]) for r in settled)
|
||||||
|
staked = sum(float(r["stake_units"]) for r in settled)
|
||||||
|
return {
|
||||||
|
"n_total": len(rows),
|
||||||
|
"n_playable_settled": len(settled),
|
||||||
|
"wins": won,
|
||||||
|
"losses": lost,
|
||||||
|
"hit_rate_pct": round(100.0 * won / len(settled), 2) if settled else None,
|
||||||
|
"unit_profit": round(profit, 3),
|
||||||
|
"staked": round(staked, 3),
|
||||||
|
"roi_pct": round(100.0 * profit / staked, 2) if staked else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate(rows: List[Dict]) -> Dict[str, Any]:
|
||||||
|
out: Dict[str, Any] = {"overall": _summary_stats(rows)}
|
||||||
|
|
||||||
|
by = lambda key_fn: defaultdict(list)
|
||||||
|
market_buckets = by(None)
|
||||||
|
conf_buckets = by(None)
|
||||||
|
odds_buckets = by(None)
|
||||||
|
grade_buckets = by(None)
|
||||||
|
cup_buckets = by(None)
|
||||||
|
motivation_buckets = by(None)
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
if r["playable"]:
|
||||||
|
market_buckets[r["market"] or "?"].append(r)
|
||||||
|
conf_buckets[_bucket(r["calibrated_confidence"],
|
||||||
|
[45, 50, 55, 60, 65, 70, 80])].append(r)
|
||||||
|
odds_buckets[_bucket(r["odds"], [1.3, 1.5, 1.8, 2.2, 3.0, 5.0])].append(r)
|
||||||
|
grade_buckets[r["bet_grade"] or "?"].append(r)
|
||||||
|
cup_buckets["cup" if r["is_cup"] else "league"].append(r)
|
||||||
|
|
||||||
|
out["by_market"] = {k: _summary_stats(v) for k, v in market_buckets.items()}
|
||||||
|
out["by_confidence"] = {k: _summary_stats(v) for k, v in conf_buckets.items() if k}
|
||||||
|
out["by_odds"] = {k: _summary_stats(v) for k, v in odds_buckets.items() if k}
|
||||||
|
out["by_grade"] = {k: _summary_stats(v) for k, v in grade_buckets.items()}
|
||||||
|
out["by_competition"] = {k: _summary_stats(v) for k, v in cup_buckets.items()}
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def loss_diagnostics(rows: List[Dict]) -> Dict[str, Any]:
|
||||||
|
losses = [r for r in rows if r["playable"] and r["won"] is False]
|
||||||
|
if not losses:
|
||||||
|
return {"n_losses": 0}
|
||||||
|
n = len(losses)
|
||||||
|
|
||||||
|
def share(predicate) -> Tuple[int, float]:
|
||||||
|
c = sum(1 for r in losses if predicate(r))
|
||||||
|
return c, round(100.0 * c / n, 2)
|
||||||
|
|
||||||
|
diagnostics = {
|
||||||
|
"n_losses": n,
|
||||||
|
"total_loss_units": round(sum(float(r["unit_profit"]) for r in losses), 3),
|
||||||
|
"patterns": {
|
||||||
|
"high_htft_reversal_prob (>=0.20)": share(
|
||||||
|
lambda r: (r.get("htft_reversal_prob") or 0) >= 0.20
|
||||||
|
),
|
||||||
|
"cup_match": share(lambda r: r["is_cup"]),
|
||||||
|
"low_league_reliability (<0.45)": share(
|
||||||
|
lambda r: (r.get("odds_reliability") or 1) < 0.45
|
||||||
|
),
|
||||||
|
"v27_disagree": share(lambda r: r.get("v27_consensus") == "DISAGREE"),
|
||||||
|
"trap_market_flagged": share(lambda r: r.get("bb_trap_market")),
|
||||||
|
"low_calibrated_conf (<55)": share(
|
||||||
|
lambda r: (r.get("calibrated_confidence") or 0) < 55
|
||||||
|
),
|
||||||
|
"high_odds_underdog (>=2.5)": share(
|
||||||
|
lambda r: (r.get("odds") or 0) >= 2.5
|
||||||
|
),
|
||||||
|
"low_data_quality (<0.55)": share(
|
||||||
|
lambda r: (r.get("data_quality_score") or 1) < 0.55
|
||||||
|
),
|
||||||
|
"high_risk_level": share(
|
||||||
|
lambda r: r.get("risk_level") in ("HIGH", "EXTREME")
|
||||||
|
),
|
||||||
|
"inferred_features": share(
|
||||||
|
lambda r: "ai_features_inferred_from_history" in (r.get("data_quality_flags") or "")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"by_market": Counter(r["market"] for r in losses).most_common(),
|
||||||
|
"by_league": Counter(r.get("league_name") for r in losses).most_common(10),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Top issue tags from betting_brain across losses
|
||||||
|
issue_counter = Counter()
|
||||||
|
veto_counter = Counter()
|
||||||
|
for r in losses:
|
||||||
|
for tag in (r.get("bb_issues") or "").split(";"):
|
||||||
|
if tag:
|
||||||
|
issue_counter[tag] += 1
|
||||||
|
for tag in (r.get("bb_vetoes") or "").split(";"):
|
||||||
|
if tag:
|
||||||
|
veto_counter[tag] += 1
|
||||||
|
diagnostics["top_bb_issues_in_losses"] = issue_counter.most_common(15)
|
||||||
|
diagnostics["top_bb_vetoes_in_losses"] = veto_counter.most_common(15)
|
||||||
|
return diagnostics
|
||||||
|
|
||||||
|
|
||||||
|
# ── Recommendations ────────────────────────────────────────────────────
|
||||||
|
def make_recommendations(rows: List[Dict], agg: Dict[str, Any],
|
||||||
|
diag: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
recs: List[Dict[str, Any]] = []
|
||||||
|
overall = agg.get("overall") or {}
|
||||||
|
if not overall.get("n_playable_settled"):
|
||||||
|
return recs
|
||||||
|
|
||||||
|
# Cross-reference market hit rate vs overall — flag chronic losers.
|
||||||
|
overall_hit = overall.get("hit_rate_pct") or 0.0
|
||||||
|
for market, stats in (agg.get("by_market") or {}).items():
|
||||||
|
n = stats.get("n_playable_settled") or 0
|
||||||
|
hit = stats.get("hit_rate_pct")
|
||||||
|
roi = stats.get("roi_pct")
|
||||||
|
if n < 30:
|
||||||
|
continue
|
||||||
|
if hit is not None and roi is not None and roi < -10 and hit < overall_hit - 10:
|
||||||
|
recs.append({
|
||||||
|
"type": "drop_market",
|
||||||
|
"market": market,
|
||||||
|
"evidence": f"hit={hit}%, roi={roi}%, n={n} — chronic loser",
|
||||||
|
"suggested_fix": f"Add veto in betting_brain when market=={market} unless overwhelming evidence",
|
||||||
|
"estimated_loss_prevented_units": round(-(stats.get("unit_profit") or 0), 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Confidence band tuning — flag bands where ROI < 0 despite passing eşik
|
||||||
|
for band, stats in (agg.get("by_confidence") or {}).items():
|
||||||
|
n = stats.get("n_playable_settled") or 0
|
||||||
|
roi = stats.get("roi_pct")
|
||||||
|
if n >= 40 and roi is not None and roi < -8:
|
||||||
|
recs.append({
|
||||||
|
"type": "raise_confidence_threshold",
|
||||||
|
"confidence_band": band,
|
||||||
|
"evidence": f"n={n}, roi={roi}%",
|
||||||
|
"suggested_fix": f"Raise MIN_BET_SCORE or market_min_conf above {band.split('-')[0]}",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Loss diagnostic — if cup matches dominate losses, recommend cup-aware filter
|
||||||
|
patterns = (diag.get("patterns") or {})
|
||||||
|
cup_share = patterns.get("cup_match", (0, 0))[1]
|
||||||
|
if cup_share >= 25:
|
||||||
|
recs.append({
|
||||||
|
"type": "cup_match_filter",
|
||||||
|
"evidence": f"{cup_share}% of losses are cup matches",
|
||||||
|
"suggested_fix": "Tighten betting_brain thresholds for is_cup_match=True picks",
|
||||||
|
})
|
||||||
|
|
||||||
|
rev_share = patterns.get("high_htft_reversal_prob (>=0.20)", (0, 0))[1]
|
||||||
|
if rev_share >= 15:
|
||||||
|
recs.append({
|
||||||
|
"type": "tighten_reversal_check",
|
||||||
|
"evidence": f"{rev_share}% of losses had HTFT reversal prob >=0.20 (already partial fix)",
|
||||||
|
"suggested_fix": "Lower reversal threshold in betting_brain from 0.25 to 0.20 for veto trigger",
|
||||||
|
})
|
||||||
|
|
||||||
|
rel_share = patterns.get("low_league_reliability (<0.45)", (0, 0))[1]
|
||||||
|
if rel_share >= 20:
|
||||||
|
recs.append({
|
||||||
|
"type": "league_reliability_filter",
|
||||||
|
"evidence": f"{rel_share}% of losses in low-reliability leagues (<0.45)",
|
||||||
|
"suggested_fix": "Add hard veto when odds_reliability<0.45 for non-value-sniper picks",
|
||||||
|
})
|
||||||
|
|
||||||
|
return recs
|
||||||
|
|
||||||
|
|
||||||
|
# ── CSV / report writers ───────────────────────────────────────────────
|
||||||
|
def write_csv(rows: List[Dict], path: str):
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
import csv
|
||||||
|
fields = list(rows[0].keys())
|
||||||
|
with open(path, "w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=fields)
|
||||||
|
w.writeheader()
|
||||||
|
for r in rows:
|
||||||
|
w.writerow(r)
|
||||||
|
|
||||||
|
|
||||||
|
def write_text_summary(rows: List[Dict], agg: Dict, diag: Dict,
|
||||||
|
recs: List[Dict], path: str, args):
|
||||||
|
lines: List[str] = []
|
||||||
|
push = lines.append
|
||||||
|
push("=" * 78)
|
||||||
|
push("DIAGNOSTIC BACKTEST REPORT")
|
||||||
|
push("=" * 78)
|
||||||
|
push(f"Generated: {datetime.now().isoformat(timespec='seconds')}")
|
||||||
|
push(f"Sample window: start={args.start or f'-{args.days}d'}, end={args.end or 'now'}")
|
||||||
|
push(f"Max matches: {args.max_matches}")
|
||||||
|
push(f"Excluded days: {sorted(EXCLUDED_DATES)}")
|
||||||
|
push("")
|
||||||
|
push("OVERALL")
|
||||||
|
push("-" * 78)
|
||||||
|
overall = agg.get("overall") or {}
|
||||||
|
for k in ("n_total", "n_playable_settled", "wins", "losses",
|
||||||
|
"hit_rate_pct", "unit_profit", "staked", "roi_pct"):
|
||||||
|
push(f" {k:25}: {overall.get(k)}")
|
||||||
|
push("")
|
||||||
|
push("PER MARKET")
|
||||||
|
push("-" * 78)
|
||||||
|
push(f" {'market':<8} {'n':>6} {'hit%':>7} {'profit':>9} {'roi%':>7}")
|
||||||
|
for market, s in sorted((agg.get("by_market") or {}).items(),
|
||||||
|
key=lambda kv: -(kv[1].get("n_playable_settled") or 0)):
|
||||||
|
push(f" {market:<8} {s.get('n_playable_settled',0):>6} "
|
||||||
|
f"{str(s.get('hit_rate_pct','')):>7} "
|
||||||
|
f"{str(s.get('unit_profit','')):>9} "
|
||||||
|
f"{str(s.get('roi_pct','')):>7}")
|
||||||
|
push("")
|
||||||
|
push("PER CALIBRATED CONFIDENCE BAND")
|
||||||
|
push("-" * 78)
|
||||||
|
push(f" {'band':<10} {'n':>6} {'hit%':>7} {'roi%':>7}")
|
||||||
|
for band, s in sorted((agg.get("by_confidence") or {}).items()):
|
||||||
|
push(f" {band:<10} {s.get('n_playable_settled',0):>6} "
|
||||||
|
f"{str(s.get('hit_rate_pct','')):>7} "
|
||||||
|
f"{str(s.get('roi_pct','')):>7}")
|
||||||
|
push("")
|
||||||
|
push("PER ODDS BAND")
|
||||||
|
push("-" * 78)
|
||||||
|
push(f" {'band':<10} {'n':>6} {'hit%':>7} {'roi%':>7}")
|
||||||
|
for band, s in sorted((agg.get("by_odds") or {}).items()):
|
||||||
|
push(f" {band:<10} {s.get('n_playable_settled',0):>6} "
|
||||||
|
f"{str(s.get('hit_rate_pct','')):>7} "
|
||||||
|
f"{str(s.get('roi_pct','')):>7}")
|
||||||
|
push("")
|
||||||
|
push("LEAGUE vs CUP")
|
||||||
|
push("-" * 78)
|
||||||
|
for k, s in (agg.get("by_competition") or {}).items():
|
||||||
|
push(f" {k:<8} n={s.get('n_playable_settled',0):>4} "
|
||||||
|
f"hit={s.get('hit_rate_pct','-')}% roi={s.get('roi_pct','-')}%")
|
||||||
|
push("")
|
||||||
|
push("LOSS DIAGNOSTICS")
|
||||||
|
push("-" * 78)
|
||||||
|
push(f" total losses: {diag.get('n_losses')}")
|
||||||
|
push(f" total lost units: {diag.get('total_loss_units')}")
|
||||||
|
push(f" By market: {diag.get('by_market')}")
|
||||||
|
push(" Loss patterns (count, % of losses):")
|
||||||
|
for pattern, (c, pct) in (diag.get("patterns") or {}).items():
|
||||||
|
push(f" {pattern:<55} {c:>4} ({pct}%)")
|
||||||
|
push(" Top betting_brain issues seen in losses:")
|
||||||
|
for issue, c in (diag.get("top_bb_issues_in_losses") or []):
|
||||||
|
push(f" {issue:<55} {c}")
|
||||||
|
push(" Top betting_brain vetoes (in losses — i.e. veto fired but bet still went through value-sniper override):")
|
||||||
|
for veto, c in (diag.get("top_bb_vetoes_in_losses") or []):
|
||||||
|
push(f" {veto:<55} {c}")
|
||||||
|
push("")
|
||||||
|
push("RECOMMENDATIONS")
|
||||||
|
push("-" * 78)
|
||||||
|
if not recs:
|
||||||
|
push(" (none surfaced — sample too small or no clear pattern)")
|
||||||
|
for r in recs:
|
||||||
|
push(f" • [{r['type']}]")
|
||||||
|
for k, v in r.items():
|
||||||
|
if k == "type":
|
||||||
|
continue
|
||||||
|
push(f" {k}: {v}")
|
||||||
|
push("")
|
||||||
|
push("=" * 78)
|
||||||
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
|
f.write("\n".join(lines))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Main loop ─────────────────────────────────────────────────────────
|
||||||
|
def _checkpoint_paths(args) -> Tuple[str, str]:
|
||||||
|
"""Stable checkpoint paths derived from the run's date window so a
|
||||||
|
re-run with the same args picks up the same checkpoint."""
|
||||||
|
key = f"{args.start or 'd' + str(args.days)}_{args.end or 'now'}_{args.max_matches}"
|
||||||
|
key = key.replace("-", "").replace(":", "")
|
||||||
|
ckpt_csv = os.path.join(REPORTS_DIR, f"_checkpoint_{key}.csv")
|
||||||
|
ckpt_state = os.path.join(REPORTS_DIR, f"_checkpoint_{key}.state")
|
||||||
|
return ckpt_csv, ckpt_state
|
||||||
|
|
||||||
|
|
||||||
|
def _load_checkpoint(args) -> Tuple[List[Dict], set]:
|
||||||
|
"""Read partial CSV + processed-IDs set if a previous run was interrupted."""
|
||||||
|
ckpt_csv, _ = _checkpoint_paths(args)
|
||||||
|
if not os.path.exists(ckpt_csv):
|
||||||
|
return [], set()
|
||||||
|
import csv
|
||||||
|
rows: List[Dict] = []
|
||||||
|
seen: set = set()
|
||||||
|
try:
|
||||||
|
with open(ckpt_csv, "r", encoding="utf-8", newline="") as f:
|
||||||
|
reader = csv.DictReader(f)
|
||||||
|
for row in reader:
|
||||||
|
rows.append(row)
|
||||||
|
seen.add(str(row.get("match_id") or ""))
|
||||||
|
except Exception as e:
|
||||||
|
print(f" checkpoint read failed ({e}); starting fresh")
|
||||||
|
return [], set()
|
||||||
|
return rows, seen
|
||||||
|
|
||||||
|
|
||||||
|
def _flush_checkpoint(args, rows: List[Dict]) -> None:
|
||||||
|
"""Atomic-ish overwrite of the partial CSV. Cheap enough at every 100 rows."""
|
||||||
|
if not rows:
|
||||||
|
return
|
||||||
|
ckpt_csv, _ = _checkpoint_paths(args)
|
||||||
|
import csv
|
||||||
|
tmp = ckpt_csv + ".tmp"
|
||||||
|
fields = list(rows[0].keys())
|
||||||
|
with open(tmp, "w", encoding="utf-8", newline="") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=fields)
|
||||||
|
w.writeheader()
|
||||||
|
for r in rows:
|
||||||
|
w.writerow(r)
|
||||||
|
os.replace(tmp, ckpt_csv)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument("--days", type=int, default=14,
|
||||||
|
help="Backwards window from now (default 14)")
|
||||||
|
parser.add_argument("--max-matches", type=int, default=2000,
|
||||||
|
help="Hard cap on matches processed (default 2000)")
|
||||||
|
parser.add_argument("--start", help="Start date YYYY-MM-DD (overrides --days)")
|
||||||
|
parser.add_argument("--end", help="End date YYYY-MM-DD")
|
||||||
|
parser.add_argument("--progress-interval", type=int, default=50)
|
||||||
|
parser.add_argument("--checkpoint-every", type=int, default=100,
|
||||||
|
help="Flush partial CSV every N matches (default 100)")
|
||||||
|
parser.add_argument("--no-resume", action="store_true",
|
||||||
|
help="Ignore any prior checkpoint and start fresh")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("DIAGNOSTIC BACKTEST")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"Loading orchestrator...")
|
||||||
|
orch = get_single_match_orchestrator()
|
||||||
|
# Warm V25 + V27 + basketball loaders so the first match doesn't pay it
|
||||||
|
try:
|
||||||
|
orch._get_v25_predictor()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" v25 warmup: {e}")
|
||||||
|
try:
|
||||||
|
orch._get_v27_predictor()
|
||||||
|
except Exception as e:
|
||||||
|
print(f" v27 warmup: {e}")
|
||||||
|
|
||||||
|
print(f"Fetching match window...")
|
||||||
|
matches = fetch_match_window(args)
|
||||||
|
n = len(matches)
|
||||||
|
print(f" {n} matches selected")
|
||||||
|
|
||||||
|
if not matches:
|
||||||
|
print("No matches to process. Exiting.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# ── Resume from prior checkpoint if available ──
|
||||||
|
rows: List[Dict[str, Any]] = []
|
||||||
|
seen_ids: set = set()
|
||||||
|
if not args.no_resume:
|
||||||
|
rows, seen_ids = _load_checkpoint(args)
|
||||||
|
if rows:
|
||||||
|
print(f" Resuming from checkpoint: {len(rows)} matches already done")
|
||||||
|
errors: List[Tuple[str, str]] = []
|
||||||
|
t0 = time.time()
|
||||||
|
|
||||||
|
for i, m in enumerate(matches, start=1):
|
||||||
|
mid = str(m["match_id"])
|
||||||
|
if mid in seen_ids:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
pkg = orch.analyze_match(mid)
|
||||||
|
if pkg is None:
|
||||||
|
continue
|
||||||
|
row = capture_bet_row(m, pkg)
|
||||||
|
rows.append(row)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted, flushing checkpoint...")
|
||||||
|
_flush_checkpoint(args, rows)
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
errors.append((mid, str(e)))
|
||||||
|
if len(errors) <= 5:
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# ── Periodic checkpoint flush so a crash doesn't lose everything ──
|
||||||
|
if i % args.checkpoint_every == 0:
|
||||||
|
_flush_checkpoint(args, rows)
|
||||||
|
|
||||||
|
if i % args.progress_interval == 0:
|
||||||
|
elapsed = time.time() - t0
|
||||||
|
rate = i / elapsed
|
||||||
|
eta = (n - i) / rate if rate else 0
|
||||||
|
playable_so_far = sum(1 for r in rows if r["playable"])
|
||||||
|
print(f" [{i}/{n}] rate={rate:.1f}/s eta={eta/60:.1f}min "
|
||||||
|
f"playable={playable_so_far} errors={len(errors)} "
|
||||||
|
f"(checkpoint at every {args.checkpoint_every})")
|
||||||
|
|
||||||
|
print(f"\nProcessed {len(rows)} rows in {(time.time()-t0):.1f}s "
|
||||||
|
f"({len(errors)} errors)")
|
||||||
|
|
||||||
|
# Aggregate
|
||||||
|
print("Aggregating...")
|
||||||
|
agg = aggregate(rows)
|
||||||
|
diag = loss_diagnostics(rows)
|
||||||
|
recs = make_recommendations(rows, agg, diag)
|
||||||
|
|
||||||
|
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
csv_path = os.path.join(REPORTS_DIR, f"diagnostic_backtest_{stamp}.csv")
|
||||||
|
json_path = os.path.join(REPORTS_DIR, f"diagnostic_backtest_{stamp}.json")
|
||||||
|
txt_path = os.path.join(REPORTS_DIR, f"diagnostic_backtest_{stamp}.txt")
|
||||||
|
|
||||||
|
write_csv(rows, csv_path)
|
||||||
|
with open(json_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump({"args": vars(args), "aggregate": agg, "loss_diagnostics": diag,
|
||||||
|
"recommendations": recs, "errors_sample": errors[:20]},
|
||||||
|
f, indent=2, default=str)
|
||||||
|
write_text_summary(rows, agg, diag, recs, txt_path, args)
|
||||||
|
|
||||||
|
print(f"\nOutputs:")
|
||||||
|
print(f" CSV: {csv_path}")
|
||||||
|
print(f" JSON: {json_path}")
|
||||||
|
print(f" TXT: {txt_path}")
|
||||||
|
print("\nOverall:", agg.get("overall"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
"""
|
||||||
|
Edge Search — is there a profitable POCKET (by league) the global model misses?
|
||||||
|
==============================================================================
|
||||||
|
Global leak-free MS is ~-5.6% (the vig). But efficiency varies: obscure / low-
|
||||||
|
tier leagues may be mispriced. This walks a leak-free model forward and slices
|
||||||
|
the value-bet ROI BY LEAGUE, requiring a real sample AND multi-fold consistency
|
||||||
|
so we don't chase one lucky window.
|
||||||
|
|
||||||
|
Leak-free: drops the confirmed/suspected leakage columns (see LEAKY). Uses odds
|
||||||
|
in features (realistic). Value bet = biggest model_prob - implied edge > margin.
|
||||||
|
|
||||||
|
⚠️ Even a positive pocket here is a LEAD, not proof: the CSV odds are a static
|
||||||
|
capture, not the verified closing line. Anything flagged must be forward-
|
||||||
|
validated with real CLV (capture_closing_odds.py) before staking.
|
||||||
|
|
||||||
|
Usage: python scripts/edge_search.py --folds 6 --min-bets 150
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, os, sys, time
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, AI_DIR)
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
|
||||||
|
META = {"match_id","home_team_id","away_team_id","league_id","mst_utc",
|
||||||
|
"score_home","score_away","ht_score_home","ht_score_away"}
|
||||||
|
LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals",
|
||||||
|
"squad_diff","home_squad_quality","away_squad_quality",
|
||||||
|
"referee_home_bias","referee_avg_goals"}
|
||||||
|
|
||||||
|
|
||||||
|
def league_names(ids):
|
||||||
|
"""Resilient id->name lookup."""
|
||||||
|
from data.db import get_clean_dsn
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
out = {}
|
||||||
|
ids = [str(i) for i in ids if i is not None]
|
||||||
|
if not ids: return out
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as c:
|
||||||
|
with c.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute("SELECT id, name FROM leagues WHERE id = ANY(%s)", (ids,))
|
||||||
|
for r in cur.fetchall(): out[str(r["id"])] = r["name"]
|
||||||
|
return out
|
||||||
|
except Exception:
|
||||||
|
time.sleep(1.0)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--folds", type=int, default=6)
|
||||||
|
ap.add_argument("--estimators", type=int, default=200)
|
||||||
|
ap.add_argument("--margin", type=float, default=0.0)
|
||||||
|
ap.add_argument("--min-bets", type=int, default=150)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
print(f"Loading {CSV} ...")
|
||||||
|
df = pd.read_csv(CSV, low_memory=False).sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh = pd.to_numeric(df["score_home"], errors="coerce")
|
||||||
|
sa = pd.to_numeric(df["score_away"], errors="coerce")
|
||||||
|
ok = sh.notna() & sa.notna()
|
||||||
|
df, sh, sa = df[ok].reset_index(drop=True), sh[ok.values].values, sa[ok.values].values
|
||||||
|
y = np.where(sh > sa, 0, np.where(sh == sa, 1, 2))
|
||||||
|
league = df["league_id"].astype(str).values
|
||||||
|
odds = df[["odds_ms_h","odds_ms_d","odds_ms_a"]].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
|
||||||
|
feats = [c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY]
|
||||||
|
X = df[feats].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
rel = pd.to_numeric(df.get("league_reliability_score", pd.Series([np.nan]*len(df))),
|
||||||
|
errors="coerce").fillna(-1.0).values
|
||||||
|
print(f" {len(df):,} rows features={len(feats)} (leak-free) folds={args.folds}")
|
||||||
|
|
||||||
|
n = len(df); start = int(n * 0.5)
|
||||||
|
bounds = np.linspace(start, n, args.folds + 1, dtype=int)
|
||||||
|
params = {"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05,
|
||||||
|
"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
|
||||||
|
# reliability quartile edges from the betting universe (rel>=0)
|
||||||
|
rv = rel[rel >= 0]
|
||||||
|
qs = np.quantile(rv, [0.25, 0.5, 0.75]) if len(rv) else [0.3, 0.5, 0.7]
|
||||||
|
def rel_band(x):
|
||||||
|
if x < 0: return "rel:unknown"
|
||||||
|
if x < qs[0]: return f"rel:Q1(<{qs[0]:.2f})"
|
||||||
|
if x < qs[1]: return f"rel:Q2"
|
||||||
|
if x < qs[2]: return f"rel:Q3"
|
||||||
|
return f"rel:Q4(>={qs[2]:.2f})"
|
||||||
|
def odds_band(o):
|
||||||
|
return ("<1.5" if o<1.5 else "1.5-2" if o<2 else "2-3" if o<3 else
|
||||||
|
"3-5" if o<5 else "5-8" if o<8 else "8+")
|
||||||
|
|
||||||
|
recs = [] # (group_key, fold, pnl, win)
|
||||||
|
glob = {"n":0,"pnl":0.0,"win":0}
|
||||||
|
for fi in range(args.folds):
|
||||||
|
te0, te1 = bounds[fi], bounds[fi+1]
|
||||||
|
if te1-te0 < 50: continue
|
||||||
|
bst = xgb.train(params, xgb.DMatrix(X[:te0], label=y[:te0]), num_boost_round=args.estimators)
|
||||||
|
proba = bst.predict(xgb.DMatrix(X[te0:te1]))
|
||||||
|
yte, ote, rte = y[te0:te1], odds[te0:te1], rel[te0:te1]
|
||||||
|
implied = np.where(ote > 1.0, 1.0/ote, np.nan)
|
||||||
|
edge = np.where(np.isnan(implied), -9.0, proba - implied)
|
||||||
|
pick = edge.argmax(1)
|
||||||
|
bet = edge[np.arange(len(yte)), pick] > args.margin
|
||||||
|
win = (pick == yte) & bet
|
||||||
|
pick_odds = ote[np.arange(len(yte)), pick]
|
||||||
|
pnl = np.where(win, pick_odds-1.0, -1.0)
|
||||||
|
for i in range(len(yte)):
|
||||||
|
if not bet[i]: continue
|
||||||
|
glob["n"]+=1; glob["pnl"]+=pnl[i]; glob["win"]+=int(win[i])
|
||||||
|
recs.append((rel_band(rte[i]), fi, pnl[i], int(win[i])))
|
||||||
|
recs.append((odds_band(pick_odds[i]), fi, pnl[i], int(win[i])))
|
||||||
|
recs.append((rel_band(rte[i])+" x "+odds_band(pick_odds[i]), fi, pnl[i], int(win[i])))
|
||||||
|
print(f" fold {fi}: tested {len(yte):,} bets {int(bet.sum()):,}")
|
||||||
|
|
||||||
|
print("\n"+"="*78)
|
||||||
|
print(f"GLOBAL leak-free: bets={glob['n']:,} hit={100*glob['win']/max(glob['n'],1):.1f}% "
|
||||||
|
f"ROI(flat1u)={100*glob['pnl']/max(glob['n'],1):.2f}%")
|
||||||
|
print("="*78)
|
||||||
|
|
||||||
|
rdf = pd.DataFrame(recs, columns=["grp","fold","pnl","win"])
|
||||||
|
def report(prefix, title):
|
||||||
|
sub = rdf[rdf["grp"].str.startswith(prefix)]
|
||||||
|
if sub.empty: return
|
||||||
|
print(f"\n{title}")
|
||||||
|
print(f" {'bucket':<26}{'bets':>6}{'hit%':>7}{'ROI%':>8}{'folds+':>8}")
|
||||||
|
print(" "+"-"*54)
|
||||||
|
g = sub.groupby("grp")
|
||||||
|
out=[]
|
||||||
|
for k,d in g:
|
||||||
|
nb=len(d)
|
||||||
|
if nb < args.min_bets: continue
|
||||||
|
roi=100*d["pnl"].sum()/nb; hit=100*d["win"].sum()/nb
|
||||||
|
fp=d.groupby("fold")["pnl"].sum(); folds_pos=int((fp>0).sum()); ft=fp.shape[0]
|
||||||
|
out.append((roi,k,nb,hit,folds_pos,ft))
|
||||||
|
for roi,k,nb,hit,fp,ft in sorted(out,reverse=True):
|
||||||
|
print(f" {k:<26}{nb:>6}{hit:>7.1f}{roi:>8.1f}{str(fp)+'/'+str(ft):>8}")
|
||||||
|
report("rel:", "BY LEAGUE-RELIABILITY BAND (Q1=most obscure ... Q4=most reliable)")
|
||||||
|
report(("<","1","2","3","5","8"), None) # odds bands start with digit/<
|
||||||
|
# odds-band buckets begin with a digit or '<'
|
||||||
|
sub = rdf[~rdf["grp"].str.startswith("rel:")]
|
||||||
|
sub = sub[~sub["grp"].str.contains(" x ")]
|
||||||
|
if not sub.empty:
|
||||||
|
print("\nBY ODDS BAND")
|
||||||
|
print(f" {'bucket':<26}{'bets':>6}{'hit%':>7}{'ROI%':>8}{'folds+':>8}")
|
||||||
|
print(" "+"-"*54)
|
||||||
|
out=[]
|
||||||
|
for k,d in sub.groupby("grp"):
|
||||||
|
nb=len(d)
|
||||||
|
if nb<args.min_bets: continue
|
||||||
|
roi=100*d["pnl"].sum()/nb; hit=100*d["win"].sum()/nb
|
||||||
|
fp=d.groupby("fold")["pnl"].sum(); out.append((roi,k,nb,hit,int((fp>0).sum()),fp.shape[0]))
|
||||||
|
for roi,k,nb,hit,fpv,ft in sorted(out,reverse=True):
|
||||||
|
print(f" {k:<26}{nb:>6}{hit:>7.1f}{roi:>8.1f}{str(fpv)+'/'+str(ft):>8}")
|
||||||
|
# 2D reliability x odds
|
||||||
|
sub2 = rdf[rdf["grp"].str.contains(" x ")]
|
||||||
|
if not sub2.empty:
|
||||||
|
print("\nBY RELIABILITY x ODDS (candidate pockets, n>=min-bets)")
|
||||||
|
print(f" {'bucket':<26}{'bets':>6}{'hit%':>7}{'ROI%':>8}{'folds+':>8}")
|
||||||
|
print(" "+"-"*54)
|
||||||
|
out=[]
|
||||||
|
for k,d in sub2.groupby("grp"):
|
||||||
|
nb=len(d)
|
||||||
|
if nb<args.min_bets: continue
|
||||||
|
roi=100*d["pnl"].sum()/nb; hit=100*d["win"].sum()/nb
|
||||||
|
fp=d.groupby("fold")["pnl"].sum(); out.append((roi,k,nb,hit,int((fp>0).sum()),fp.shape[0]))
|
||||||
|
for roi,k,nb,hit,fpv,ft in sorted(out,reverse=True)[:15]:
|
||||||
|
print(f" {k:<26}{nb:>6}{hit:>7.1f}{roi:>8.1f}{str(fpv)+'/'+str(ft):>8}")
|
||||||
|
print("\nREAD: a pocket is a real LEAD only if ROI>0 AND positive in MOST folds")
|
||||||
|
print("(folds+ near full) AND bets large. +ROI in 1-2 folds = noise / overfit.")
|
||||||
|
print("Then forward-validate with CLV (capture_closing_odds.py) before staking.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
"""
|
||||||
|
Extract Upcoming Features — leak-free feature rows for UPCOMING (NS) matches,
|
||||||
|
produced by the EXACT same pipeline that built training_data_v27.csv.
|
||||||
|
=============================================================================
|
||||||
|
Why this exists: the picker (generate_daily_picks.py) needs the 133 leak-free
|
||||||
|
features for tomorrow's matches, computed IDENTICALLY to training (any drift =
|
||||||
|
train/serve skew = broken model). So we reuse V27Loader + V27Extractor verbatim:
|
||||||
|
|
||||||
|
1. load_all() builds ELO / team history / league / squad caches from FT
|
||||||
|
matches ONLY (untouched — guarantees identical feature computation).
|
||||||
|
2. We then APPEND upcoming NS matches as targets and inject their odds from
|
||||||
|
live_matches.odds (all markets, same mapping as the trainer's _load_odds).
|
||||||
|
3. extract_all() replays FT chronologically (ELO fully built), then computes
|
||||||
|
features for the NS targets at the end. ELO update + labels are guarded
|
||||||
|
for null scores (NS has no result yet); the 133 model features never use
|
||||||
|
the current score, so they come out identical to training.
|
||||||
|
4. Write ONLY the upcoming rows -> data/upcoming_features.csv
|
||||||
|
|
||||||
|
Then: generate_daily_picks.py --features data/upcoming_features.csv --log
|
||||||
|
|
||||||
|
Run nightly (heavy: full ELO replay, like training). Read-only on the DB.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, AI_DIR)
|
||||||
|
|
||||||
|
from scripts.extract_training_data_v27 import ( # noqa: E402
|
||||||
|
V27Loader, V27Extractor, ALL_COLS, get_conn,
|
||||||
|
)
|
||||||
|
|
||||||
|
OUTPUT = os.path.join(AI_DIR, "data", "upcoming_features.csv")
|
||||||
|
DAYS_AHEAD = 4
|
||||||
|
|
||||||
|
|
||||||
|
def map_live_odds(odds_json) -> dict:
|
||||||
|
"""Map live_matches.odds JSON → odds_cache keys, IDENTICAL to the trainer's
|
||||||
|
_load_odds category/selection logic (so odds features match training)."""
|
||||||
|
out: dict = {}
|
||||||
|
if isinstance(odds_json, str):
|
||||||
|
try:
|
||||||
|
odds_json = json.loads(odds_json)
|
||||||
|
except Exception:
|
||||||
|
return out
|
||||||
|
if not isinstance(odds_json, dict):
|
||||||
|
return out
|
||||||
|
for cat, sels in odds_json.items():
|
||||||
|
if not isinstance(sels, dict):
|
||||||
|
continue
|
||||||
|
c = str(cat).lower().strip()
|
||||||
|
for sel, val in sels.items():
|
||||||
|
try:
|
||||||
|
v = float(val)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
if v <= 0:
|
||||||
|
continue
|
||||||
|
sn = str(sel)
|
||||||
|
s = sn.lower().strip()
|
||||||
|
if c == "maç sonucu":
|
||||||
|
if sn == "1": out["ms_h"] = v
|
||||||
|
elif sn in ("0", "X"): out["ms_d"] = v
|
||||||
|
elif sn == "2": out["ms_a"] = v
|
||||||
|
elif c == "1. yarı sonucu":
|
||||||
|
if sn == "1": out["ht_ms_h"] = v
|
||||||
|
elif sn in ("0", "X"): out["ht_ms_d"] = v
|
||||||
|
elif sn == "2": out["ht_ms_a"] = v
|
||||||
|
elif c == "karşılıklı gol":
|
||||||
|
if "var" in s: out["btts_y"] = v
|
||||||
|
elif "yok" in s: out["btts_n"] = v
|
||||||
|
elif c == "0,5 alt/üst":
|
||||||
|
if "alt" in s: out["ou05_u"] = v
|
||||||
|
elif "üst" in s: out["ou05_o"] = v
|
||||||
|
elif c == "1,5 alt/üst":
|
||||||
|
if "alt" in s: out["ou15_u"] = v
|
||||||
|
elif "üst" in s: out["ou15_o"] = v
|
||||||
|
elif c == "2,5 alt/üst":
|
||||||
|
if "alt" in s: out["ou25_u"] = v
|
||||||
|
elif "üst" in s: out["ou25_o"] = v
|
||||||
|
elif c == "3,5 alt/üst":
|
||||||
|
if "alt" in s: out["ou35_u"] = v
|
||||||
|
elif "üst" in s: out["ou35_o"] = v
|
||||||
|
elif c == "1. yarı 0,5 alt/üst":
|
||||||
|
if "alt" in s: out["ht_ou05_u"] = v
|
||||||
|
elif "üst" in s: out["ht_ou05_o"] = v
|
||||||
|
elif c == "1. yarı 1,5 alt/üst":
|
||||||
|
if "alt" in s: out["ht_ou15_u"] = v
|
||||||
|
elif "üst" in s: out["ht_ou15_o"] = v
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class UpcomingExtractor(V27Extractor):
|
||||||
|
"""Same feature computation as training; only guards null (NS) scores."""
|
||||||
|
|
||||||
|
def _update_elo(self, home_id, away_id, score_home, score_away):
|
||||||
|
if score_home is None or score_away is None:
|
||||||
|
return # upcoming match — no result, don't move ELO
|
||||||
|
return super()._update_elo(home_id, away_id, score_home, score_away)
|
||||||
|
|
||||||
|
def _extract_one(self, mid, hid, aid, sh, sa, hth, hta, mst, lid, hn, an, ln):
|
||||||
|
if sh is None or sa is None:
|
||||||
|
# Upcoming TARGET. Dummy scores so label/total_goals don't crash;
|
||||||
|
# those columns are labels/LEAKY and are NOT among the 133 model
|
||||||
|
# features, so the served feature vector is identical to training.
|
||||||
|
row = super()._extract_one(mid, hid, aid, 0, 0, 0, 0, mst, lid, hn, an, ln)
|
||||||
|
if row:
|
||||||
|
row["_upcoming"] = 1
|
||||||
|
return row
|
||||||
|
# FT match: needed ONLY to advance ELO (extract_all calls _update_elo
|
||||||
|
# afterwards regardless). Skip the expensive per-match feature
|
||||||
|
# computation — that turns a ~6h full extraction into seconds while
|
||||||
|
# producing the IDENTICAL final ELO the upcoming targets read.
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
t0 = time.time()
|
||||||
|
conn = get_conn()
|
||||||
|
|
||||||
|
# ── Cheap check FIRST: are there upcoming matches with odds? ──
|
||||||
|
now_ms = int(time.time() * 1000)
|
||||||
|
hi_ms = now_ms + DAYS_AHEAD * 24 * 3600 * 1000
|
||||||
|
cur = conn.cursor()
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT lm.id, lm.home_team_id, lm.away_team_id, lm.mst_utc, lm.league_id,
|
||||||
|
ht.name, at.name, l.name, lm.odds
|
||||||
|
FROM live_matches lm
|
||||||
|
JOIN teams ht ON ht.id = lm.home_team_id
|
||||||
|
JOIN teams at ON at.id = lm.away_team_id
|
||||||
|
JOIN leagues l ON l.id = lm.league_id
|
||||||
|
WHERE lm.sport = 'football'
|
||||||
|
AND lm.odds IS NOT NULL
|
||||||
|
AND lm.mst_utc > %s AND lm.mst_utc <= %s
|
||||||
|
ORDER BY lm.mst_utc ASC
|
||||||
|
""",
|
||||||
|
(now_ms, hi_ms),
|
||||||
|
)
|
||||||
|
upcoming = cur.fetchall()
|
||||||
|
targets = []
|
||||||
|
for mid, hid, aid, mst, lid, hn, an, ln, odds_json in upcoming:
|
||||||
|
oc = map_live_odds(odds_json)
|
||||||
|
if "ms_h" not in oc or "ms_a" not in oc:
|
||||||
|
continue # need MS odds for the policy
|
||||||
|
targets.append((mid, hid, aid, mst, lid, hn, an, ln, oc))
|
||||||
|
print(f"Upcoming NS matches with MS odds (next {DAYS_AHEAD}d): {len(targets)}", flush=True)
|
||||||
|
if not targets:
|
||||||
|
print("⚠️ Nothing to extract. Deploy the 4-day window + let the odds cron\n"
|
||||||
|
" populate live_matches, then re-run.")
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
print("📦 Loading FT history (ELO/form/league/squad caches; heavy) ...", flush=True)
|
||||||
|
loader = V27Loader(conn)
|
||||||
|
loader.load_all()
|
||||||
|
loader.load_league_matches()
|
||||||
|
print(f" FT matches: {len(loader.matches)}", flush=True)
|
||||||
|
|
||||||
|
for mid, hid, aid, mst, lid, hn, an, ln, oc in targets:
|
||||||
|
loader.odds_cache[mid] = oc
|
||||||
|
loader.matches.append(
|
||||||
|
(mid, hid, aid, None, None, None, None, mst, lid, hn, an, ln)
|
||||||
|
)
|
||||||
|
# NS targets must be processed AFTER all FT (ELO fully built)
|
||||||
|
loader.matches.sort(key=lambda m: m[7] if m[7] is not None else 0)
|
||||||
|
added = len(targets)
|
||||||
|
|
||||||
|
print("🔄 Extracting features (FT replay + upcoming targets) ...", flush=True)
|
||||||
|
ext = UpcomingExtractor(conn, loader)
|
||||||
|
rows = ext.extract_all()
|
||||||
|
up_rows = [r for r in rows if r.get("_upcoming")]
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
|
||||||
|
with open(OUTPUT, "w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=ALL_COLS, extrasaction="ignore")
|
||||||
|
w.writeheader()
|
||||||
|
w.writerows(up_rows)
|
||||||
|
|
||||||
|
with_odds = sum(1 for r in up_rows if r.get("odds_ms_h", 0) and r["odds_ms_h"] > 0)
|
||||||
|
print(f"\n✅ Wrote {len(up_rows)} upcoming feature rows ({with_odds} with MS odds) → {OUTPUT}")
|
||||||
|
print(f" Time: {(time.time()-t0)/60:.1f} min")
|
||||||
|
print(" Next: python scripts/generate_daily_picks.py --features data/upcoming_features.csv --log")
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
"""Guarded self-correction loop — fits the market-anchor correction table.
|
||||||
|
|
||||||
|
What it does (the "tablo üreteci" of the feedback loop):
|
||||||
|
|
||||||
|
1. MEASURE: on settled real-odds matches, per implied-probability band, the
|
||||||
|
gap between the RAW de-vigged probability and the actual rate — for BOTH
|
||||||
|
the home side (ms_home) and the away side (ms_away).
|
||||||
|
2. BRAKE: a band only earns a correction if it passes the safety gates —
|
||||||
|
* min sample (>= MIN_N matches in the band, fitted on TRAIN window)
|
||||||
|
* shrinkage (delta = SHRINK x measured gap — never the full gap)
|
||||||
|
* clipping (|delta| <= CLIP)
|
||||||
|
* materiality (|delta| >= MIN_DELTA, else 0 — don't chase noise)
|
||||||
|
3. PROVE: the candidate table must beat the CURRENTLY ACTIVE corrections
|
||||||
|
out-of-sample (most recent TEST_DAYS, never seen during fitting) on
|
||||||
|
combined home+away ECE. If it doesn't, nothing is written.
|
||||||
|
4. WRITE: versioned artifact `config/market_anchor_corrections.json`
|
||||||
|
(+ timestamped copy under `config/history/`). The engine reads the table
|
||||||
|
at runtime (models/market_anchor.py) — the loop never modifies code.
|
||||||
|
|
||||||
|
Run weekly (cron) or manually after big data ingests:
|
||||||
|
python scripts/fit_anchor_corrections.py [--days 540] [--test-days 90]
|
||||||
|
python scripts/fit_anchor_corrections.py --dry-run # measure only
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
import psycopg2 # noqa: E402
|
||||||
|
from psycopg2.extras import RealDictCursor # noqa: E402
|
||||||
|
|
||||||
|
from data.db import get_clean_dsn # noqa: E402
|
||||||
|
from models.market_anchor import ( # noqa: E402
|
||||||
|
away_favorite_delta,
|
||||||
|
home_favorite_delta,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── safety gates ─────────────────────────────────────────────────────
|
||||||
|
MIN_N = 1500 # band needs this many TRAIN matches to earn a correction
|
||||||
|
SHRINK = 0.5 # apply only half of the measured gap
|
||||||
|
CLIP = 0.05 # never correct more than 5 points
|
||||||
|
MIN_DELTA = 0.004 # below this the correction is noise — emit 0
|
||||||
|
ACCEPT_MARGIN = 0.0002 # candidate must beat active combined ECE by this
|
||||||
|
|
||||||
|
BANDS: Tuple[Tuple[float, float], ...] = (
|
||||||
|
(0.05, 0.15), (0.15, 0.25), (0.25, 0.35), (0.35, 0.45),
|
||||||
|
(0.45, 0.55), (0.55, 0.65), (0.65, 0.75), (0.75, 0.85), (0.85, 1.01),
|
||||||
|
)
|
||||||
|
|
||||||
|
REAL_ODDS_MIN_OVERROUND = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(days: int) -> List[Dict[str, Any]]:
|
||||||
|
since_ms = int((time.time() - days * 86400) * 1000)
|
||||||
|
sql = """
|
||||||
|
SELECT f.implied_home AS p1, f.implied_draw AS px, f.implied_away AS p2,
|
||||||
|
m.mst_utc,
|
||||||
|
(m.winner = 'home')::int AS home_won,
|
||||||
|
(m.winner = 'away')::int AS away_won
|
||||||
|
FROM football_ai_features f
|
||||||
|
JOIN matches m ON m.id = f.match_id
|
||||||
|
WHERE m.sport = 'football'
|
||||||
|
AND m.winner IN ('home', 'away', 'draw')
|
||||||
|
AND f.odds_overround > %s
|
||||||
|
AND m.mst_utc >= %s
|
||||||
|
"""
|
||||||
|
out: List[Dict[str, Any]] = []
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute("SET statement_timeout = '120s'")
|
||||||
|
with conn.cursor("fit_stream", cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.itersize = 5000
|
||||||
|
cur.execute(sql, (REAL_ODDS_MIN_OVERROUND, since_ms))
|
||||||
|
for r in cur:
|
||||||
|
p1, px, p2 = r["p1"], r["px"], r["p2"]
|
||||||
|
if p1 is None or px is None or p2 is None:
|
||||||
|
continue
|
||||||
|
if abs(float(p1) + float(px) + float(p2) - 1.0) > 0.02:
|
||||||
|
continue
|
||||||
|
out.append({
|
||||||
|
"p1": float(p1), "p2": float(p2),
|
||||||
|
"y1": int(r["home_won"]), "y2": int(r["away_won"]),
|
||||||
|
"mst_utc": int(r["mst_utc"]),
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def band_of(p: float) -> Optional[int]:
|
||||||
|
for i, (lo, hi) in enumerate(BANDS):
|
||||||
|
if lo <= p < hi:
|
||||||
|
return i
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def fit_candidate(
|
||||||
|
train: List[Dict[str, Any]], pkey: str, ykey: str
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
n = defaultdict(int); sp = defaultdict(float); sy = defaultdict(int)
|
||||||
|
for r in train:
|
||||||
|
b = band_of(r[pkey])
|
||||||
|
if b is None:
|
||||||
|
continue
|
||||||
|
n[b] += 1; sp[b] += r[pkey]; sy[b] += r[ykey]
|
||||||
|
bands: List[Dict[str, Any]] = []
|
||||||
|
for i, (lo, hi) in enumerate(BANDS):
|
||||||
|
if n[i] < MIN_N:
|
||||||
|
continue # gate: not enough evidence — no correction for this band
|
||||||
|
raw_gap = (sy[i] / n[i]) - (sp[i] / n[i])
|
||||||
|
delta = max(-CLIP, min(CLIP, SHRINK * raw_gap))
|
||||||
|
if abs(delta) < MIN_DELTA:
|
||||||
|
delta = 0.0
|
||||||
|
bands.append({"lo": lo, "hi": hi, "delta": round(delta, 4),
|
||||||
|
"n": n[i], "raw_gap": round(raw_gap, 4)})
|
||||||
|
return bands
|
||||||
|
|
||||||
|
|
||||||
|
def table_delta_fn(table: List[Dict[str, Any]]) -> Callable[[float], float]:
|
||||||
|
def fn(p: float) -> float:
|
||||||
|
for b in table:
|
||||||
|
if b["lo"] <= p < b["hi"]:
|
||||||
|
return b["delta"]
|
||||||
|
return 0.0
|
||||||
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def ece(rows: List[Dict[str, Any]], pkey: str, ykey: str,
|
||||||
|
delta_fn: Callable[[float], float]) -> float:
|
||||||
|
n = defaultdict(int); sp = defaultdict(float); sy = defaultdict(int)
|
||||||
|
for r in rows:
|
||||||
|
pc = min(0.98, r[pkey] + delta_fn(r[pkey]))
|
||||||
|
b = min(19, int(pc * 20))
|
||||||
|
n[b] += 1; sp[b] += pc; sy[b] += r[ykey]
|
||||||
|
total = sum(n.values())
|
||||||
|
if not total:
|
||||||
|
return 0.0
|
||||||
|
return sum(n[b] * abs(sp[b] / n[b] - sy[b] / n[b]) for b in n) / total
|
||||||
|
|
||||||
|
|
||||||
|
def print_bands(title: str, bands: List[Dict[str, Any]]) -> None:
|
||||||
|
print(f"\ncandidate bands — {title} (after gates):")
|
||||||
|
print(f"{'band':>12} {'n':>8} {'raw_gap_pt':>11} {'delta_pt':>9}")
|
||||||
|
for b in bands:
|
||||||
|
print(f"{b['lo']:>5.2f}-{b['hi']:<5.2f} {b['n']:>8} "
|
||||||
|
f"{100 * b['raw_gap']:>11.2f} {100 * b['delta']:>9.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--days", type=int, default=540, help="total lookback")
|
||||||
|
ap.add_argument("--test-days", type=int, default=90,
|
||||||
|
help="most recent window held out for acceptance")
|
||||||
|
ap.add_argument("--dry-run", action="store_true",
|
||||||
|
help="measure and report only — never write")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
rows = fetch(args.days)
|
||||||
|
cutoff_ms = int((time.time() - args.test_days * 86400) * 1000)
|
||||||
|
train = [r for r in rows if r["mst_utc"] < cutoff_ms]
|
||||||
|
test = [r for r in rows if r["mst_utc"] >= cutoff_ms]
|
||||||
|
print(f"matches: total={len(rows)} train={len(train)} test(OOS)={len(test)}")
|
||||||
|
if len(train) < 10 * MIN_N or len(test) < 2000:
|
||||||
|
print("ABORT: not enough data for a safe fit — keeping active table.")
|
||||||
|
return
|
||||||
|
|
||||||
|
cand_home = fit_candidate(train, "p1", "y1")
|
||||||
|
cand_away = fit_candidate(train, "p2", "y2")
|
||||||
|
print_bands("ms_home", cand_home)
|
||||||
|
print_bands("ms_away", cand_away)
|
||||||
|
|
||||||
|
# active = whatever the engine currently loads (artifact or fallback)
|
||||||
|
ece_h_act = ece(test, "p1", "y1", home_favorite_delta)
|
||||||
|
ece_a_act = ece(test, "p2", "y2", away_favorite_delta)
|
||||||
|
ece_h_cand = ece(test, "p1", "y1", table_delta_fn(cand_home))
|
||||||
|
ece_a_cand = ece(test, "p2", "y2", table_delta_fn(cand_away))
|
||||||
|
ece_h_raw = ece(test, "p1", "y1", lambda _p: 0.0)
|
||||||
|
ece_a_raw = ece(test, "p2", "y2", lambda _p: 0.0)
|
||||||
|
|
||||||
|
print(f"\nOOS ECE (home/away/combined):")
|
||||||
|
print(f" raw (devig only) : {100 * ece_h_raw:.3f}% / {100 * ece_a_raw:.3f}% "
|
||||||
|
f"/ {100 * (ece_h_raw + ece_a_raw):.3f}%")
|
||||||
|
print(f" ACTIVE table : {100 * ece_h_act:.3f}% / {100 * ece_a_act:.3f}% "
|
||||||
|
f"/ {100 * (ece_h_act + ece_a_act):.3f}%")
|
||||||
|
print(f" CANDIDATE table : {100 * ece_h_cand:.3f}% / {100 * ece_a_cand:.3f}% "
|
||||||
|
f"/ {100 * (ece_h_cand + ece_a_cand):.3f}%")
|
||||||
|
|
||||||
|
if args.dry_run:
|
||||||
|
print("\n(dry-run: nothing written)")
|
||||||
|
return
|
||||||
|
|
||||||
|
combined_act = ece_h_act + ece_a_act
|
||||||
|
combined_cand = ece_h_cand + ece_a_cand
|
||||||
|
if combined_cand > combined_act - ACCEPT_MARGIN:
|
||||||
|
print("\nREJECTED: candidate does not beat the active table "
|
||||||
|
"out-of-sample. Active corrections stay. (Bu fren tasarim geregi:"
|
||||||
|
" kanitlayamayan tablo yazilmaz.)")
|
||||||
|
return
|
||||||
|
|
||||||
|
cfg_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "config"))
|
||||||
|
path = os.path.join(cfg_dir, "market_anchor_corrections.json")
|
||||||
|
artifact = {
|
||||||
|
"version": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||||
|
"fitted_on": {"days": args.days, "test_days": args.test_days,
|
||||||
|
"n_train": len(train), "n_test": len(test)},
|
||||||
|
"validated": {
|
||||||
|
"ece_home": {"raw": round(ece_h_raw, 5), "active_before": round(ece_h_act, 5),
|
||||||
|
"candidate_oos": round(ece_h_cand, 5)},
|
||||||
|
"ece_away": {"raw": round(ece_a_raw, 5), "active_before": round(ece_a_act, 5),
|
||||||
|
"candidate_oos": round(ece_a_cand, 5)},
|
||||||
|
},
|
||||||
|
"gates": {"min_n": MIN_N, "shrink": SHRINK, "clip": CLIP,
|
||||||
|
"min_delta": MIN_DELTA},
|
||||||
|
"corrections": {"ms_home": cand_home, "ms_away": cand_away},
|
||||||
|
}
|
||||||
|
hist_dir = os.path.join(cfg_dir, "history")
|
||||||
|
os.makedirs(hist_dir, exist_ok=True)
|
||||||
|
if os.path.exists(path):
|
||||||
|
shutil.copy2(path, os.path.join(
|
||||||
|
hist_dir, f"market_anchor_corrections-{int(time.time())}.json"))
|
||||||
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
json.dump(artifact, fh, ensure_ascii=False, indent=2)
|
||||||
|
print(f"\nACCEPTED: wrote {path}")
|
||||||
|
|
||||||
|
# The deployed ai-engine container has NO volume mounts, so the file above
|
||||||
|
# is invisible to it — app_settings is the shared medium. Running engines
|
||||||
|
# re-read it within ~10 minutes (TTL in models/market_anchor.py).
|
||||||
|
try:
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as conn:
|
||||||
|
with conn.cursor() as cur:
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO app_settings (key, value, updated_at)
|
||||||
|
VALUES ('market_anchor_corrections', %s, now())
|
||||||
|
ON CONFLICT (key)
|
||||||
|
DO UPDATE SET value = EXCLUDED.value, updated_at = now()
|
||||||
|
""",
|
||||||
|
(json.dumps(artifact, ensure_ascii=False),),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
print("ACCEPTED: upserted app_settings['market_anchor_corrections'] "
|
||||||
|
"(live engines refresh within ~10 min)")
|
||||||
|
except Exception as exc: # file artifact still written — warn only
|
||||||
|
print(f"WARN: app_settings upsert failed: {exc}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
"""
|
||||||
|
Generate Daily Picks — the serving picker for the validated favourite policy.
|
||||||
|
============================================================================
|
||||||
|
Loads the saved leak-free MS model (models/favorite_v1) and applies the
|
||||||
|
favourite-band value policy to a set of matches, emitting the day's STAKED
|
||||||
|
picks and logging them for forward paper-trade settlement.
|
||||||
|
|
||||||
|
Train/serve consistency: features MUST come from the SAME extractor that built
|
||||||
|
training_data_v27.csv. Production path = run the extractor nightly INCLUDING
|
||||||
|
upcoming (status NS) matches, then point this script at that CSV. Demo path =
|
||||||
|
use the tail of the training CSV as stand-in "today" matches (with the real
|
||||||
|
result shown, since those are settled).
|
||||||
|
|
||||||
|
Policy: bet the MS side with the biggest model_prob - implied edge, ONLY if
|
||||||
|
odds in [--lo,--hi] and edge>--margin. Flat 1u. No longshots, no parlays.
|
||||||
|
Non-MS markets are NOT staked (efficient -> model error). One bet per match.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/generate_daily_picks.py --demo --n 20 # see it work now
|
||||||
|
python scripts/generate_daily_picks.py --features today.csv # production
|
||||||
|
python scripts/generate_daily_picks.py --settle # settle paper log
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, json, os, sys, datetime
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
MODEL_DIR = os.path.join(AI_DIR, "models", "favorite_v1")
|
||||||
|
TRAIN_CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
PAPER_LOG = os.path.join(AI_DIR, "data", "paper_trades.csv")
|
||||||
|
MS_ODDS = ["odds_ms_h", "odds_ms_d", "odds_ms_a"]
|
||||||
|
MS_PICKS = ["1", "X", "2"]
|
||||||
|
|
||||||
|
|
||||||
|
def load_model():
|
||||||
|
bst = xgb.Booster(); bst.load_model(os.path.join(MODEL_DIR, "model.json"))
|
||||||
|
with open(os.path.join(MODEL_DIR, "feature_cols.json"), encoding="utf-8") as f:
|
||||||
|
feats = json.load(f)
|
||||||
|
with open(os.path.join(MODEL_DIR, "metadata.json"), encoding="utf-8") as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
return bst, feats, meta
|
||||||
|
|
||||||
|
|
||||||
|
def pick_for_rows(df, bst, feats, lo, hi, margin):
|
||||||
|
X = df.reindex(columns=feats).apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
P = bst.predict(xgb.DMatrix(X)) # [n,3] home/draw/away
|
||||||
|
O = df[MS_ODDS].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
implied = np.where(O > 1.0, 1.0/O, np.nan)
|
||||||
|
edge = np.where(np.isnan(implied), -9.0, P - implied)
|
||||||
|
out = []
|
||||||
|
for i in range(len(df)):
|
||||||
|
k = int(np.argmax(edge[i])); o = float(O[i, k]); e = float(edge[i, k])
|
||||||
|
staked = (e > margin) and (lo <= o < hi)
|
||||||
|
out.append({"idx": i, "pick": MS_PICKS[k], "odds": round(o, 2),
|
||||||
|
"model_prob": round(float(P[i, k]), 4), "edge": round(e, 4),
|
||||||
|
"staked": staked})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def settle():
|
||||||
|
if not os.path.exists(PAPER_LOG):
|
||||||
|
print("No paper_trades.csv yet."); return
|
||||||
|
pt = pd.read_csv(PAPER_LOG)
|
||||||
|
open_bets = pt[pt["result"].isna()] if "result" in pt.columns else pt
|
||||||
|
if open_bets.empty:
|
||||||
|
print("No open bets to settle.");
|
||||||
|
# settle from training CSV scores if present, else needs DB (left as note)
|
||||||
|
src = pd.read_csv(TRAIN_CSV, low_memory=False, usecols=["match_id","score_home","score_away"])
|
||||||
|
sc = src.set_index("match_id")
|
||||||
|
def res(row):
|
||||||
|
if not pd.isna(row.get("result")): return row["result"]
|
||||||
|
m = sc.index == row["match_id"]
|
||||||
|
if not m.any(): return np.nan
|
||||||
|
r = sc[m].iloc[0]; sh, sa = r["score_home"], r["score_away"]
|
||||||
|
if pd.isna(sh): return np.nan
|
||||||
|
outcome = "1" if sh > sa else ("X" if sh == sa else "2")
|
||||||
|
won = (str(row["pick"]) == outcome)
|
||||||
|
return "WON" if won else "LOST"
|
||||||
|
pt["result"] = pt.apply(res, axis=1)
|
||||||
|
pt["pnl"] = pt.apply(lambda r: (r["odds"]-1.0) if r["result"]=="WON"
|
||||||
|
else (-1.0 if r["result"]=="LOST" else np.nan), axis=1)
|
||||||
|
pt.to_csv(PAPER_LOG, index=False)
|
||||||
|
s = pt.dropna(subset=["pnl"])
|
||||||
|
if len(s):
|
||||||
|
print(f"Settled {len(s)} bets: hit={100*(s['result']=='WON').mean():.1f}% "
|
||||||
|
f"ROI={100*s['pnl'].sum()/len(s):+.2f}% net={s['pnl'].sum():+.1f}u")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--features", help="CSV of upcoming matches in training schema")
|
||||||
|
ap.add_argument("--demo", action="store_true", help="use tail of training CSV as 'today'")
|
||||||
|
ap.add_argument("--n", type=int, default=20)
|
||||||
|
ap.add_argument("--lo", type=float, default=1.5)
|
||||||
|
ap.add_argument("--hi", type=float, default=2.2)
|
||||||
|
ap.add_argument("--margin", type=float, default=0.03)
|
||||||
|
ap.add_argument("--settle", action="store_true")
|
||||||
|
ap.add_argument("--log", action="store_true", help="append staked picks to paper_trades.csv")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.settle:
|
||||||
|
settle(); return
|
||||||
|
|
||||||
|
bst, feats, meta = load_model()
|
||||||
|
print(f"Model {meta['version']} (trained {meta['trained_at']}, holdout "
|
||||||
|
f"ROI {meta['holdout_eval']['roi_pct']}%) band[{args.lo},{args.hi}] margin {args.margin}\n")
|
||||||
|
|
||||||
|
if args.features:
|
||||||
|
df = pd.read_csv(args.features, low_memory=False)
|
||||||
|
demo = False
|
||||||
|
else:
|
||||||
|
df = pd.read_csv(TRAIN_CSV, low_memory=False).sort_values("mst_utc").tail(args.n).reset_index(drop=True)
|
||||||
|
demo = True
|
||||||
|
print("(DEMO: last matches of training CSV as stand-in for today)\n")
|
||||||
|
|
||||||
|
picks = pick_for_rows(df, bst, feats, args.lo, args.hi, args.margin)
|
||||||
|
staked = [p for p in picks if p["staked"]]
|
||||||
|
print(f"{len(df)} matches scanned -> {len(staked)} STAKED MS picks\n")
|
||||||
|
print(f" {'match_id':<28}{'pick':>5}{'odds':>7}{'model%':>8}{'edge%':>7}" + (" result" if demo else ""))
|
||||||
|
print(" "+"-"*60)
|
||||||
|
log_rows = []
|
||||||
|
for p in picks:
|
||||||
|
if not p["staked"]: continue
|
||||||
|
r = df.iloc[p["idx"]]; mid = str(r["match_id"])
|
||||||
|
res = ""
|
||||||
|
if demo:
|
||||||
|
sh, sa = r.get("score_home"), r.get("score_away")
|
||||||
|
if pd.notna(sh):
|
||||||
|
out = "1" if sh>sa else ("X" if sh==sa else "2")
|
||||||
|
res = " WON" if p["pick"]==out else " lost"
|
||||||
|
print(f" {mid:<28}{p['pick']:>5}{p['odds']:>7.2f}{100*p['model_prob']:>8.1f}{100*p['edge']:>+7.1f}{res}")
|
||||||
|
log_rows.append({"logged_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"match_id": mid, "market": "MS", "pick": p["pick"], "odds": p["odds"],
|
||||||
|
"model_prob": p["model_prob"], "edge": p["edge"], "stake": 1.0,
|
||||||
|
"result": np.nan, "pnl": np.nan})
|
||||||
|
if args.log and log_rows and not demo:
|
||||||
|
new = pd.DataFrame(log_rows)
|
||||||
|
if os.path.exists(PAPER_LOG):
|
||||||
|
new = pd.concat([pd.read_csv(PAPER_LOG), new], ignore_index=True)
|
||||||
|
new.to_csv(PAPER_LOG, index=False)
|
||||||
|
print(f"\n logged {len(log_rows)} picks -> {PAPER_LOG}")
|
||||||
|
elif args.log and demo:
|
||||||
|
print("\n (--log ignored in --demo; only real upcoming picks are logged)")
|
||||||
|
print("\nReminder: paper-trade only. Stake real money after weeks of forward")
|
||||||
|
print("CLV>0 + ROI>0 (settle with --settle, check scoreboard/clv_report).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
"""
|
||||||
|
Live Scoreboard — the single source of truth for real betting performance.
|
||||||
|
=========================================================================
|
||||||
|
Reads the *forward-tracked* results in `prediction_runs` (one row per analyzed
|
||||||
|
match, with the staked main pick + actual outcome + realized unit_profit) and
|
||||||
|
reports what ACTUALLY happened with real money logic — NOT a backtest.
|
||||||
|
|
||||||
|
Why this exists: backtests on this codebase are overfit (a paper "+32.7% ROI"
|
||||||
|
strategy that the live engine never even ran). The only trustworthy number is
|
||||||
|
the realized P/L recorded after matches settle. This tool surfaces it.
|
||||||
|
|
||||||
|
Read-only. SELECT only. Safe to run anytime.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/live_scoreboard.py
|
||||||
|
python scripts/live_scoreboard.py --days 30
|
||||||
|
python scripts/live_scoreboard.py --version v28-pro-max
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
# utf-8 stdout so Turkish market/league names never crash on Windows cp1252
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
|
||||||
|
sys.path.insert(0, AI_ENGINE_DIR)
|
||||||
|
|
||||||
|
from data.db import get_clean_dsn # noqa: E402
|
||||||
|
import psycopg2 # noqa: E402
|
||||||
|
from psycopg2.extras import RealDictCursor # noqa: E402
|
||||||
|
|
||||||
|
ODDS_BANDS = [(0, 1.5, "<1.5"), (1.5, 2.0, "1.5-2"), (2.0, 3.0, "2-3"),
|
||||||
|
(3.0, 5.0, "3-5"), (5.0, 6.0, "5-6"), (6.0, 7.5, "6-7.5"),
|
||||||
|
(7.5, 999, "7.5+")]
|
||||||
|
|
||||||
|
|
||||||
|
def _f(x: Any, d: Optional[float] = None) -> Optional[float]:
|
||||||
|
try:
|
||||||
|
return float(x) if x is not None else d
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(j: Any) -> Dict[str, Any]:
|
||||||
|
if isinstance(j, str):
|
||||||
|
try:
|
||||||
|
return json.loads(j)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return j or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _band(odds: Optional[float]) -> str:
|
||||||
|
if odds is None:
|
||||||
|
return "?"
|
||||||
|
for lo, hi, name in ODDS_BANDS:
|
||||||
|
if lo <= odds < hi:
|
||||||
|
return name
|
||||||
|
return "?"
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_rows(args) -> List[Dict[str, Any]]:
|
||||||
|
dsn = get_clean_dsn()
|
||||||
|
where = ["eventual_outcome IS NOT NULL"]
|
||||||
|
params: List[Any] = []
|
||||||
|
if args.version:
|
||||||
|
where.append("engine_version = %s")
|
||||||
|
params.append(args.version)
|
||||||
|
if args.days:
|
||||||
|
cutoff = datetime.now(timezone.utc) - timedelta(days=args.days)
|
||||||
|
where.append("generated_at >= %s")
|
||||||
|
params.append(cutoff)
|
||||||
|
sql = f"""
|
||||||
|
SELECT match_id, engine_version, generated_at, eventual_outcome,
|
||||||
|
unit_profit, payload_summary
|
||||||
|
FROM prediction_runs
|
||||||
|
WHERE {' AND '.join(where)}
|
||||||
|
ORDER BY generated_at ASC
|
||||||
|
"""
|
||||||
|
with psycopg2.connect(dsn) as conn:
|
||||||
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute(sql, params)
|
||||||
|
return cur.fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
def distill(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
|
"""One analytic record per run with the staked pick + realized P/L."""
|
||||||
|
out = []
|
||||||
|
for r in rows:
|
||||||
|
ps = _parse(r["payload_summary"])
|
||||||
|
mp = ps.get("main_pick") or {}
|
||||||
|
playable = bool(mp.get("playable"))
|
||||||
|
stake = _f(mp.get("stake_units"), 0.0) or 0.0
|
||||||
|
profit = _f(r["unit_profit"], 0.0) or 0.0
|
||||||
|
outcome = str(r["eventual_outcome"] or "")
|
||||||
|
staked = playable and stake > 0
|
||||||
|
# settled stake = a real bet with a win/loss (exclude NO_BET / push)
|
||||||
|
settled_stake = staked and not outcome.startswith(("NO_BET", "PUSH", "VOID", "CANCEL"))
|
||||||
|
out.append({
|
||||||
|
"match_id": r["match_id"],
|
||||||
|
"version": r["engine_version"],
|
||||||
|
"ts": r["generated_at"],
|
||||||
|
"market": mp.get("market") or "?",
|
||||||
|
"pick": mp.get("pick"),
|
||||||
|
"odds": _f(mp.get("odds")),
|
||||||
|
"stake": stake,
|
||||||
|
"profit": profit,
|
||||||
|
"outcome": outcome,
|
||||||
|
"staked": staked,
|
||||||
|
"settled_stake": settled_stake,
|
||||||
|
"win": settled_stake and profit > 0,
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _agg(recs: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
# NOTE: recorded unit_profit is on a FLAT 1u basis (win=odds-1, loss=-1),
|
||||||
|
# independent of the brain's suggested stake_units. So ROI is profit per
|
||||||
|
# bet at 1u flat = profit / n. (Using stake_units as denominator is wrong:
|
||||||
|
# it double-counts and produces impossible >100% losses.)
|
||||||
|
s = [r for r in recs if r["settled_stake"]]
|
||||||
|
n = len(s)
|
||||||
|
wins = sum(1 for r in s if r["win"])
|
||||||
|
sug_stake = sum(r["stake"] for r in s)
|
||||||
|
profit = sum(r["profit"] for r in s)
|
||||||
|
return {
|
||||||
|
"n": n,
|
||||||
|
"wins": wins,
|
||||||
|
"hit_pct": round(100.0 * wins / n, 1) if n else None,
|
||||||
|
"sug_stake": round(sug_stake, 2),
|
||||||
|
"profit": round(profit, 2),
|
||||||
|
"roi_pct": round(100.0 * profit / n, 1) if n else None, # flat 1u
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _line(label: str, a: Dict[str, Any]) -> str:
|
||||||
|
return (f" {label:<14} n={a['n']:>4} hit={str(a['hit_pct'] if a['hit_pct'] is not None else '-'):>5}% "
|
||||||
|
f"profit={a['profit']:>8.2f}u ROI(flat1u)={str(a['roi_pct'] if a['roi_pct'] is not None else '-'):>7}%")
|
||||||
|
|
||||||
|
|
||||||
|
def risk_metrics(recs: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
s = [r for r in sorted(recs, key=lambda x: x["ts"]) if r["settled_stake"]]
|
||||||
|
cum = 0.0
|
||||||
|
peak = 0.0
|
||||||
|
max_dd = 0.0
|
||||||
|
streak = 0
|
||||||
|
worst_streak = 0
|
||||||
|
for r in s:
|
||||||
|
cum += r["profit"]
|
||||||
|
peak = max(peak, cum)
|
||||||
|
max_dd = min(max_dd, cum - peak)
|
||||||
|
if r["profit"] <= 0:
|
||||||
|
streak += 1
|
||||||
|
worst_streak = max(worst_streak, streak)
|
||||||
|
else:
|
||||||
|
streak = 0
|
||||||
|
return {"max_drawdown_u": round(max_dd, 2),
|
||||||
|
"longest_losing_streak": worst_streak,
|
||||||
|
"final_cum_u": round(cum, 2)}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--days", type=int, default=None, help="Only last N days")
|
||||||
|
ap.add_argument("--version", help="Filter by engine_version")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
rows = fetch_rows(args)
|
||||||
|
recs = distill(rows)
|
||||||
|
|
||||||
|
print("=" * 74)
|
||||||
|
print("LIVE SCOREBOARD — realized results from prediction_runs (NOT backtest)")
|
||||||
|
print("=" * 74)
|
||||||
|
if recs:
|
||||||
|
lo = min(r["ts"] for r in recs).date()
|
||||||
|
hi = max(r["ts"] for r in recs).date()
|
||||||
|
print(f"window: {lo} .. {hi} settled runs: {len(recs)}"
|
||||||
|
+ (f" filter: {args.version}" if args.version else ""))
|
||||||
|
print()
|
||||||
|
|
||||||
|
overall = _agg(recs)
|
||||||
|
print("OVERALL (staked = playable bets only)")
|
||||||
|
print(_line("ALL", overall))
|
||||||
|
no_bet = sum(1 for r in recs if not r["staked"])
|
||||||
|
print(f" (analyzed {len(recs)} matches; {overall['n']} actually staked, "
|
||||||
|
f"{no_bet} NO_BET)")
|
||||||
|
if overall["n"]:
|
||||||
|
rm = risk_metrics(recs)
|
||||||
|
print(f" max drawdown: {rm['max_drawdown_u']}u "
|
||||||
|
f"longest losing streak: {rm['longest_losing_streak']} "
|
||||||
|
f"net: {rm['final_cum_u']}u")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("BY ENGINE VERSION")
|
||||||
|
by_v = defaultdict(list)
|
||||||
|
for r in recs:
|
||||||
|
by_v[r["version"]].append(r)
|
||||||
|
for v, rs in sorted(by_v.items(), key=lambda kv: -len(kv[1])):
|
||||||
|
print(_line(v, _agg(rs)))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("BY MARKET (staked)")
|
||||||
|
by_m = defaultdict(list)
|
||||||
|
for r in recs:
|
||||||
|
if r["settled_stake"]:
|
||||||
|
by_m[r["market"]].append(r)
|
||||||
|
for m, rs in sorted(by_m.items(), key=lambda kv: -len(kv[1])):
|
||||||
|
print(_line(m, _agg(rs)))
|
||||||
|
if not by_m:
|
||||||
|
print(" (no staked settled bets in window)")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("BY ODDS BAND (staked)")
|
||||||
|
by_b = defaultdict(list)
|
||||||
|
for r in recs:
|
||||||
|
if r["settled_stake"]:
|
||||||
|
by_b[_band(r["odds"])].append(r)
|
||||||
|
for _, _, name in ODDS_BANDS:
|
||||||
|
if name in by_b:
|
||||||
|
print(_line(name, _agg(by_b[name])))
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("WEEKLY TREND (staked)")
|
||||||
|
by_w = defaultdict(list)
|
||||||
|
for r in recs:
|
||||||
|
if r["settled_stake"]:
|
||||||
|
iso = r["ts"].isocalendar()
|
||||||
|
by_w[f"{iso[0]}-W{iso[1]:02d}"].append(r)
|
||||||
|
for w in sorted(by_w):
|
||||||
|
a = _agg(by_w[w])
|
||||||
|
print(_line(w, a))
|
||||||
|
print()
|
||||||
|
print("=" * 74)
|
||||||
|
print("READ: ROI < 0 over a meaningful sample = the staked signals are not")
|
||||||
|
print("profitable. 'NO_BET' rows are free (no stake). CLV is unmeasurable")
|
||||||
|
print("until odds movement is captured (see scripts + odds_history fix).")
|
||||||
|
print("=" * 74)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
Market Calibration Scan — find where the ODDS THEMSELVES are systematically wrong.
|
||||||
|
=================================================================================
|
||||||
|
The legit, measurable version of "odds şike": pockets (leagues / teams / bands)
|
||||||
|
where the market's implied probability does NOT match realized frequency, so a
|
||||||
|
SIMPLE rule (no model) is +EV. This is pure market inefficiency — soft pricing
|
||||||
|
in obscure leagues, persistent team bias, etc.
|
||||||
|
|
||||||
|
Discipline against false 'rigged' pockets (the multiple-comparison trap):
|
||||||
|
* split history by time into HALF-1 (discover) and HALF-2 (validate)
|
||||||
|
* a pocket counts ONLY if it is +EV in BOTH halves with enough bets each
|
||||||
|
* report realized-vs-implied gap (the miscalibration) + ROI
|
||||||
|
|
||||||
|
No model. Just odds vs outcomes. Read-only on the training CSV (104k matches
|
||||||
|
with odds). Forward 'suspicious line movement' detection needs odds_history
|
||||||
|
(currently empty) — separate, forward-only.
|
||||||
|
|
||||||
|
Usage: python scripts/market_calibration.py --min-bets 120 --side fav
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, os, sys
|
||||||
|
import numpy as np, pandas as pd
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
|
||||||
|
|
||||||
|
def league_names(ids):
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, AI_DIR)
|
||||||
|
from data.db import get_clean_dsn
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
ids = [str(i) for i in ids if i is not None]
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as c:
|
||||||
|
with c.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute("SELECT id,name FROM leagues WHERE id = ANY(%s)", (ids,))
|
||||||
|
return {str(r["id"]): r["name"] for r in cur.fetchall()}
|
||||||
|
except Exception:
|
||||||
|
import time; time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def team_names(ids):
|
||||||
|
try:
|
||||||
|
sys.path.insert(0, AI_DIR)
|
||||||
|
from data.db import get_clean_dsn
|
||||||
|
import psycopg2
|
||||||
|
from psycopg2.extras import RealDictCursor
|
||||||
|
ids = [str(i) for i in ids if i is not None]
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as c:
|
||||||
|
with c.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute("SELECT id,name FROM teams WHERE id = ANY(%s)", (ids,))
|
||||||
|
return {str(r["id"]): r["name"] for r in cur.fetchall()}
|
||||||
|
except Exception:
|
||||||
|
import time; time.sleep(1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--min-bets", type=int, default=120, help="min bets PER HALF")
|
||||||
|
ap.add_argument("--fav-max", type=float, default=2.5, help="only count favourites below this odds")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
df = pd.read_csv(CSV, low_memory=False,
|
||||||
|
usecols=["match_id","league_id","home_team_id","away_team_id","mst_utc",
|
||||||
|
"odds_ms_h","odds_ms_d","odds_ms_a","score_home","score_away"])
|
||||||
|
df = df.sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh = pd.to_numeric(df["score_home"],errors="coerce"); sa = pd.to_numeric(df["score_away"],errors="coerce")
|
||||||
|
ok = sh.notna()&sa.notna()
|
||||||
|
df = df[ok].reset_index(drop=True); sh=sh[ok.values].values; sa=sa[ok.values].values
|
||||||
|
O = df[["odds_ms_h","odds_ms_d","odds_ms_a"]].apply(pd.to_numeric,errors="coerce").fillna(0.0).values
|
||||||
|
valid = (O>1.0).all(1)
|
||||||
|
outcome = np.where(sh>sa,0,np.where(sh==sa,1,2)) # 0 home,1 draw,2 away
|
||||||
|
fav = O.argmin(1); fav_odds = O[np.arange(len(O)),fav]
|
||||||
|
fav_won = (fav==outcome).astype(float)
|
||||||
|
fav_implied = 1.0/fav_odds
|
||||||
|
pnl = np.where(fav_won, fav_odds-1.0, -1.0)
|
||||||
|
half = (np.arange(len(df)) >= len(df)//2).astype(int) # 0=first half,1=second
|
||||||
|
use = valid & (fav_odds <= args.fav_max)
|
||||||
|
|
||||||
|
base = pd.DataFrame({
|
||||||
|
"league": df["league_id"].astype(str).values,
|
||||||
|
"home": df["home_team_id"].astype(str).values,
|
||||||
|
"fav_is_home": (fav==0),
|
||||||
|
"won": fav_won, "implied": fav_implied, "pnl": pnl, "half": half, "use": use,
|
||||||
|
"fav_odds": fav_odds,
|
||||||
|
})
|
||||||
|
b = base[base["use"]].copy()
|
||||||
|
print(f"{len(b):,} favourite bets (odds<= {args.fav_max}); split into 2 time halves\n")
|
||||||
|
print(f"GLOBAL favourite: realized={100*b['won'].mean():.1f}% implied={100*b['implied'].mean():.1f}% "
|
||||||
|
f"ROI={100*b['pnl'].mean():+.2f}% (negative = vig; market roughly right)")
|
||||||
|
|
||||||
|
def scan(groupcol, label, namefn, min_bets):
|
||||||
|
rows=[]
|
||||||
|
for key,d in b.groupby(groupcol):
|
||||||
|
h0=d[d["half"]==0]; h1=d[d["half"]==1]
|
||||||
|
if len(h0)<min_bets or len(h1)<min_bets: continue
|
||||||
|
r0=100*h0["pnl"].mean(); r1=100*h1["pnl"].mean()
|
||||||
|
# miscalibration gap: realized - implied (positive = market underprices the favourite)
|
||||||
|
gap=100*(d["won"].mean()-d["implied"].mean())
|
||||||
|
both_pos = r0>0 and r1>0
|
||||||
|
rows.append((min(r0,r1), key, len(d), 100*d["pnl"].mean(), r0, r1, gap, both_pos))
|
||||||
|
rows.sort(reverse=True)
|
||||||
|
names = namefn([r[1] for r in rows[:40]])
|
||||||
|
print(f"\n{'='*82}\n{label} (✓ = +EV in BOTH halves, the only trustworthy ones)\n{'='*82}")
|
||||||
|
print(f" {'name':<30}{'n':>6}{'ROI%':>7}{'H1%':>7}{'H2%':>7}{'gap%':>7} ✓")
|
||||||
|
print(" "+"-"*72)
|
||||||
|
shown=0
|
||||||
|
for mn,key,n,roi,r0,r1,gap,both in rows:
|
||||||
|
if shown>=20 and not both: continue
|
||||||
|
nm=(names.get(key,key) or key)[:28]
|
||||||
|
mark = "✓" if both else ""
|
||||||
|
print(f" {nm:<30}{n:>6}{roi:>+7.1f}{r0:>+7.1f}{r1:>+7.1f}{gap:>+7.1f} {mark}")
|
||||||
|
shown+=1
|
||||||
|
if shown>=25: break
|
||||||
|
good=[r for r in rows if r[7]]
|
||||||
|
print(f"\n -> {len(good)} {label.split()[0].lower()} pockets are +EV in BOTH halves "
|
||||||
|
f"(out of {len(rows)} with enough data)")
|
||||||
|
return good
|
||||||
|
|
||||||
|
scan("league", "BY LEAGUE (favourite flat bet)", league_names, args.min_bets)
|
||||||
|
# team: only when the team is the home favourite (cleanest, most samples)
|
||||||
|
bt = b[b["fav_is_home"]]
|
||||||
|
globals()['b'] = bt # reuse scan on home-favourite subset
|
||||||
|
# inline team scan
|
||||||
|
rows=[]
|
||||||
|
for key,d in bt.groupby("home"):
|
||||||
|
h0=d[d["half"]==0]; h1=d[d["half"]==1]
|
||||||
|
if len(h0)<max(25,args.min_bets//3) or len(h1)<max(25,args.min_bets//3): continue
|
||||||
|
r0=100*h0["pnl"].mean(); r1=100*h1["pnl"].mean()
|
||||||
|
gap=100*(d["won"].mean()-d["implied"].mean())
|
||||||
|
rows.append((min(r0,r1), key, len(d), 100*d["pnl"].mean(), r0, r1, gap, r0>0 and r1>0))
|
||||||
|
rows.sort(reverse=True)
|
||||||
|
tn=team_names([r[1] for r in rows[:40]])
|
||||||
|
print(f"\n{'='*82}\nBY TEAM as HOME FAVOURITE (✓ = +EV both halves)\n{'='*82}")
|
||||||
|
print(f" {'team':<30}{'n':>6}{'ROI%':>7}{'H1%':>7}{'H2%':>7}{'gap%':>7} ✓")
|
||||||
|
print(" "+"-"*72)
|
||||||
|
for mn,key,n,roi,r0,r1,gap,both in rows[:22]:
|
||||||
|
nm=(tn.get(key,key) or key)[:28]; mark="✓" if both else ""
|
||||||
|
print(f" {nm:<30}{n:>6}{roi:>+7.1f}{r0:>+7.1f}{r1:>+7.1f}{gap:>+7.1f} {mark}")
|
||||||
|
good=[r for r in rows if r[7]]
|
||||||
|
print(f"\n -> {len(good)} teams +EV in BOTH halves (out of {len(rows)})")
|
||||||
|
print("\nREAD: ✓ pockets survived a time-split = candidate real inefficiencies (not noise).")
|
||||||
|
print("Still forward-validate with CLV. No ✓ = market is efficient there; don't bet.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
Match Report — calibrated outcome probabilities + loss-minimizing pick per match.
|
||||||
|
================================================================================
|
||||||
|
For each match, shows the model's CALIBRATED probability for every outcome
|
||||||
|
(1X2, Double Chance, OU 1.5/2.5/3.5, BTTS, HT), next to the market's implied
|
||||||
|
probability, and recommends:
|
||||||
|
* EN GÜVENLİ = highest-probability outcome (most likely to hit / lowest variance)
|
||||||
|
* EN İYİ DEĞER = least-negative-EV outcome (smartest bet given the margin)
|
||||||
|
|
||||||
|
Probabilities are leak-free and calibrated (ECE ~0.43%, see calibration_report).
|
||||||
|
This is a LOSS-MINIMIZER, not a profit machine — accurate probabilities to make
|
||||||
|
the smartest, least-losing decisions against İddaa's high margin.
|
||||||
|
|
||||||
|
Trains the market models on the full history (leak-free), then scores the input.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/match_report.py --features data/upcoming_features.csv
|
||||||
|
python scripts/match_report.py --demo --n 6
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, os, sys, time
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, AI_DIR)
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
META = {"match_id","home_team_id","away_team_id","league_id","mst_utc",
|
||||||
|
"score_home","score_away","ht_score_home","ht_score_away"}
|
||||||
|
LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals",
|
||||||
|
"squad_diff","home_squad_quality","away_squad_quality",
|
||||||
|
"referee_home_bias","referee_avg_goals"}
|
||||||
|
|
||||||
|
def ou(line): return lambda sh,sa,hh,ha: (0 if (sh+sa) > line else 1)
|
||||||
|
def htou(line):return lambda sh,sa,hh,ha: (None if np.isnan(hh) else (0 if (hh+ha)>line else 1))
|
||||||
|
MARKETS = {
|
||||||
|
"MS": ("multi", ["odds_ms_h","odds_ms_d","odds_ms_a"], ["1","X","2"],
|
||||||
|
lambda sh,sa,hh,ha: 0 if sh>sa else (1 if sh==sa else 2)),
|
||||||
|
"OU15": ("binary",["odds_ou15_o","odds_ou15_u"], ["1.5 Üst","1.5 Alt"], ou(1.5)),
|
||||||
|
"OU25": ("binary",["odds_ou25_o","odds_ou25_u"], ["2.5 Üst","2.5 Alt"], ou(2.5)),
|
||||||
|
"OU35": ("binary",["odds_ou35_o","odds_ou35_u"], ["3.5 Üst","3.5 Alt"], ou(3.5)),
|
||||||
|
"BTTS": ("binary",["odds_btts_y","odds_btts_n"], ["KG Var","KG Yok"],
|
||||||
|
lambda sh,sa,hh,ha: 0 if (sh>0 and sa>0) else 1),
|
||||||
|
"HT": ("multi", ["odds_ht_ms_h","odds_ht_ms_d","odds_ht_ms_a"], ["İY 1","İY X","İY 2"],
|
||||||
|
lambda sh,sa,hh,ha: None if np.isnan(hh) else (0 if hh>ha else (1 if hh==ha else 2))),
|
||||||
|
}
|
||||||
|
PM={"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05,"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
PB={"objective":"binary:logistic","max_depth":5,"eta":0.05,"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
|
||||||
|
|
||||||
|
def team_names(ids):
|
||||||
|
try:
|
||||||
|
from data.db import get_clean_dsn
|
||||||
|
import psycopg2; from psycopg2.extras import RealDictCursor
|
||||||
|
ids=[str(i) for i in ids]
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
with psycopg2.connect(get_clean_dsn()) as c:
|
||||||
|
with c.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute("SELECT id,name FROM teams WHERE id = ANY(%s)",(ids,))
|
||||||
|
return {str(r["id"]):r["name"] for r in cur.fetchall()}
|
||||||
|
except Exception: time.sleep(1)
|
||||||
|
except Exception: pass
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap=argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--features"); ap.add_argument("--demo",action="store_true")
|
||||||
|
ap.add_argument("--n",type=int,default=8); ap.add_argument("--estimators",type=int,default=250)
|
||||||
|
args=ap.parse_args()
|
||||||
|
|
||||||
|
df=pd.read_csv(CSV,low_memory=False).sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh=pd.to_numeric(df["score_home"],errors="coerce"); sa=pd.to_numeric(df["score_away"],errors="coerce")
|
||||||
|
ok=sh.notna()&sa.notna(); df=df[ok].reset_index(drop=True)
|
||||||
|
SH=sh[ok.values].values.astype(float); SA=sa[ok.values].values.astype(float)
|
||||||
|
HH=pd.to_numeric(df["ht_score_home"],errors="coerce").values.astype(float)
|
||||||
|
HA=pd.to_numeric(df["ht_score_away"],errors="coerce").values.astype(float)
|
||||||
|
feats=[c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY]
|
||||||
|
X=df[feats].apply(pd.to_numeric,errors="coerce").fillna(0.0).values
|
||||||
|
N=len(df)
|
||||||
|
print(f"Training {len(MARKETS)} leak-free calibrated market models on {N:,} matches ...",flush=True)
|
||||||
|
models={}
|
||||||
|
for m,(kind,oc,picks,tfn) in MARKETS.items():
|
||||||
|
truth=np.array([tfn(SH[i],SA[i],HH[i],HA[i]) for i in range(N)],dtype=object)
|
||||||
|
valid=np.array([v is not None for v in truth])
|
||||||
|
if kind=="multi":
|
||||||
|
b=xgb.train(PM,xgb.DMatrix(X[valid],label=truth[valid].astype(int)),num_boost_round=args.estimators)
|
||||||
|
else:
|
||||||
|
b=xgb.train(PB,xgb.DMatrix(X[valid],label=(truth[valid].astype(int)==0).astype(int)),num_boost_round=args.estimators)
|
||||||
|
models[m]=(kind,oc,picks,b)
|
||||||
|
|
||||||
|
# input matches
|
||||||
|
if args.features:
|
||||||
|
inp=pd.read_csv(args.features,low_memory=False); demo=False
|
||||||
|
else:
|
||||||
|
inp=df.tail(args.n).reset_index(drop=True); demo=True
|
||||||
|
print("(DEMO: training CSV son maçları)\n")
|
||||||
|
names=team_names(list(inp.get("home_team_id",[]))+list(inp.get("away_team_id",[]))) if "home_team_id" in inp.columns else {}
|
||||||
|
Xi=inp.reindex(columns=feats).apply(pd.to_numeric,errors="coerce").fillna(0.0).values
|
||||||
|
|
||||||
|
shown=0
|
||||||
|
for i in range(len(inp)):
|
||||||
|
if shown>=args.n: break
|
||||||
|
r=inp.iloc[i]; xrow=Xi[i:i+1]
|
||||||
|
hn=names.get(str(r.get("home_team_id")),str(r.get("home_team_id","?"))[:8])
|
||||||
|
an=names.get(str(r.get("away_team_id")),str(r.get("away_team_id","?"))[:8])
|
||||||
|
print("="*68)
|
||||||
|
print(f"{hn} vs {an}")
|
||||||
|
print(f" {'market':<8}{'sonuç':<10}{'model%':>8}{'piyasa%':>9}{'oran':>7}{'EV%':>8}")
|
||||||
|
print(" "+"-"*58)
|
||||||
|
bets=[]; ms_probs=None
|
||||||
|
for m,(kind,oc,picks,b) in models.items():
|
||||||
|
if kind=="multi":
|
||||||
|
P=b.predict(xgb.DMatrix(xrow))[0]
|
||||||
|
else:
|
||||||
|
p=float(b.predict(xgb.DMatrix(xrow))[0]); P=np.array([p,1-p])
|
||||||
|
if m=="MS": ms_probs=P
|
||||||
|
O=pd.to_numeric(r.reindex(oc),errors="coerce").fillna(0.0).values
|
||||||
|
for k in range(len(picks)):
|
||||||
|
o=float(O[k]); mp=float(P[k])
|
||||||
|
if o>1.0:
|
||||||
|
imp=1/o; ev=mp*o-1
|
||||||
|
print(f" {m:<8}{picks[k]:<10}{100*mp:>7.0f}%{100*imp:>8.0f}%{o:>7.2f}{100*ev:>+7.1f}")
|
||||||
|
bets.append((m,picks[k],mp,o,ev))
|
||||||
|
else:
|
||||||
|
print(f" {m:<8}{picks[k]:<10}{100*mp:>7.0f}%{'-':>8} {'-':>6} {'-':>7}")
|
||||||
|
# Double Chance derived from MS (no odds shown — Nesine'de oranına bakarsın)
|
||||||
|
if ms_probs is not None:
|
||||||
|
h,d,a=ms_probs
|
||||||
|
print(f" {'DC':<8}{'1X':<10}{100*(h+d):>7.0f}% (türetilmiş 'en güvenli' seçenek)")
|
||||||
|
print(f" {'DC':<8}{'X2':<10}{100*(d+a):>7.0f}%")
|
||||||
|
print(f" {'DC':<8}{'12':<10}{100*(h+a):>7.0f}%")
|
||||||
|
print(" "+"-"*58)
|
||||||
|
if bets:
|
||||||
|
safe=max(bets,key=lambda x:x[2]) # highest probability
|
||||||
|
value=max(bets,key=lambda x:x[4]) # least-negative EV
|
||||||
|
print(f" >>> EN GÜVENLİ : {safe[0]} {safe[1]} (model %{100*safe[2]:.0f}, oran {safe[3]:.2f})")
|
||||||
|
print(f" >>> EN İYİ DEĞER: {value[0]} {value[1]} (EV %{100*value[4]:+.1f}, model %{100*value[2]:.0f}, oran {value[3]:.2f})")
|
||||||
|
if value[4] <= 0:
|
||||||
|
print(f" (EV negatif → marj yüzünden 'kâr' yok; en az kaybettiren bu. Değer yoksa PAS geç.)")
|
||||||
|
shown+=1
|
||||||
|
print("\nNOT: olasılıklar kalibre (model %X ⇒ gerçekte ~%X). EV<0 her yerde olabilir")
|
||||||
|
print("(İddaa marjı); amaç KAYBI MİNİMİZE etmek + en doğru maç okumasını görmek.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"""
|
||||||
|
Odds Movement Monitor — opening→closing line movement + steam radar.
|
||||||
|
===================================================================
|
||||||
|
Reads live_odds_history (filled by data-fetcher.task.ts every 15 min for
|
||||||
|
upcoming matches, all markets) and reports, PER MATCH:
|
||||||
|
* opening odd (first capture) vs closing odd (latest capture)
|
||||||
|
* total move % = (closing - opening) / opening ← the headline signal
|
||||||
|
* the steam side (the selection that shortened the most = money/info/şike)
|
||||||
|
|
||||||
|
Why opening→closing matters: it is the market's TOTAL revision. A side that
|
||||||
|
shortened a lot from open to close = the market learned something. If you can
|
||||||
|
bet EARLY (before the shortening), that gap is real value (positive CLV) — the
|
||||||
|
one realistic edge vs İddaa. As a closing bettor it's a RISK FILTER: heavy
|
||||||
|
late steam against your pick = skip.
|
||||||
|
|
||||||
|
Capture is done by the NestJS cron now (DB); this is a pure READER.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/monitor_odds_movement.py # MS movers
|
||||||
|
python scripts/monitor_odds_movement.py --min-move 0.08 --market "Maç Sonucu"
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, os, sys, time
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
sys.path.insert(0, AI_DIR)
|
||||||
|
from data.db import get_clean_dsn # noqa: E402
|
||||||
|
import psycopg2 # noqa: E402
|
||||||
|
from psycopg2.extras import RealDictCursor # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def connect():
|
||||||
|
last = None
|
||||||
|
for _ in range(8):
|
||||||
|
try:
|
||||||
|
return psycopg2.connect(get_clean_dsn())
|
||||||
|
except Exception as e:
|
||||||
|
last = e; time.sleep(3)
|
||||||
|
raise last
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--min-move", type=float, default=0.05,
|
||||||
|
help="flag matches whose focus-market move >= this fraction (default 0.05)")
|
||||||
|
ap.add_argument("--market", default="Maç Sonucu", help="focus market for the watchlist")
|
||||||
|
ap.add_argument("--limit", type=int, default=25)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
with connect() as c, c.cursor(cursor_factory=RealDictCursor) as cur:
|
||||||
|
cur.execute("SELECT to_regclass('public.live_odds_history') AS ex")
|
||||||
|
if not cur.fetchall()[0]["ex"]:
|
||||||
|
print("live_odds_history yok — NestJS cron'u henüz yazmamış (deploy/build kontrol)."); return
|
||||||
|
|
||||||
|
# opening (earliest) + closing (latest) per match/market/selection
|
||||||
|
cur.execute("""
|
||||||
|
SELECT match_id, market, selection,
|
||||||
|
(array_agg(new_value ORDER BY change_time ASC))[1] AS opening,
|
||||||
|
(array_agg(new_value ORDER BY change_time DESC))[1] AS closing,
|
||||||
|
count(*) AS ticks
|
||||||
|
FROM live_odds_history
|
||||||
|
GROUP BY match_id, market, selection
|
||||||
|
""")
|
||||||
|
rows = cur.fetchall()
|
||||||
|
if not rows:
|
||||||
|
print("live_odds_history boş (henüz yakalama yok)."); return
|
||||||
|
|
||||||
|
# per match aggregation
|
||||||
|
by_match = defaultdict(lambda: {"focus": {}, "any_ticks": 0, "max_abs": 0.0})
|
||||||
|
for r in rows:
|
||||||
|
mid = r["match_id"]; o = r["opening"]; cl = r["closing"]
|
||||||
|
d = by_match[mid]
|
||||||
|
d["any_ticks"] = max(d["any_ticks"], r["ticks"])
|
||||||
|
if o and cl and o > 0:
|
||||||
|
mv = (cl - o) / o
|
||||||
|
d["max_abs"] = max(d["max_abs"], abs(mv))
|
||||||
|
if r["market"] == args.market:
|
||||||
|
d["focus"][r["selection"]] = (o, cl, mv)
|
||||||
|
|
||||||
|
# team names + kickoff
|
||||||
|
ids = list(by_match.keys())
|
||||||
|
names = {}
|
||||||
|
if ids:
|
||||||
|
cur.execute("""SELECT lm.id, ht.name h, at.name a, lm.mst_utc
|
||||||
|
FROM live_matches lm
|
||||||
|
JOIN teams ht ON ht.id=lm.home_team_id
|
||||||
|
JOIN teams at ON at.id=lm.away_team_id
|
||||||
|
WHERE lm.id = ANY(%s)""", (ids,))
|
||||||
|
for r in cur.fetchall():
|
||||||
|
names[r["id"]] = (f"{r['h']} v {r['a']}", r["mst_utc"])
|
||||||
|
|
||||||
|
moved = [(m, d) for m, d in by_match.items() if d["any_ticks"] > 1]
|
||||||
|
print("="*78)
|
||||||
|
print("ODDS MOVEMENT — açılış→kapanış (live_odds_history)")
|
||||||
|
print("="*78)
|
||||||
|
print(f"izlenen maç: {len(by_match)} | hareket başlamış (>1 yakalama): {len(moved)}")
|
||||||
|
if not moved:
|
||||||
|
print("\nHenüz hareket yok — hepsi tek yakalama (açılış). Oranlar oynadıkça dolacak.")
|
||||||
|
print("(NestJS 15-dk cron'u her tazelemede değişen oranı ekliyor.)")
|
||||||
|
return
|
||||||
|
|
||||||
|
flagged = sorted(
|
||||||
|
[(m, d) for m, d in moved if d["focus"] and d["max_abs"] >= args.min_move],
|
||||||
|
key=lambda x: -x[1]["max_abs"],
|
||||||
|
)
|
||||||
|
now = int(time.time()*1000)
|
||||||
|
print(f"\n{args.market} hareketi >= %{args.min_move*100:.0f} olan maçlar:")
|
||||||
|
print(f" {'maç':<32}{'sel':>5}{'açılış':>8}{'kapanış':>9}{'hareket':>9}")
|
||||||
|
print(" "+"-"*64)
|
||||||
|
for mid, d in flagged[:args.limit]:
|
||||||
|
nm, mst = names.get(mid, (mid[:30], None))
|
||||||
|
ko = ""
|
||||||
|
if mst:
|
||||||
|
mins = (mst-now)/60000
|
||||||
|
ko = f" KO~{mins/60:.1f}h" if mins > 0 else " (başladı)"
|
||||||
|
# steam side = most shortened (most negative move)
|
||||||
|
steam = min(d["focus"].items(), key=lambda kv: kv[1][2])
|
||||||
|
print(f" {nm[:30]:<32}{'':>5}{'':>8}{'':>9}{'':>9}{ko}")
|
||||||
|
for sel, (o, cl, mv) in d["focus"].items():
|
||||||
|
tag = " ↓STEAM" if sel == steam[0] and mv < 0 else ""
|
||||||
|
print(f" {'':<32}{sel:>5}{o:>8.2f}{cl:>9.2f}{100*mv:>+8.1f}%{tag}")
|
||||||
|
if not flagged:
|
||||||
|
print(" (eşiği geçen yok — hareketler küçük)")
|
||||||
|
print("\nOKUMA: kapanışta oynuyorsan, pick'ine KARŞI ↓STEAM olan maçı PAS geç.")
|
||||||
|
print("Erken oynayabiliyorsan, kısalan tarafı açılışta yakalamak = gerçek değer (CLV).")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""
|
||||||
|
Multi-Market Edge + Best-Bet Selector — pick the best value bet PER MATCH
|
||||||
|
========================================================================
|
||||||
|
Not "play the handed main_pick". For each match, score EVERY market the model
|
||||||
|
covers, compare model prob vs market implied, and select the single best VALUE
|
||||||
|
bet across all markets. Leak-free, walk-forward, honest.
|
||||||
|
|
||||||
|
Markets (truth derived from scores, not trusted labels):
|
||||||
|
MS(1X2), HT-result, OU0.5/1.5/2.5/3.5, HT_OU0.5/1.5, BTTS.
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
(A) per-market value ROI -> which bet types actually carry edge
|
||||||
|
(B) cross-market SELECTOR -> best value bet per match, with odds-band filter,
|
||||||
|
fold-consistency, and the model-free baseline.
|
||||||
|
|
||||||
|
⚠️ CSV odds are a static capture, not verified closing. Positive = LEAD; forward
|
||||||
|
paper-trade with real CLV before staking.
|
||||||
|
|
||||||
|
Usage: python scripts/multi_market_edge.py --folds 5 --lo 1.5 --hi 2.6 --margin 0.03
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, os, sys
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
META = {"match_id","home_team_id","away_team_id","league_id","mst_utc",
|
||||||
|
"score_home","score_away","ht_score_home","ht_score_away"}
|
||||||
|
LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals",
|
||||||
|
"squad_diff","home_squad_quality","away_squad_quality",
|
||||||
|
"referee_home_bias","referee_avg_goals"}
|
||||||
|
|
||||||
|
# market -> (kind, [odds_cols aligned to classes], truth_fn(sh,sa,hh,ha)->class idx or None)
|
||||||
|
def ou(line): return lambda sh,sa,hh,ha: (0 if (sh+sa) > line else 1) # 0=Over,1=Under
|
||||||
|
def htou(line): return lambda sh,sa,hh,ha: (None if np.isnan(hh) else (0 if (hh+ha) > line else 1))
|
||||||
|
def ms_truth(sh,sa,hh,ha): return 0 if sh>sa else (1 if sh==sa else 2)
|
||||||
|
def ht_truth(sh,sa,hh,ha): return None if np.isnan(hh) else (0 if hh>ha else (1 if hh==ha else 2))
|
||||||
|
def btts_truth(sh,sa,hh,ha): return 0 if (sh>0 and sa>0) else 1 # 0=Yes,1=No
|
||||||
|
|
||||||
|
MARKETS = {
|
||||||
|
"MS": ("multi", ["odds_ms_h","odds_ms_d","odds_ms_a"], ["1","X","2"], ms_truth),
|
||||||
|
"HT": ("multi", ["odds_ht_ms_h","odds_ht_ms_d","odds_ht_ms_a"], ["1","X","2"], ht_truth),
|
||||||
|
"OU05": ("binary", ["odds_ou05_o","odds_ou05_u"], ["Üst","Alt"], ou(0.5)),
|
||||||
|
"OU15": ("binary", ["odds_ou15_o","odds_ou15_u"], ["Üst","Alt"], ou(1.5)),
|
||||||
|
"OU25": ("binary", ["odds_ou25_o","odds_ou25_u"], ["Üst","Alt"], ou(2.5)),
|
||||||
|
"OU35": ("binary", ["odds_ou35_o","odds_ou35_u"], ["Üst","Alt"], ou(3.5)),
|
||||||
|
"HT_OU05": ("binary", ["odds_ht_ou05_o","odds_ht_ou05_u"], ["Üst","Alt"], htou(0.5)),
|
||||||
|
"HT_OU15": ("binary", ["odds_ht_ou15_o","odds_ht_ou15_u"], ["Üst","Alt"], htou(1.5)),
|
||||||
|
"BTTS": ("binary", ["odds_btts_y","odds_btts_n"], ["Var","Yok"], btts_truth),
|
||||||
|
}
|
||||||
|
PARAMS_M = {"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05,
|
||||||
|
"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
PARAMS_B = {"objective":"binary:logistic","max_depth":5,"eta":0.05,
|
||||||
|
"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--folds", type=int, default=5)
|
||||||
|
ap.add_argument("--estimators", type=int, default=150)
|
||||||
|
ap.add_argument("--lo", type=float, default=1.5)
|
||||||
|
ap.add_argument("--hi", type=float, default=2.6)
|
||||||
|
ap.add_argument("--margin", type=float, default=0.03)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
df = pd.read_csv(CSV, low_memory=False).sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh = pd.to_numeric(df["score_home"], errors="coerce")
|
||||||
|
sa = pd.to_numeric(df["score_away"], errors="coerce")
|
||||||
|
ok = sh.notna() & sa.notna()
|
||||||
|
df = df[ok].reset_index(drop=True)
|
||||||
|
SH = sh[ok.values].values.astype(float); SA = sa[ok.values].values.astype(float)
|
||||||
|
HH = pd.to_numeric(df["ht_score_home"], errors="coerce").values.astype(float)
|
||||||
|
HA = pd.to_numeric(df["ht_score_away"], errors="coerce").values.astype(float)
|
||||||
|
feats = [c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY]
|
||||||
|
X = df[feats].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
N = len(df)
|
||||||
|
print(f"{N:,} matches, {len(feats)} leak-free feats, {len(MARKETS)} markets, folds={args.folds}")
|
||||||
|
|
||||||
|
# precompute truth + odds per market
|
||||||
|
MK = {}
|
||||||
|
for mname,(kind,ocols,picks,tfn) in MARKETS.items():
|
||||||
|
if not all(c in df.columns for c in ocols):
|
||||||
|
print(f" skip {mname}: missing odds cols"); continue
|
||||||
|
O = df[ocols].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
truth = np.array([tfn(SH[i],SA[i],HH[i],HA[i]) for i in range(N)], dtype=object)
|
||||||
|
MK[mname] = (kind, O, picks, truth)
|
||||||
|
|
||||||
|
start = int(N*0.5); bounds = np.linspace(start, N, args.folds+1, dtype=int)
|
||||||
|
|
||||||
|
# accumulators
|
||||||
|
per_market = {m: {"n":0,"pnl":0.0,"win":0} for m in MK} # (A) best value pick within market
|
||||||
|
sel = {"n":0,"pnl":0.0,"win":0,"fold":{}} # (B) cross-market selector
|
||||||
|
sel_by_mkt = {m: {"n":0,"pnl":0.0,"win":0} for m in MK}
|
||||||
|
|
||||||
|
for fi in range(args.folds):
|
||||||
|
te0,te1 = bounds[fi], bounds[fi+1]
|
||||||
|
if te1-te0 < 50: continue
|
||||||
|
idx = np.arange(te0,te1)
|
||||||
|
# train each market model on [:te0], predict test
|
||||||
|
cand = {} # market -> (P_matrix[n_test, n_picks], O_test, truth_test)
|
||||||
|
for m,(kind,O,picks,truth) in MK.items():
|
||||||
|
ytr_full = truth[:te0]
|
||||||
|
# mask invalid truth (e.g., HT markets with missing HT score)
|
||||||
|
valid_tr = np.array([v is not None for v in ytr_full])
|
||||||
|
if kind=="multi":
|
||||||
|
ytr = ytr_full[valid_tr].astype(int)
|
||||||
|
bst = xgb.train(PARAMS_M, xgb.DMatrix(X[:te0][valid_tr], label=ytr), num_boost_round=args.estimators)
|
||||||
|
P = bst.predict(xgb.DMatrix(X[te0:te1])) # [n,3]
|
||||||
|
else:
|
||||||
|
ytr = ytr_full[valid_tr].astype(int) # 0=positive,1=neg
|
||||||
|
pos = (ytr==0).astype(int)
|
||||||
|
bst = xgb.train(PARAMS_B, xgb.DMatrix(X[:te0][valid_tr], label=pos), num_boost_round=args.estimators)
|
||||||
|
ppos = bst.predict(xgb.DMatrix(X[te0:te1]))
|
||||||
|
P = np.column_stack([ppos, 1.0-ppos]) # [n,2] -> [pos,neg]
|
||||||
|
cand[m] = (P, O[te0:te1], truth[te0:te1])
|
||||||
|
|
||||||
|
# iterate test matches
|
||||||
|
for j in range(te1-te0):
|
||||||
|
best = None # (edge, market, pickidx, odds, won)
|
||||||
|
for m,(P,Ot,Tt) in cand.items():
|
||||||
|
t = Tt[j]
|
||||||
|
if t is None: continue
|
||||||
|
probs = P[j]; odds = Ot[j]
|
||||||
|
for k in range(len(probs)):
|
||||||
|
o = odds[k]
|
||||||
|
if o <= 1.0: continue
|
||||||
|
edge = probs[k] - 1.0/o
|
||||||
|
won = int(t==k)
|
||||||
|
# (A) per-market: track best value pick in this market (any band, edge>margin)
|
||||||
|
if edge > args.margin:
|
||||||
|
d = per_market[m]
|
||||||
|
# only count the market's single best pick per match
|
||||||
|
# collect for selector if in band + margin
|
||||||
|
if edge > args.margin and args.lo <= o < args.hi:
|
||||||
|
if best is None or edge > best[0]:
|
||||||
|
best = (edge, m, k, o, won)
|
||||||
|
# per-market best pick (separate loop for clean per-market ROI in band)
|
||||||
|
bestk=None
|
||||||
|
for k in range(len(probs)):
|
||||||
|
o=odds[k]
|
||||||
|
if o<=1.0: continue
|
||||||
|
e=probs[k]-1.0/o
|
||||||
|
if e>args.margin and args.lo<=o<args.hi and (bestk is None or e>bestk[0]):
|
||||||
|
bestk=(e,k,o,int(t==k))
|
||||||
|
if bestk is not None:
|
||||||
|
e,k,o,won = bestk
|
||||||
|
pnl = (o-1.0) if won else -1.0
|
||||||
|
d=per_market[m]; d["n"]+=1; d["pnl"]+=pnl; d["win"]+=won
|
||||||
|
# selector: single best value bet across all markets for this match
|
||||||
|
if best is not None:
|
||||||
|
edge,m,k,o,won = best
|
||||||
|
pnl = (o-1.0) if won else -1.0
|
||||||
|
sel["n"]+=1; sel["pnl"]+=pnl; sel["win"]+=won
|
||||||
|
sel["fold"][fi] = sel["fold"].get(fi,0.0)+pnl
|
||||||
|
d=sel_by_mkt[m]; d["n"]+=1; d["pnl"]+=pnl; d["win"]+=won
|
||||||
|
print(f" fold {fi}: tested {te1-te0:,}")
|
||||||
|
|
||||||
|
def line(name,d):
|
||||||
|
n=d["n"]; roi=100*d["pnl"]/n if n else float('nan'); hit=100*d["win"]/n if n else float('nan')
|
||||||
|
return f" {name:<10} bets={n:>6} hit={hit:>5.1f}% ROI={roi:>7.2f}% net={d['pnl']:>7.1f}u"
|
||||||
|
|
||||||
|
print("\n"+"="*70); print(f"(A) PER-MARKET value ROI (best value pick in band [{args.lo},{args.hi}], margin {args.margin})"); print("="*70)
|
||||||
|
for m in sorted(per_market, key=lambda x:-(100*per_market[x]['pnl']/per_market[x]['n'] if per_market[x]['n'] else -99)):
|
||||||
|
print(line(m, per_market[m]))
|
||||||
|
|
||||||
|
print("\n"+"="*70); print("(B) CROSS-MARKET SELECTOR (best value bet per match, all markets)"); print("="*70)
|
||||||
|
print(line("SELECTOR", sel))
|
||||||
|
folds_pos = sum(1 for v in sel["fold"].values() if v>0)
|
||||||
|
print(f" folds positive: {folds_pos}/{len(sel['fold'])}")
|
||||||
|
print(" selector picks distributed across markets:")
|
||||||
|
for m in sorted(sel_by_mkt, key=lambda x:-sel_by_mkt[x]['n']):
|
||||||
|
if sel_by_mkt[m]["n"]>0: print(" "+line(m, sel_by_mkt[m]).strip())
|
||||||
|
print("\nREAD: a market/selector is a LEAD only if ROI>0, folds consistent, n large.")
|
||||||
|
print("Forward-validate with CLV before staking. Static CSV odds may overstate edge.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
"""
|
||||||
|
Filter Optimizer
|
||||||
|
================
|
||||||
|
Grid-search over filter thresholds (per market) using the existing
|
||||||
|
diagnostic_backtest CSV. Finds the (confidence, edge, odds, reliability)
|
||||||
|
combination that maximizes ROI while keeping bet volume reasonable.
|
||||||
|
|
||||||
|
No re-prediction needed — pure offline simulation on the bets already
|
||||||
|
captured. Output: per-market optimal thresholds + projected ROI lift +
|
||||||
|
JSON patch ready to drop into config/market_thresholds.json.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/optimize_filters.py
|
||||||
|
python scripts/optimize_filters.py --csv reports/diagnostic_backtest_X.csv
|
||||||
|
python scripts/optimize_filters.py --min-bets 20 --apply
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import glob
|
||||||
|
import itertools
|
||||||
|
from typing import List, Dict, Tuple, Optional
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
|
||||||
|
sys.path.insert(0, AI_ENGINE_DIR)
|
||||||
|
|
||||||
|
REPORTS_DIR = os.path.join(AI_ENGINE_DIR, "reports")
|
||||||
|
CONFIG_PATH = os.path.join(AI_ENGINE_DIR, "config", "market_thresholds.json")
|
||||||
|
|
||||||
|
|
||||||
|
def latest_csv() -> Optional[str]:
|
||||||
|
files = sorted(glob.glob(os.path.join(REPORTS_DIR, "diagnostic_backtest_*.csv")),
|
||||||
|
key=os.path.getmtime, reverse=True)
|
||||||
|
return files[0] if files else None
|
||||||
|
|
||||||
|
|
||||||
|
def load_backtest(path: str) -> pd.DataFrame:
|
||||||
|
df = pd.read_csv(path)
|
||||||
|
# Keep only playable + settled bets — these are what the SYSTEM
|
||||||
|
# actually placed and got an outcome on.
|
||||||
|
pdf = df[(df["playable"] == True) & (df["won"].notna())].copy()
|
||||||
|
pdf["won"] = pdf["won"].astype(bool)
|
||||||
|
pdf["calibrated_confidence"] = pdf["calibrated_confidence"].fillna(0)
|
||||||
|
pdf["ev_edge"] = pdf["ev_edge"].fillna(0)
|
||||||
|
pdf["odds"] = pdf["odds"].fillna(0)
|
||||||
|
pdf["odds_reliability"] = pdf["odds_reliability"].fillna(0.35)
|
||||||
|
return pdf
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(pdf: pd.DataFrame, mask) -> Dict:
|
||||||
|
kept = pdf[mask]
|
||||||
|
if len(kept) == 0:
|
||||||
|
return {"n": 0, "hit_pct": 0, "profit": 0, "staked": 0, "roi_pct": 0}
|
||||||
|
wins = kept["won"].sum()
|
||||||
|
profit = kept["unit_profit"].sum()
|
||||||
|
staked = kept["stake_units"].sum()
|
||||||
|
return {
|
||||||
|
"n": int(len(kept)),
|
||||||
|
"hit_pct": round(100.0 * wins / len(kept), 2),
|
||||||
|
"profit": round(profit, 3),
|
||||||
|
"staked": round(staked, 3),
|
||||||
|
"roi_pct": round(100.0 * profit / staked, 2) if staked else 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def grid_search_market(
|
||||||
|
market_df: pd.DataFrame,
|
||||||
|
market: str,
|
||||||
|
min_bets: int = 15,
|
||||||
|
) -> List[Dict]:
|
||||||
|
"""Try a wide grid of (min_conf, min_edge, max_edge, min_odds, max_odds,
|
||||||
|
min_reliability) combinations. Return all candidates with n >= min_bets,
|
||||||
|
sorted by ROI descending."""
|
||||||
|
|
||||||
|
conf_options = [0, 45, 50, 55, 60, 65, 70]
|
||||||
|
min_edge_options = [-1.0, -0.05, 0.0, 0.03, 0.05, 0.08]
|
||||||
|
max_edge_options = [10.0, 0.30, 0.20, 0.15, 0.10]
|
||||||
|
min_odds_options = [1.20, 1.30, 1.40, 1.50, 1.60, 1.80]
|
||||||
|
max_odds_options = [10.0, 3.0, 2.5, 2.2, 2.0]
|
||||||
|
rel_options = [0.0, 0.30, 0.45, 0.55]
|
||||||
|
consensus_options = ["any", "agree_or_null"]
|
||||||
|
|
||||||
|
candidates: List[Dict] = []
|
||||||
|
for mc, mine, maxe, mino, maxo, mrel, cons in itertools.product(
|
||||||
|
conf_options, min_edge_options, max_edge_options,
|
||||||
|
min_odds_options, max_odds_options, rel_options, consensus_options,
|
||||||
|
):
|
||||||
|
if mine >= maxe or mino >= maxo:
|
||||||
|
continue
|
||||||
|
mask = (
|
||||||
|
(market_df["calibrated_confidence"] >= mc)
|
||||||
|
& (market_df["ev_edge"] >= mine)
|
||||||
|
& (market_df["ev_edge"] <= maxe)
|
||||||
|
& (market_df["odds"] >= mino)
|
||||||
|
& (market_df["odds"] <= maxo)
|
||||||
|
& (market_df["odds_reliability"] >= mrel)
|
||||||
|
)
|
||||||
|
if cons == "agree_or_null":
|
||||||
|
mask &= market_df["v27_consensus"] != "DISAGREE"
|
||||||
|
result = evaluate(market_df, mask)
|
||||||
|
if result["n"] >= min_bets:
|
||||||
|
candidates.append({
|
||||||
|
"market": market,
|
||||||
|
"min_conf": mc,
|
||||||
|
"min_edge": mine,
|
||||||
|
"max_edge": maxe,
|
||||||
|
"min_odds": mino,
|
||||||
|
"max_odds": maxo,
|
||||||
|
"min_reliability": mrel,
|
||||||
|
"consensus": cons,
|
||||||
|
**result,
|
||||||
|
})
|
||||||
|
candidates.sort(key=lambda r: (r["roi_pct"], r["n"]), reverse=True)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def baseline(pdf: pd.DataFrame, market: str) -> Dict:
|
||||||
|
m = pdf[pdf["market"] == market]
|
||||||
|
return evaluate(m, pd.Series([True] * len(m), index=m.index))
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("--csv", default=None, help="Override CSV path")
|
||||||
|
parser.add_argument("--min-bets", type=int, default=15,
|
||||||
|
help="Min bet count to consider a config valid")
|
||||||
|
parser.add_argument("--top-k", type=int, default=3,
|
||||||
|
help="Show top K configs per market")
|
||||||
|
parser.add_argument("--apply", action="store_true",
|
||||||
|
help="Patch config/market_thresholds.json with winners")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
csv_path = args.csv or latest_csv()
|
||||||
|
if not csv_path or not os.path.exists(csv_path):
|
||||||
|
print("No backtest CSV found.")
|
||||||
|
return
|
||||||
|
print(f"Loading: {csv_path}")
|
||||||
|
pdf = load_backtest(csv_path)
|
||||||
|
print(f"Playable + settled bets: {len(pdf)}")
|
||||||
|
|
||||||
|
markets = sorted(pdf["market"].dropna().unique())
|
||||||
|
print(f"Markets: {markets}\n")
|
||||||
|
|
||||||
|
all_winners: Dict[str, Dict] = {}
|
||||||
|
|
||||||
|
for market in markets:
|
||||||
|
market_df = pdf[pdf["market"] == market]
|
||||||
|
n_total = len(market_df)
|
||||||
|
base = baseline(pdf, market)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 78}")
|
||||||
|
print(f"MARKET: {market} (n={n_total} baseline_roi={base['roi_pct']}%)")
|
||||||
|
print(f"{'=' * 78}")
|
||||||
|
|
||||||
|
if n_total < args.min_bets * 2:
|
||||||
|
print(f" Sample too small to grid-search reliably (n={n_total}). Skip.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
candidates = grid_search_market(market_df, market, args.min_bets)
|
||||||
|
if not candidates:
|
||||||
|
print(f" No config kept >= {args.min_bets} bets. Skip.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Pareto-ish: show top-K by ROI but also one that keeps higher bet count
|
||||||
|
winners = candidates[:args.top_k]
|
||||||
|
keep_high_volume = None
|
||||||
|
for c in candidates:
|
||||||
|
if c["n"] >= max(40, n_total // 3) and c["roi_pct"] > base["roi_pct"]:
|
||||||
|
keep_high_volume = c
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f" {'rank':<5}{'n':>5}{'hit%':>7}{'roi%':>8} "
|
||||||
|
f"{'min_conf':>9}{'min_edge':>10}{'max_edge':>10}"
|
||||||
|
f"{'min_odds':>10}{'max_odds':>10}{'min_rel':>9}{'cons':>15}")
|
||||||
|
for i, w in enumerate(winners, 1):
|
||||||
|
print(f" {i:<5}{w['n']:>5}{w['hit_pct']:>7}{w['roi_pct']:>+8}"
|
||||||
|
f" {w['min_conf']:>9}{w['min_edge']:>+10.3f}{w['max_edge']:>+10.3f}"
|
||||||
|
f"{w['min_odds']:>10.2f}{w['max_odds']:>10.2f}"
|
||||||
|
f"{w['min_reliability']:>9.2f}{w['consensus']:>15}")
|
||||||
|
if keep_high_volume and keep_high_volume not in winners:
|
||||||
|
print(f" high {keep_high_volume['n']:>5}{keep_high_volume['hit_pct']:>7}"
|
||||||
|
f"{keep_high_volume['roi_pct']:>+8}"
|
||||||
|
f" {keep_high_volume['min_conf']:>9}"
|
||||||
|
f"{keep_high_volume['min_edge']:>+10.3f}"
|
||||||
|
f"{keep_high_volume['max_edge']:>+10.3f}"
|
||||||
|
f"{keep_high_volume['min_odds']:>10.2f}"
|
||||||
|
f"{keep_high_volume['max_odds']:>10.2f}"
|
||||||
|
f"{keep_high_volume['min_reliability']:>9.2f}"
|
||||||
|
f"{keep_high_volume['consensus']:>15}")
|
||||||
|
|
||||||
|
# Pick a "good" recommendation: best ROI with n >= min_bets
|
||||||
|
# If best ROI is still negative, flag the market as unprofitable.
|
||||||
|
best = winners[0]
|
||||||
|
all_winners[market] = best
|
||||||
|
if best["roi_pct"] <= 0:
|
||||||
|
print(f" ⚠️ Best config still loses money (ROI={best['roi_pct']}%) "
|
||||||
|
f"— consider muting this market entirely.")
|
||||||
|
else:
|
||||||
|
print(f" ✅ Best config: ROI={best['roi_pct']}% on {best['n']} bets "
|
||||||
|
f"(vs baseline {base['roi_pct']}% on {n_total}).")
|
||||||
|
|
||||||
|
# ─── Aggregate impact ────────────────────────────────────────────────
|
||||||
|
print(f"\n{'=' * 78}")
|
||||||
|
print("AGGREGATE IMPACT (if we apply each market's best config)")
|
||||||
|
print(f"{'=' * 78}")
|
||||||
|
total_old_bets = total_old_profit = total_old_staked = 0
|
||||||
|
total_new_bets = total_new_profit = total_new_staked = 0
|
||||||
|
for market, win in all_winners.items():
|
||||||
|
base = baseline(pdf, market)
|
||||||
|
total_old_bets += base["n"]
|
||||||
|
total_old_profit += base["profit"]
|
||||||
|
total_old_staked += base["staked"]
|
||||||
|
total_new_bets += win["n"]
|
||||||
|
total_new_profit += win["profit"]
|
||||||
|
total_new_staked += win["staked"]
|
||||||
|
base_roi = 100.0 * total_old_profit / total_old_staked if total_old_staked else 0
|
||||||
|
new_roi = 100.0 * total_new_profit / total_new_staked if total_new_staked else 0
|
||||||
|
print(f" Baseline: {total_old_bets:>4} bets, "
|
||||||
|
f"profit={total_old_profit:+.2f}u, ROI={base_roi:+.2f}%")
|
||||||
|
print(f" Optimized: {total_new_bets:>4} bets, "
|
||||||
|
f"profit={total_new_profit:+.2f}u, ROI={new_roi:+.2f}%")
|
||||||
|
print(f" Δ: {total_new_bets - total_old_bets:+d} bets, "
|
||||||
|
f"{total_new_profit - total_old_profit:+.2f}u, "
|
||||||
|
f"{new_roi - base_roi:+.2f}pp")
|
||||||
|
|
||||||
|
# ─── Write JSON patch ────────────────────────────────────────────────
|
||||||
|
patch_path = os.path.join(REPORTS_DIR, "filter_optimization_patch.json")
|
||||||
|
patch = {market: {
|
||||||
|
"min_calibrated_confidence": win["min_conf"],
|
||||||
|
"min_ev_edge": win["min_edge"],
|
||||||
|
"max_ev_edge": win["max_edge"],
|
||||||
|
"min_odds": win["min_odds"],
|
||||||
|
"max_odds": win["max_odds"],
|
||||||
|
"min_odds_reliability": win["min_reliability"],
|
||||||
|
"require_v27_agree": win["consensus"] == "agree_or_null",
|
||||||
|
"expected_n_bets": win["n"],
|
||||||
|
"expected_hit_pct": win["hit_pct"],
|
||||||
|
"expected_roi_pct": win["roi_pct"],
|
||||||
|
} for market, win in all_winners.items()}
|
||||||
|
with open(patch_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(patch, f, indent=2)
|
||||||
|
print(f"\nPatch saved: {patch_path}")
|
||||||
|
|
||||||
|
if args.apply:
|
||||||
|
print("\n--apply flag set. Patching not implemented yet — "
|
||||||
|
"review the patch JSON and update config/market_thresholds.json manually.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
"""Smoke test for the score-coherence filter using the LAFC vs Sounders
|
||||||
|
1-0 scenario from production. Verifies that markets that contradict the
|
||||||
|
predicted score are correctly excluded from the coherent set, and that
|
||||||
|
the markets the model got right are all included.
|
||||||
|
"""
|
||||||
|
import os, sys
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from services.betting_brain import BettingBrain
|
||||||
|
|
||||||
|
brain = BettingBrain()
|
||||||
|
pkg = {
|
||||||
|
"score_prediction": {"ft": "1-0", "ht": "0-0"},
|
||||||
|
}
|
||||||
|
coh = brain._score_consistent_markets(pkg)
|
||||||
|
print(f"Predicted: 1-0 (HT 0-0)")
|
||||||
|
print(f"Coherent set size: {len(coh)}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Each pick the system actually offered for the LAFC match, with whether
|
||||||
|
# it was the *actual* winning pick.
|
||||||
|
test_picks = [
|
||||||
|
("MS", "1", True, "correct"),
|
||||||
|
("MS", "2", False, "wrong"),
|
||||||
|
("MS", "X", False, "wrong"),
|
||||||
|
("DC", "1X", True, "correct"),
|
||||||
|
("DC", "12", True, "correct"),
|
||||||
|
("DC", "X2", False, "wrong"),
|
||||||
|
("OU25", "Üst", False, "WRONG — system featured this"),
|
||||||
|
("OU25", "Alt", True, "correct"),
|
||||||
|
("OU35", "Alt", True, "correct"),
|
||||||
|
("OU35", "Üst", False, "wrong"),
|
||||||
|
("BTTS", "Var", False, "wrong"),
|
||||||
|
("BTTS", "Yok", True, "correct"),
|
||||||
|
("HT", "X", True, "correct"),
|
||||||
|
("HT", "1", False, "wrong"),
|
||||||
|
("HTFT", "X/1", True, "correct"),
|
||||||
|
("HTFT", "1/1", False, "wrong (HT was 0-0)"),
|
||||||
|
("HT_OU05", "Üst", False, "wrong"),
|
||||||
|
("HT_OU05", "Alt", True, "correct"),
|
||||||
|
("OE", "Çift", False, "wrong (1 is odd)"),
|
||||||
|
("OE", "Tek", True, "correct"),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"{'market':<10}{'pick':<10}{'real-win?':<12}{'in-coherent?':<14}{'match?'}")
|
||||||
|
print("-" * 60)
|
||||||
|
ok = 0
|
||||||
|
for market, pick, would_win, note in test_picks:
|
||||||
|
in_coh = (market, pick) in coh
|
||||||
|
match = "✓" if in_coh == would_win else "✗ MISMATCH"
|
||||||
|
if in_coh == would_win: ok += 1
|
||||||
|
print(f"{market:<10}{pick:<10}{str(would_win):<12}{str(in_coh):<14}{match} {note}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print(f"Result: {ok}/{len(test_picks)} picks correctly classified")
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""
|
||||||
|
Train Favorite-Policy Model (v1) — leak-free MS model for the validated strategy.
|
||||||
|
================================================================================
|
||||||
|
Trains a LEAK-FREE 1X2 model (drops the result-encoding columns) and saves it
|
||||||
|
plus the feature list and policy metadata. This is the brain of the new system;
|
||||||
|
the favourite-band value policy (odds ~1.5-2.2, model_prob>implied, flat stake)
|
||||||
|
is applied on top of its probabilities at serving time.
|
||||||
|
|
||||||
|
Honest holdout: trains on the first --holdout-frac of history, evaluates the
|
||||||
|
EXACT policy on the most recent slice (never seen in training), then retrains
|
||||||
|
on ALL history for the saved production artifact.
|
||||||
|
|
||||||
|
Saves to models/favorite_v1/: model.json, feature_cols.json, metadata.json
|
||||||
|
|
||||||
|
Usage: python scripts/train_favorite_model.py
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse, json, os, sys, datetime
|
||||||
|
import numpy as np, pandas as pd, xgboost as xgb
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try: sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
OUT = os.path.join(AI_DIR, "models", "favorite_v1")
|
||||||
|
|
||||||
|
META = {"match_id","home_team_id","away_team_id","league_id","mst_utc",
|
||||||
|
"score_home","score_away","ht_score_home","ht_score_away"}
|
||||||
|
# Result-encoding leakage — never feed these to the model (train OR serve).
|
||||||
|
LEAKY = {"home_goals_form","away_goals_form","total_goals","ht_total_goals",
|
||||||
|
"squad_diff","home_squad_quality","away_squad_quality",
|
||||||
|
"referee_home_bias","referee_avg_goals"}
|
||||||
|
|
||||||
|
PARAMS = {"objective":"multi:softprob","num_class":3,"max_depth":5,"eta":0.05,
|
||||||
|
"subsample":0.8,"colsample_bytree":0.8,"tree_method":"hist","verbosity":0}
|
||||||
|
|
||||||
|
|
||||||
|
def policy_eval(P, y, O, lo, hi, margin):
|
||||||
|
implied = np.where(O > 1.0, 1.0/O, np.nan)
|
||||||
|
edge = np.where(np.isnan(implied), -9.0, P - implied)
|
||||||
|
pick = edge.argmax(1); pe = edge[np.arange(len(y)), pick]; po = O[np.arange(len(y)), pick]
|
||||||
|
bet = (pe > margin) & (po >= lo) & (po < hi)
|
||||||
|
win = (pick == y) & bet
|
||||||
|
pnl = np.where(win, po-1.0, -1.0)[bet]
|
||||||
|
n = int(bet.sum())
|
||||||
|
return {"bets": n, "hit_pct": round(100*win.sum()/max(n,1),1),
|
||||||
|
"roi_pct": round(100*pnl.sum()/max(n,1),2), "net_u": round(float(pnl.sum()),1)}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--lo", type=float, default=1.5)
|
||||||
|
ap.add_argument("--hi", type=float, default=2.2)
|
||||||
|
ap.add_argument("--margin", type=float, default=0.0)
|
||||||
|
ap.add_argument("--holdout-frac", type=float, default=0.15)
|
||||||
|
ap.add_argument("--estimators", type=int, default=300)
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
print(f"Loading {CSV} ...")
|
||||||
|
df = pd.read_csv(CSV, low_memory=False).sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
sh = pd.to_numeric(df["score_home"], errors="coerce")
|
||||||
|
sa = pd.to_numeric(df["score_away"], errors="coerce")
|
||||||
|
ok = sh.notna() & sa.notna()
|
||||||
|
df, sh, sa = df[ok].reset_index(drop=True), sh[ok.values].values, sa[ok.values].values
|
||||||
|
y = np.where(sh > sa, 0, np.where(sh == sa, 1, 2))
|
||||||
|
O = df[["odds_ms_h","odds_ms_d","odds_ms_a"]].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
feats = [c for c in df.columns if c not in META and not c.startswith("label_") and c not in LEAKY]
|
||||||
|
X = df[feats].apply(pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
print(f" {len(df):,} rows, {len(feats)} leak-free features")
|
||||||
|
|
||||||
|
# ── Honest holdout (last slice, never trained on) ──
|
||||||
|
cut = int(len(df) * (1 - args.holdout_frac))
|
||||||
|
bst = xgb.train(PARAMS, xgb.DMatrix(X[:cut], label=y[:cut]), num_boost_round=args.estimators)
|
||||||
|
Ph = bst.predict(xgb.DMatrix(X[cut:]))
|
||||||
|
acc = float((Ph.argmax(1) == y[cut:]).mean())
|
||||||
|
hold = policy_eval(Ph, y[cut:], O[cut:], args.lo, args.hi, args.margin)
|
||||||
|
print(f"\nHOLDOUT (last {args.holdout_frac:.0%}, {len(df)-cut:,} matches, never seen):")
|
||||||
|
print(f" MS accuracy: {acc*100:.1f}%")
|
||||||
|
print(f" POLICY band[{args.lo},{args.hi}] margin {args.margin}: {hold}")
|
||||||
|
|
||||||
|
# ── Production model: retrain on ALL history ──
|
||||||
|
print("\nTraining production model on ALL history ...")
|
||||||
|
final = xgb.train(PARAMS, xgb.DMatrix(X, label=y), num_boost_round=args.estimators)
|
||||||
|
os.makedirs(OUT, exist_ok=True)
|
||||||
|
final.save_model(os.path.join(OUT, "model.json"))
|
||||||
|
with open(os.path.join(OUT, "feature_cols.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(feats, f, ensure_ascii=False, indent=2)
|
||||||
|
meta = {
|
||||||
|
"version": "favorite_v1",
|
||||||
|
"trained_at": datetime.datetime.now().isoformat(timespec="seconds"),
|
||||||
|
"market": "MS",
|
||||||
|
"classes": {"0": "home(1)", "1": "draw(X)", "2": "away(2)"},
|
||||||
|
"policy": {"odds_lo": args.lo, "odds_hi": args.hi, "margin": args.margin,
|
||||||
|
"stake": "flat 1u", "rule": "bet model's max value edge if picked odds in band",
|
||||||
|
"never": ["longshots odds>=hi", "parlays/combos"]},
|
||||||
|
"n_train": len(df), "n_features": len(feats),
|
||||||
|
"leaky_excluded": sorted(LEAKY),
|
||||||
|
"holdout_eval": {"accuracy_pct": round(acc*100,1), **hold},
|
||||||
|
"caveat": "CSV odds are a static capture, not verified closing. Forward paper-trade with real CLV before staking.",
|
||||||
|
}
|
||||||
|
with open(os.path.join(OUT, "metadata.json"), "w", encoding="utf-8") as f:
|
||||||
|
json.dump(meta, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"\n✅ Saved production model to {OUT}/")
|
||||||
|
print(f" model.json, feature_cols.json ({len(feats)} feats), metadata.json")
|
||||||
|
print("\nNEXT: serving wrapper that loads this + applies the policy to upcoming")
|
||||||
|
print("matches, logs paper-trade picks, and we measure real forward CLV/ROI.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
"""
|
||||||
|
Walk-Forward Odds-Blind Experiment — THE pivotal test.
|
||||||
|
======================================================
|
||||||
|
Question this answers: can a model BEAT THE MARKET out-of-sample, betting only
|
||||||
|
on information the price doesn't already contain?
|
||||||
|
|
||||||
|
Method (no leakage, time-ordered):
|
||||||
|
* data sorted by kickoff (mst_utc); train on the past, test on the future,
|
||||||
|
rolled over several folds.
|
||||||
|
* TWO models on the MS (1X2) market:
|
||||||
|
ALL = every feature INCLUDING the bookmaker odds (what the live
|
||||||
|
engine does -> it mostly re-learns the price).
|
||||||
|
BLIND = identical but odds/implied/_present columns REMOVED, so the
|
||||||
|
model must disagree with the market using fundamentals only.
|
||||||
|
* For each, an honest value-bet simulation on the test fold using the REAL
|
||||||
|
odds payouts (margin included): bet the outcome with the biggest
|
||||||
|
model_prob - implied_prob edge above a margin; ROI = realized P/L per 1u.
|
||||||
|
|
||||||
|
Read: if BLIND's value ROI is consistently > 0 across folds, there is a real,
|
||||||
|
exploitable lead. If both are <= 0 (expected), these markets aren't beatable
|
||||||
|
with this data and the honest move is to stop staking.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/walkforward_oddsblind.py
|
||||||
|
python scripts/walkforward_oddsblind.py --folds 6 --estimators 300
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
if sys.stdout and hasattr(sys.stdout, "reconfigure"):
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
AI_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
CSV = os.path.join(AI_DIR, "data", "training_data_v27.csv")
|
||||||
|
|
||||||
|
import xgboost as xgb # noqa: E402
|
||||||
|
|
||||||
|
META = {"match_id", "home_team_id", "away_team_id", "league_id", "mst_utc",
|
||||||
|
"score_home", "score_away", "ht_score_home", "ht_score_away"}
|
||||||
|
|
||||||
|
# Confirmed target leakage: *_goals_form integer-valued and ~0.63 correlated
|
||||||
|
# with THIS match's goals; their diff equals the actual goal diff 73% of the
|
||||||
|
# time. Excluded so the experiment measures genuine pre-match predictive power.
|
||||||
|
LEAKY = {
|
||||||
|
# CONFIRMED (encode the actual match result):
|
||||||
|
"home_goals_form", "away_goals_form", # ~0.63 corr w/ this match's goals
|
||||||
|
"total_goals", # this match's full-time total
|
||||||
|
"ht_total_goals", # this match's half-time total
|
||||||
|
# STRONG SUSPECTS (dominate importance + high outcome corr; audit extractor):
|
||||||
|
"squad_diff", "home_squad_quality", "away_squad_quality",
|
||||||
|
"referee_home_bias", "referee_avg_goals",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_odds_col(c: str) -> bool:
|
||||||
|
cl = c.lower()
|
||||||
|
return ("odds" in cl) or ("implied" in cl)
|
||||||
|
|
||||||
|
|
||||||
|
def logloss(y: np.ndarray, p: np.ndarray) -> float:
|
||||||
|
p = np.clip(p, 1e-9, 1 - 1e-9)
|
||||||
|
return float(-np.mean(np.log(p[np.arange(len(y)), y])))
|
||||||
|
|
||||||
|
|
||||||
|
def value_sim(proba: np.ndarray, y: np.ndarray, odds: np.ndarray,
|
||||||
|
margin: float) -> dict:
|
||||||
|
"""Bet the class with the biggest (model_prob - 1/odds) edge above margin."""
|
||||||
|
implied = np.where(odds > 1.0, 1.0 / odds, np.nan)
|
||||||
|
edge = proba - implied
|
||||||
|
# ignore classes without valid odds
|
||||||
|
edge = np.where(np.isnan(implied), -9.0, edge)
|
||||||
|
pick = np.argmax(edge, axis=1)
|
||||||
|
best_edge = edge[np.arange(len(y)), pick]
|
||||||
|
bet = best_edge > margin
|
||||||
|
n = int(bet.sum())
|
||||||
|
if n == 0:
|
||||||
|
return {"n": 0, "roi": None, "hit": None}
|
||||||
|
win = (pick == y) & bet
|
||||||
|
pick_odds = odds[np.arange(len(y)), pick]
|
||||||
|
pnl = np.where(win, pick_odds - 1.0, -1.0)
|
||||||
|
pnl = pnl[bet]
|
||||||
|
return {"n": n, "roi": round(100.0 * pnl.sum() / n, 2),
|
||||||
|
"hit": round(100.0 * win[bet].sum() / n, 1)}
|
||||||
|
|
||||||
|
|
||||||
|
def train_eval(Xtr, ytr, Xte, yte, odds_te, est, margins):
|
||||||
|
dtr = xgb.DMatrix(Xtr, label=ytr)
|
||||||
|
dte = xgb.DMatrix(Xte)
|
||||||
|
params = {"objective": "multi:softprob", "num_class": 3, "max_depth": 5,
|
||||||
|
"eta": 0.05, "subsample": 0.8, "colsample_bytree": 0.8,
|
||||||
|
"tree_method": "hist", "verbosity": 0}
|
||||||
|
booster = xgb.train(params, dtr, num_boost_round=est)
|
||||||
|
proba = booster.predict(dte)
|
||||||
|
out = {"logloss": round(logloss(yte, proba), 4),
|
||||||
|
"acc": round(100.0 * (proba.argmax(1) == yte).mean(), 1)}
|
||||||
|
for mg in margins:
|
||||||
|
out[f"val@{mg}"] = value_sim(proba, yte, odds_te, mg)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
ap = argparse.ArgumentParser(description=__doc__)
|
||||||
|
ap.add_argument("--folds", type=int, default=5)
|
||||||
|
ap.add_argument("--estimators", type=int, default=250)
|
||||||
|
ap.add_argument("--test-frac", type=float, default=0.5,
|
||||||
|
help="Fraction at the end used as rolling OOS (default 0.5)")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
print(f"Loading {CSV} ...")
|
||||||
|
df = pd.read_csv(CSV, low_memory=False)
|
||||||
|
df = df.sort_values("mst_utc").reset_index(drop=True)
|
||||||
|
print(f" {len(df)} rows, {df.shape[1]} cols")
|
||||||
|
|
||||||
|
# Derive true MS outcome from scores: 0=home,1=draw,2=away (robust, no label trust)
|
||||||
|
sh = pd.to_numeric(df["score_home"], errors="coerce")
|
||||||
|
sa = pd.to_numeric(df["score_away"], errors="coerce")
|
||||||
|
y = np.where(sh > sa, 0, np.where(sh == sa, 1, 2))
|
||||||
|
valid = sh.notna() & sa.notna()
|
||||||
|
df, y = df[valid].reset_index(drop=True), y[valid.values]
|
||||||
|
|
||||||
|
odds = df[["odds_ms_h", "odds_ms_d", "odds_ms_a"]].apply(
|
||||||
|
pd.to_numeric, errors="coerce").fillna(0.0).values
|
||||||
|
|
||||||
|
feat_all = [c for c in df.columns if c not in META and not c.startswith("label_")
|
||||||
|
and c not in LEAKY]
|
||||||
|
feat_blind = [c for c in feat_all if not is_odds_col(c)]
|
||||||
|
print(f" excluded leaky cols: {sorted(LEAKY)}")
|
||||||
|
Xall = df[feat_all].apply(pd.to_numeric, errors="coerce").fillna(0.0)
|
||||||
|
Xblind = df[feat_blind].apply(pd.to_numeric, errors="coerce").fillna(0.0)
|
||||||
|
print(f" features: ALL={len(feat_all)} BLIND={len(feat_blind)} "
|
||||||
|
f"(dropped {len(feat_all)-len(feat_blind)} odds cols)")
|
||||||
|
print(f" base rates: home={100*(y==0).mean():.1f}% draw={100*(y==1).mean():.1f}% "
|
||||||
|
f"away={100*(y==2).mean():.1f}%")
|
||||||
|
|
||||||
|
n = len(df)
|
||||||
|
start = int(n * (1 - args.test_frac))
|
||||||
|
bounds = np.linspace(start, n, args.folds + 1, dtype=int)
|
||||||
|
margins = [0.0, 0.05, 0.10]
|
||||||
|
|
||||||
|
agg = {"ALL": {f"val@{m}": [] for m in margins}, "BLIND": {f"val@{m}": [] for m in margins}}
|
||||||
|
agg["ALL"]["logloss"] = []; agg["BLIND"]["logloss"] = []
|
||||||
|
|
||||||
|
print(f"\nWalk-forward: {args.folds} folds, train=expanding, est={args.estimators}\n")
|
||||||
|
hdr = f"{'fold':<5}{'model':<7}{'logloss':>9}{'acc%':>7}" + "".join(
|
||||||
|
f"{('val@'+str(m)):>22}" for m in margins)
|
||||||
|
print(hdr); print("-" * len(hdr))
|
||||||
|
for i in range(args.folds):
|
||||||
|
te0, te1 = bounds[i], bounds[i + 1]
|
||||||
|
if te1 - te0 < 50:
|
||||||
|
continue
|
||||||
|
tr = slice(0, te0)
|
||||||
|
te = slice(te0, te1)
|
||||||
|
for name, X in (("ALL", Xall), ("BLIND", Xblind)):
|
||||||
|
r = train_eval(X.iloc[tr].values, y[tr], X.iloc[te].values, y[te],
|
||||||
|
odds[te], args.estimators, margins)
|
||||||
|
agg[name]["logloss"].append(r["logloss"])
|
||||||
|
cells = ""
|
||||||
|
for m in margins:
|
||||||
|
v = r[f"val@{m}"]
|
||||||
|
agg[name][f"val@{m}"].append(v)
|
||||||
|
cells += f"{('n=' + str(v['n']) + ' roi=' + str(v['roi'])):>22}"
|
||||||
|
print(f"{i:<5}{name:<7}{r['logloss']:>9}{r['acc']:>7}{cells}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("=" * 70)
|
||||||
|
print("AGGREGATE (sum bets, weighted ROI across folds)")
|
||||||
|
print("=" * 70)
|
||||||
|
for name in ("ALL", "BLIND"):
|
||||||
|
ll = np.mean(agg[name]["logloss"]) if agg[name]["logloss"] else float("nan")
|
||||||
|
print(f"\n{name} mean logloss={ll:.4f}")
|
||||||
|
for m in margins:
|
||||||
|
vs = agg[name][f"val@{m}"]
|
||||||
|
tot_n = sum(v["n"] for v in vs)
|
||||||
|
tot_pnl = sum((v["roi"] / 100.0 * v["n"]) for v in vs if v["roi"] is not None)
|
||||||
|
roi = round(100.0 * tot_pnl / tot_n, 2) if tot_n else None
|
||||||
|
print(f" margin {m}: total_bets={tot_n:>6} ROI(flat1u)={roi}%")
|
||||||
|
print("\nREAD: BLIND ROI>0 across margins/folds = real edge. Both <=0 = no")
|
||||||
|
print("exploitable edge in MS with this data (stop staking; the -EV is the vig).")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -12,16 +12,52 @@ 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
|
||||||
|
|
||||||
|
# ── V31f: NATIONAL-TEAM REGIME ───────────────────────────────────────
|
||||||
|
# National matches behave nothing like clubs (2300-match backtest):
|
||||||
|
# * Only MS carries edge — OU/BTTS/HT/DC/OE all -12%..-21% → hard mute.
|
||||||
|
# * MS edge lives in the 4.0–7.0 odds band for HAZIRLIK/ELEME fixtures
|
||||||
|
# (+17% ROI, stable across older/newer halves: +22%/+24%).
|
||||||
|
# * Favorites (odds<3) lose (-10..-18%); TURNUVA inverts the pattern
|
||||||
|
# (4-7 band is -9% there) → tournaments get NO bet (analysis only).
|
||||||
|
# Calibration is fine; this is a *bet-selection* gate, applied only when
|
||||||
|
# match_info.is_national is True. Clubs are completely unaffected.
|
||||||
|
# See mds/national-team-strategy.md.
|
||||||
|
NATIONAL_BET_MARKET = "MS"
|
||||||
|
NATIONAL_MIN_ODDS = 4.0
|
||||||
|
NATIONAL_MAX_ODDS = 7.0
|
||||||
|
NATIONAL_ALLOWED_COMPETITIONS = {"HAZIRLIK", "ELEME"}
|
||||||
|
NATIONAL_BASE_SCORE = 66.0 # clears MIN_BET_SCORE(62) when gate passes
|
||||||
|
NATIONAL_STAKE_UNITS = 0.5 # flat, high-variance band (~24% hit)
|
||||||
|
|
||||||
MARKET_MIN_CONFIDENCE = {
|
MARKET_MIN_CONFIDENCE = {
|
||||||
"MS": 45.0,
|
"MS": 45.0,
|
||||||
"DC": 55.0,
|
"DC": 55.0,
|
||||||
@@ -39,6 +75,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"}
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# 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 = {
|
MARKET_PRIORS = {
|
||||||
"DC": 4.0,
|
"DC": 4.0,
|
||||||
"OU15": 3.0,
|
"OU15": 3.0,
|
||||||
@@ -52,7 +263,14 @@ class BettingBrain:
|
|||||||
"OE": -12.0,
|
"OE": -12.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
def judge(self, package: Dict[str, Any]) -> Dict[str, Any]:
|
def judge(
|
||||||
|
self,
|
||||||
|
package: Dict[str, Any],
|
||||||
|
ms_real_odds: Optional[Dict[str, float]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
# V35c: real bookmaker MS odds (from odds_data) for reference rows —
|
||||||
|
# the brain must never display synthetic 1/p "fair odds" as offered.
|
||||||
|
self._ms_real_odds = ms_real_odds if isinstance(ms_real_odds, dict) else {}
|
||||||
v27_engine = package.get("v27_engine")
|
v27_engine = package.get("v27_engine")
|
||||||
if not isinstance(v27_engine, dict):
|
if not isinstance(v27_engine, dict):
|
||||||
return package
|
return package
|
||||||
@@ -86,6 +304,36 @@ class BettingBrain:
|
|||||||
watchlist.sort(key=self._candidate_sort_key, reverse=True)
|
watchlist.sort(key=self._candidate_sort_key, reverse=True)
|
||||||
no_value.sort(key=self._candidate_sort_key, reverse=True)
|
no_value.sort(key=self._candidate_sort_key, reverse=True)
|
||||||
|
|
||||||
|
# ── SCORE COHERENCE FILTER ──────────────────────────────────────
|
||||||
|
# If the model also produced a score prediction (e.g. 1-0), pick
|
||||||
|
# main_pick from the subset of candidates that would WIN at that
|
||||||
|
# score. Stops the system from recommending OU25 Üst while also
|
||||||
|
# predicting 1-0 (only 1 goal). Falls back to original list if no
|
||||||
|
# coherent candidate exists.
|
||||||
|
coherent_set = self._score_consistent_markets(guarded)
|
||||||
|
coherent_flag = False
|
||||||
|
if coherent_set:
|
||||||
|
def is_coherent(row: Dict[str, Any]) -> bool:
|
||||||
|
m = str(row.get("market") or "")
|
||||||
|
p = str(row.get("pick") or "")
|
||||||
|
return (m, p) in coherent_set
|
||||||
|
|
||||||
|
approved_coh = [r for r in approved if is_coherent(r)]
|
||||||
|
watchlist_coh = [r for r in watchlist if is_coherent(r)]
|
||||||
|
|
||||||
|
if approved_coh:
|
||||||
|
approved = approved_coh
|
||||||
|
coherent_flag = True
|
||||||
|
elif watchlist_coh:
|
||||||
|
# No coherent BET candidates — at least promote a coherent
|
||||||
|
# watch over an incoherent BET.
|
||||||
|
watchlist = watchlist_coh + [r for r in watchlist if not is_coherent(r)]
|
||||||
|
coherent_flag = True
|
||||||
|
# Tag every row so the UI/diagnostics can see what happened
|
||||||
|
for row in judged_rows.values():
|
||||||
|
row.setdefault("betting_brain", {})
|
||||||
|
row["betting_brain"]["score_coherent"] = is_coherent(row)
|
||||||
|
|
||||||
original_main = guarded.get("main_pick") or {}
|
original_main = guarded.get("main_pick") or {}
|
||||||
main_pick = None
|
main_pick = None
|
||||||
decision = "NO_BET"
|
decision = "NO_BET"
|
||||||
@@ -142,10 +390,11 @@ 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-v1",
|
"version": "judge-v31f-national-regime",
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
"reason": decision_reason,
|
"reason": decision_reason,
|
||||||
"main_pick_key": main_key or None,
|
"main_pick_key": main_key or None,
|
||||||
|
"score_coherent_filter_applied": coherent_flag,
|
||||||
"approved_count": len(approved),
|
"approved_count": len(approved),
|
||||||
"watchlist_count": len(watchlist),
|
"watchlist_count": len(watchlist),
|
||||||
"rejected_count": len(rejected),
|
"rejected_count": len(rejected),
|
||||||
@@ -171,6 +420,10 @@ class BettingBrain:
|
|||||||
pick = str(row.get("pick") or "")
|
pick = str(row.get("pick") or "")
|
||||||
model_prob = self._market_probability(row, package)
|
model_prob = self._market_probability(row, package)
|
||||||
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
||||||
|
# V31f: national-team match flags (set by orchestrator in match_info).
|
||||||
|
_mi = package.get("match_info") or {}
|
||||||
|
is_national = bool(_mi.get("is_national"))
|
||||||
|
competition_type = str(_mi.get("competition_type") or "")
|
||||||
implied = (1.0 / odds) if odds > 1.0 else 0.0
|
implied = (1.0 / odds) if odds > 1.0 else 0.0
|
||||||
model_gap = (model_prob - implied) if model_prob is not None and implied > 0 else None
|
model_gap = (model_prob - implied) if model_prob is not None and implied > 0 else None
|
||||||
calibrated_conf = self._safe_float(row.get("calibrated_confidence", row.get("confidence")), 0.0) or 0.0
|
calibrated_conf = self._safe_float(row.get("calibrated_confidence", row.get("confidence")), 0.0) or 0.0
|
||||||
@@ -184,6 +437,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] = []
|
||||||
@@ -200,7 +468,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))
|
||||||
@@ -220,7 +488,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
|
||||||
@@ -243,6 +511,85 @@ class BettingBrain:
|
|||||||
if play_score < 50.0 and not is_value_sniper:
|
if play_score < 50.0 and not is_value_sniper:
|
||||||
vetoes.append("play_score_too_low")
|
vetoes.append("play_score_too_low")
|
||||||
|
|
||||||
|
# ── HARD EV-EDGE VETO ───────────────────────────────────────────
|
||||||
|
# Diagnostic backtest (1000 maç, 524 settled bet) gösterdi ki
|
||||||
|
# ev_edge < 0 olan bahisler %76 of all picks ve ROI yaklaşık -%16.
|
||||||
|
# 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).
|
||||||
|
# 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.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: # V29: hard veto, no sniper bypass
|
||||||
|
vetoes.append("market_muted_by_backtest")
|
||||||
|
issues.append(f"market_{market}_muted")
|
||||||
|
|
||||||
|
# ── 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 > legacy_env["max_edge"]:
|
||||||
|
vetoes.append("outside_envelope_edge_high")
|
||||||
|
if odds and odds < legacy_env["min_odds"]:
|
||||||
|
vetoes.append("outside_envelope_odds_low")
|
||||||
|
if odds and odds > legacy_env["max_odds"]:
|
||||||
|
vetoes.append("outside_envelope_odds_high")
|
||||||
|
if odds_rel < legacy_env["min_reliability"]:
|
||||||
|
vetoes.append("outside_envelope_reliability_low")
|
||||||
|
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 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
|
||||||
@@ -254,22 +601,10 @@ 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
|
score -= 14.0
|
||||||
if isinstance(triple, dict):
|
issues.append("trap_market_market_overpriced")
|
||||||
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")
|
|
||||||
|
|
||||||
if isinstance(triple, dict):
|
if isinstance(triple, dict):
|
||||||
if triple_is_value:
|
if triple_is_value:
|
||||||
@@ -371,6 +706,120 @@ 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}")
|
||||||
|
|
||||||
|
# ── V31f: NATIONAL-TEAM REGIME (overrides club logic) ─────────────
|
||||||
|
# For national matches the validated strategy is a narrow, mechanical
|
||||||
|
# value gate (MS / odds 4-7 / Hazırlık+Eleme). We REPLACE the club
|
||||||
|
# verdict so club-tuned vetoes/scores don't distort it. All the rich
|
||||||
|
# analysis (probs, model_gap, divergence, triple) above is preserved
|
||||||
|
# in the payload below — only action/score/stake are decided here.
|
||||||
|
national_gate_passed = False
|
||||||
|
if is_national:
|
||||||
|
in_band = self.NATIONAL_MIN_ODDS <= odds <= self.NATIONAL_MAX_ODDS
|
||||||
|
is_bet_market = market == self.NATIONAL_BET_MARKET
|
||||||
|
comp_ok = competition_type in self.NATIONAL_ALLOWED_COMPETITIONS
|
||||||
|
# Genuine safety vetoes still kill the bet even for national matches.
|
||||||
|
hard_unsafe = {
|
||||||
|
"low_reliability_league_hard_block",
|
||||||
|
"v25_v27_hard_disagreement",
|
||||||
|
"extreme_negative_ev",
|
||||||
|
"htft_reversal_risk_high",
|
||||||
|
}
|
||||||
|
has_hard_unsafe = any(v in hard_unsafe for v in vetoes)
|
||||||
|
|
||||||
|
national_vetoes: List[str] = []
|
||||||
|
if not is_bet_market:
|
||||||
|
national_vetoes.append("national_non_ms_market_muted")
|
||||||
|
if not comp_ok:
|
||||||
|
national_vetoes.append("national_tournament_no_bet")
|
||||||
|
if not in_band:
|
||||||
|
national_vetoes.append("national_odds_outside_value_band")
|
||||||
|
if has_hard_unsafe:
|
||||||
|
national_vetoes.append("national_hard_safety_veto")
|
||||||
|
|
||||||
|
if national_vetoes:
|
||||||
|
vetoes = national_vetoes
|
||||||
|
action = "REJECT"
|
||||||
|
score = min(score, 40.0)
|
||||||
|
issues.append(f"national_gate:{competition_type or 'unknown'}")
|
||||||
|
else:
|
||||||
|
vetoes = []
|
||||||
|
national_gate_passed = True
|
||||||
|
score = self.NATIONAL_BASE_SCORE + max(-4.0, min(8.0, ev_edge * 30.0))
|
||||||
|
score = max(0.0, min(100.0, score))
|
||||||
|
action = "BET"
|
||||||
|
positives.append("national_value_gate_passed")
|
||||||
|
issues.append(f"national_gate:{competition_type}")
|
||||||
|
# skip the club action logic below
|
||||||
|
row["betting_brain"] = {
|
||||||
|
"action": action,
|
||||||
|
"score": round(score, 1),
|
||||||
|
"summary": self._summary(action, market, pick, positives, issues, vetoes),
|
||||||
|
"positives": positives[:5],
|
||||||
|
"issues": issues[:6],
|
||||||
|
"vetoes": vetoes[:6],
|
||||||
|
"sniper_bypassed": sniper_bypassed,
|
||||||
|
"value_tier_bypassed": [],
|
||||||
|
"is_value_tier": False,
|
||||||
|
"is_national": True, # V31f
|
||||||
|
"competition_type": competition_type,
|
||||||
|
"trap_market_flag": trap_market_flag,
|
||||||
|
"trap_market_gap": trap_market_gap,
|
||||||
|
"tier_label": "national_value" if national_gate_passed else None,
|
||||||
|
"value_tier": None,
|
||||||
|
"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,
|
||||||
|
"v27_prob": round(v27_prob, 4) if v27_prob is not None else None,
|
||||||
|
"divergence": round(divergence, 4) if divergence is not None else None,
|
||||||
|
"triple_key": triple_key,
|
||||||
|
"triple_value": triple,
|
||||||
|
}
|
||||||
|
if national_gate_passed:
|
||||||
|
row["is_guaranteed"] = False # high-variance band, never "guaranteed"
|
||||||
|
row["pick_reason"] = "national_value_gate"
|
||||||
|
row["stake_units"] = self.NATIONAL_STAKE_UNITS
|
||||||
|
row["bet_grade"] = "B"
|
||||||
|
row["playable"] = True
|
||||||
|
else:
|
||||||
|
self._force_no_bet(row, f"betting_brain_{action.lower()}")
|
||||||
|
self._append_reason(row, f"betting_brain_national_{action.lower()}_{round(score)}")
|
||||||
|
return row
|
||||||
|
|
||||||
score = max(0.0, min(100.0, score))
|
score = max(0.0, min(100.0, score))
|
||||||
action = "BET"
|
action = "BET"
|
||||||
if vetoes:
|
if vetoes:
|
||||||
@@ -393,8 +842,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,
|
||||||
@@ -407,10 +860,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)}")
|
||||||
@@ -465,9 +923,13 @@ class BettingBrain:
|
|||||||
prob = self._safe_float(probs.get(pick), 0.0)
|
prob = self._safe_float(probs.get(pick), 0.0)
|
||||||
if prob is None or prob <= 0.0:
|
if prob is None or prob <= 0.0:
|
||||||
continue
|
continue
|
||||||
implied_odd = round(1.0 / prob, 2) if prob > 0.01 else 0.0
|
# V35c: only REAL bookmaker odds may be displayed. The old fallback
|
||||||
ref_odd = existing_odds_by_pick.get(pick) or implied_odd
|
# showed synthetic fair-odds (1/prob) as "Oran" — users could read
|
||||||
rows[key] = {
|
# it as an offered price (e.g. X shown at 4.53 while the bulletin
|
||||||
|
# offered ~3.58). No real price → odds 0.0 and the FE renders "-".
|
||||||
|
real = self._safe_float(getattr(self, "_ms_real_odds", {}).get(pick), 0.0) or 0.0
|
||||||
|
ref_odd = existing_odds_by_pick.get(pick) or (real if real > 1.01 else 0.0)
|
||||||
|
row = {
|
||||||
"market": "MS",
|
"market": "MS",
|
||||||
"pick": pick,
|
"pick": pick,
|
||||||
"probability": round(prob, 4),
|
"probability": round(prob, 4),
|
||||||
@@ -481,6 +943,12 @@ class BettingBrain:
|
|||||||
"bet_grade": "PASS",
|
"bet_grade": "PASS",
|
||||||
"decision_reasons": ["underdog_reference_for_completeness"],
|
"decision_reasons": ["underdog_reference_for_completeness"],
|
||||||
}
|
}
|
||||||
|
if ref_odd > 1.01:
|
||||||
|
# honest economics vs the real price (vig shows as it truly is)
|
||||||
|
row["implied_prob"] = round(1.0 / ref_odd, 4)
|
||||||
|
row["ev_edge"] = round(prob * ref_odd - 1.0, 4)
|
||||||
|
row["edge"] = row["ev_edge"]
|
||||||
|
rows[key] = row
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _merge_row(existing: Optional[Dict[str, Any]], incoming: Dict[str, Any]) -> Dict[str, Any]:
|
def _merge_row(existing: Optional[Dict[str, Any]], incoming: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
@@ -506,12 +974,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),
|
||||||
@@ -521,9 +991,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 {}
|
||||||
@@ -563,6 +1046,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
|
||||||
@@ -635,6 +1124,112 @@ class BettingBrain:
|
|||||||
return self._safe_float(ou25.get(key)) if key else None
|
return self._safe_float(ou25.get(key)) if key else None
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _score_consistent_markets(self, package: Dict[str, Any]) -> Optional[set]:
|
||||||
|
"""Build the set of (market, pick) tuples that WOULD WIN if the
|
||||||
|
model's own score prediction came true. We use this as a coherence
|
||||||
|
gate: if the model is confident about a 1-0 outcome but also wants
|
||||||
|
to play OU25 Üst, those two beliefs contradict each other — and the
|
||||||
|
score prediction is the more informative one because it aggregates
|
||||||
|
all market signals into a single most-likely scenario.
|
||||||
|
|
||||||
|
Returns None if the score prediction is missing or malformed; in
|
||||||
|
that case we skip the coherence check.
|
||||||
|
"""
|
||||||
|
score_pred = package.get("score_prediction") or {}
|
||||||
|
ft_raw = str(score_pred.get("ft") or score_pred.get("full_time") or "").strip()
|
||||||
|
ht_raw = str(score_pred.get("ht") or score_pred.get("half_time") or "").strip()
|
||||||
|
|
||||||
|
def parse(s: str) -> Optional[Tuple[int, int]]:
|
||||||
|
for sep in ("-", ":", "–"):
|
||||||
|
if sep in s:
|
||||||
|
parts = s.split(sep, 1)
|
||||||
|
try:
|
||||||
|
return int(parts[0].strip()), int(parts[1].strip())
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
ft = parse(ft_raw)
|
||||||
|
if ft is None:
|
||||||
|
return None
|
||||||
|
ht = parse(ht_raw)
|
||||||
|
|
||||||
|
fh, fa = ft
|
||||||
|
total = fh + fa
|
||||||
|
consistent: set = set()
|
||||||
|
|
||||||
|
# MS / 1X2 — single outcome
|
||||||
|
if fh > fa:
|
||||||
|
consistent.add(("MS", "1"))
|
||||||
|
consistent.add(("ML", "1"))
|
||||||
|
elif fh < fa:
|
||||||
|
consistent.add(("MS", "2"))
|
||||||
|
consistent.add(("ML", "2"))
|
||||||
|
else:
|
||||||
|
consistent.add(("MS", "X"))
|
||||||
|
consistent.add(("ML", "X"))
|
||||||
|
|
||||||
|
# DC — two of three legs win at any score
|
||||||
|
if fh >= fa:
|
||||||
|
consistent.add(("DC", "1X"))
|
||||||
|
if fh <= fa:
|
||||||
|
consistent.add(("DC", "X2"))
|
||||||
|
if fh != fa:
|
||||||
|
consistent.add(("DC", "12"))
|
||||||
|
|
||||||
|
# Over/Under main lines
|
||||||
|
for line, market in ((0.5, "OU05"), (1.5, "OU15"),
|
||||||
|
(2.5, "OU25"), (3.5, "OU35"), (4.5, "OU45")):
|
||||||
|
if total > line:
|
||||||
|
for p in ("Üst", "Ust", "Over", "OVER"):
|
||||||
|
consistent.add((market, p))
|
||||||
|
elif total < line:
|
||||||
|
for p in ("Alt", "Under", "UNDER"):
|
||||||
|
consistent.add((market, p))
|
||||||
|
# total == line → push, neither side wins → don't add
|
||||||
|
|
||||||
|
# BTTS — both teams score
|
||||||
|
if fh > 0 and fa > 0:
|
||||||
|
for p in ("Var", "KG Var", "Yes", "YES"):
|
||||||
|
consistent.add(("BTTS", p))
|
||||||
|
else:
|
||||||
|
for p in ("Yok", "KG Yok", "No", "NO"):
|
||||||
|
consistent.add(("BTTS", p))
|
||||||
|
|
||||||
|
# OE — total goals odd/even
|
||||||
|
if total % 2 == 1:
|
||||||
|
for p in ("Tek", "Odd", "ODD"):
|
||||||
|
consistent.add(("OE", p))
|
||||||
|
else:
|
||||||
|
for p in ("Çift", "Cift", "Even", "EVEN"):
|
||||||
|
consistent.add(("OE", p))
|
||||||
|
|
||||||
|
# HT-only markets (need HT score)
|
||||||
|
if ht is not None:
|
||||||
|
hh, ha = ht
|
||||||
|
ht_total = hh + ha
|
||||||
|
if hh > ha:
|
||||||
|
consistent.add(("HT", "1"))
|
||||||
|
elif hh < ha:
|
||||||
|
consistent.add(("HT", "2"))
|
||||||
|
else:
|
||||||
|
consistent.add(("HT", "X"))
|
||||||
|
for line, market in ((0.5, "HT_OU05"), (1.5, "HT_OU15"), (2.5, "HT_OU25")):
|
||||||
|
if ht_total > line:
|
||||||
|
for p in ("Üst", "Ust", "Over"):
|
||||||
|
consistent.add((market, p))
|
||||||
|
elif ht_total < line:
|
||||||
|
for p in ("Alt", "Under"):
|
||||||
|
consistent.add((market, p))
|
||||||
|
|
||||||
|
# HTFT — single combo
|
||||||
|
ht_o = "1" if hh > ha else "2" if hh < ha else "X"
|
||||||
|
ft_o = "1" if fh > fa else "2" if fh < fa else "X"
|
||||||
|
consistent.add(("HTFT", f"{ht_o}/{ft_o}"))
|
||||||
|
consistent.add(("HTFT", f"{ht_o}{ft_o}"))
|
||||||
|
|
||||||
|
return consistent
|
||||||
|
|
||||||
def _triple_value(self, package: Dict[str, Any], key: Optional[str]) -> Optional[Dict[str, Any]]:
|
def _triple_value(self, package: Dict[str, Any], key: Optional[str]) -> Optional[Dict[str, Any]]:
|
||||||
if not key:
|
if not key:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -449,6 +449,12 @@ class DataLoaderMixin:
|
|||||||
return 1.5, 1.2
|
return 1.5, 1.2
|
||||||
return weighted_for / total_weight, weighted_against / total_weight
|
return weighted_for / total_weight, weighted_against / total_weight
|
||||||
|
|
||||||
|
# Approximate European season window — Eredivisie/PL/La Liga start late
|
||||||
|
# July / mid-August, end May. Using 300 days as a buffer covers most
|
||||||
|
# competitions while excluding "career points" from previous seasons.
|
||||||
|
# When a proper seasons table lands this should query season boundaries.
|
||||||
|
_SEASON_LOOKBACK_MS = 300 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
def _estimate_league_position(
|
def _estimate_league_position(
|
||||||
self,
|
self,
|
||||||
cur: RealDictCursor,
|
cur: RealDictCursor,
|
||||||
@@ -458,6 +464,7 @@ class DataLoaderMixin:
|
|||||||
) -> int:
|
) -> int:
|
||||||
if not team_id or not league_id:
|
if not team_id or not league_id:
|
||||||
return 10
|
return 10
|
||||||
|
season_start_ms = before_date_ms - self._SEASON_LOOKBACK_MS
|
||||||
try:
|
try:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
@@ -478,6 +485,7 @@ class DataLoaderMixin:
|
|||||||
AND m.score_home IS NOT NULL
|
AND m.score_home IS NOT NULL
|
||||||
AND m.score_away IS NOT NULL
|
AND m.score_away IS NOT NULL
|
||||||
AND m.mst_utc < %s
|
AND m.mst_utc < %s
|
||||||
|
AND m.mst_utc >= %s
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
m.away_team_id AS team_id,
|
m.away_team_id AS team_id,
|
||||||
@@ -492,11 +500,15 @@ class DataLoaderMixin:
|
|||||||
AND m.score_home IS NOT NULL
|
AND m.score_home IS NOT NULL
|
||||||
AND m.score_away IS NOT NULL
|
AND m.score_away IS NOT NULL
|
||||||
AND m.mst_utc < %s
|
AND m.mst_utc < %s
|
||||||
|
AND m.mst_utc >= %s
|
||||||
) tm
|
) tm
|
||||||
GROUP BY tm.team_id
|
GROUP BY tm.team_id
|
||||||
ORDER BY points DESC
|
ORDER BY points DESC
|
||||||
""",
|
""",
|
||||||
(league_id, before_date_ms, league_id, before_date_ms),
|
(
|
||||||
|
league_id, before_date_ms, season_start_ms,
|
||||||
|
league_id, before_date_ms, season_start_ms,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
rows = cur.fetchall()
|
rows = cur.fetchall()
|
||||||
if not rows:
|
if not rows:
|
||||||
|
|||||||
@@ -225,20 +225,43 @@ class FeatureBuilderMixin:
|
|||||||
if enrichment_failures:
|
if enrichment_failures:
|
||||||
print(f"⚠️ Enrichment partial failures for {data.match_id}: {', '.join(enrichment_failures)}")
|
print(f"⚠️ Enrichment partial failures for {data.match_id}: {', '.join(enrichment_failures)}")
|
||||||
|
|
||||||
|
# ── Cup game detection (used by upset engine + elo dampening below) ──
|
||||||
|
_league_name_lower = (getattr(data, 'league_name', '') or '').lower()
|
||||||
|
_cup_keywords = ("kupa", "cup", "coupe", "copa", "coppa", "pokal",
|
||||||
|
"trophy", "shield", "ziraat", "süper kupa", "super cup",
|
||||||
|
"beker", "taça", "taca")
|
||||||
|
_is_cup_match = any(kw in _league_name_lower for kw in _cup_keywords)
|
||||||
|
|
||||||
|
# ── League size hint: top European leagues 18-20 teams, lower 16-24 ──
|
||||||
|
# We don't have a per-league team count, so fall back to 20 (standard).
|
||||||
|
# When standings infra lands this should pull from seasons table.
|
||||||
|
_league_total_teams = 20
|
||||||
|
|
||||||
# Upset engine features
|
# Upset engine features
|
||||||
upset_atmosphere, upset_motivation, upset_fatigue = 0.0, 0.0, 0.0
|
upset_atmosphere, upset_motivation, upset_fatigue = 0.0, 0.0, 0.0
|
||||||
try:
|
try:
|
||||||
upset_engine = get_upset_engine()
|
upset_engine = get_upset_engine()
|
||||||
|
# Use the real position estimates from data_loader; fall back to mid-
|
||||||
|
# table (10) only when the loader couldn't compute one. Hardcoding 10
|
||||||
|
# for every team made motivation_score collapse to 0 for everyone.
|
||||||
|
_home_pos = getattr(data, 'home_position', None)
|
||||||
|
_away_pos = getattr(data, 'away_position', None)
|
||||||
|
if _home_pos is None or _home_pos <= 0:
|
||||||
|
_home_pos = 10
|
||||||
|
if _away_pos is None or _away_pos <= 0:
|
||||||
|
_away_pos = 10
|
||||||
upset_feats = upset_engine.get_features(
|
upset_feats = upset_engine.get_features(
|
||||||
home_team_name=getattr(data, 'home_team_name', '') or '',
|
home_team_name=getattr(data, 'home_team_name', '') or '',
|
||||||
home_team_id=data.home_team_id,
|
home_team_id=data.home_team_id,
|
||||||
away_team_name=getattr(data, 'away_team_name', '') or '',
|
away_team_name=getattr(data, 'away_team_name', '') or '',
|
||||||
league_name=getattr(data, 'league_name', '') or '',
|
league_name=getattr(data, 'league_name', '') or '',
|
||||||
home_position=10,
|
home_position=_home_pos,
|
||||||
away_position=10,
|
away_position=_away_pos,
|
||||||
match_date_ms=data.match_date_ms,
|
match_date_ms=data.match_date_ms,
|
||||||
|
is_cup_match=_is_cup_match,
|
||||||
home_days_rest=int(home_rest),
|
home_days_rest=int(home_rest),
|
||||||
away_days_rest=int(away_rest),
|
away_days_rest=int(away_rest),
|
||||||
|
total_teams=_league_total_teams,
|
||||||
)
|
)
|
||||||
upset_atmosphere = upset_feats.get('upset_atmosphere', 0.0)
|
upset_atmosphere = upset_feats.get('upset_atmosphere', 0.0)
|
||||||
upset_motivation = upset_feats.get('upset_motivation', 0.0)
|
upset_motivation = upset_feats.get('upset_motivation', 0.0)
|
||||||
@@ -276,15 +299,10 @@ class FeatureBuilderMixin:
|
|||||||
is_season_start = 1.0 if match_month in (7, 8, 9) else 0.0
|
is_season_start = 1.0 if match_month in (7, 8, 9) else 0.0
|
||||||
is_season_end = 1.0 if match_month in (5, 6) else 0.0
|
is_season_end = 1.0 if match_month in (5, 6) else 0.0
|
||||||
|
|
||||||
# ── Cup game detection: dampen home advantage in feature space ──
|
|
||||||
_league_name = (getattr(data, 'league_name', '') or '').lower()
|
|
||||||
_cup_keywords = ("kupa", "cup", "coupe", "copa", "coppa", "pokal",
|
|
||||||
"trophy", "shield", "ziraat", "süper kupa", "super cup")
|
|
||||||
_is_cup = any(kw in _league_name for kw in _cup_keywords)
|
|
||||||
|
|
||||||
# ── Derived / Interaction features (V27) ──
|
# ── Derived / Interaction features (V27) ──
|
||||||
# Cup games: home ELO advantage is ~30% weaker (rotation, lower motivation)
|
# Cup games: home ELO advantage is ~30% weaker (rotation, lower motivation)
|
||||||
elo_diff = (home_elo - away_elo) * (0.70 if _is_cup else 1.0)
|
# Uses _is_cup_match computed earlier (before upset engine call).
|
||||||
|
elo_diff = (home_elo - away_elo) * (0.70 if _is_cup_match else 1.0)
|
||||||
form_elo_diff = home_form_elo_val - away_form_elo_val
|
form_elo_diff = home_form_elo_val - away_form_elo_val
|
||||||
attack_vs_defense_home = data.home_goals_avg - data.away_conceded_avg
|
attack_vs_defense_home = data.home_goals_avg - data.away_conceded_avg
|
||||||
attack_vs_defense_away = data.away_goals_avg - data.home_conceded_avg
|
attack_vs_defense_away = data.away_goals_avg - data.home_conceded_avg
|
||||||
|
|||||||
@@ -56,10 +56,81 @@ from services.match_commentary import generate_match_commentary
|
|||||||
from utils.top_leagues import load_top_league_ids
|
from utils.top_leagues import load_top_league_ids
|
||||||
from utils.league_reliability import load_league_reliability
|
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, get_final_recalibrator
|
||||||
|
from models.market_anchor import devig, apply_corrections
|
||||||
|
from models.score_matrix import build_calibrated_score_package
|
||||||
|
from models.live_matrix import build_live_projection, estimate_minute
|
||||||
|
|
||||||
|
# ── 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 _league_confidence_for(self, league_id: Optional[str]) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Return the backtest-derived confidence record for a league, or None.
|
||||||
|
|
||||||
|
Shape: {"label": high|medium|low, "bet_roi": float, "bet_n": int,
|
||||||
|
"hit": float}. None → league absent or too few bets ('unknown') → FE
|
||||||
|
shows no badge. Never raises (missing artifact = graceful None)."""
|
||||||
|
if not league_id:
|
||||||
|
return None
|
||||||
|
lookup = getattr(self, "league_confidence", None) or {}
|
||||||
|
info = lookup.get(str(league_id))
|
||||||
|
if not isinstance(info, dict):
|
||||||
|
return None
|
||||||
|
label = info.get("label")
|
||||||
|
if label in (None, "unknown"):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"label": label,
|
||||||
|
"bet_roi": info.get("bet_roi"),
|
||||||
|
"bet_n": info.get("bet_n"),
|
||||||
|
"hit": info.get("hit"),
|
||||||
|
}
|
||||||
|
|
||||||
|
def _is_national_match(self, league_id: Optional[str]) -> bool:
|
||||||
|
"""True if this league is an A-milli (senior men's) national competition."""
|
||||||
|
if not league_id:
|
||||||
|
return False
|
||||||
|
natl = getattr(self, "national_leagues", None) or set()
|
||||||
|
return str(league_id) in natl
|
||||||
|
|
||||||
|
def _competition_type_for(
|
||||||
|
self, league_id: Optional[str], league_name: Optional[str]
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""For national matches, classify HAZIRLIK/ELEME/TURNUVA from the league
|
||||||
|
name. None for non-national leagues (clubs don't use this)."""
|
||||||
|
if not self._is_national_match(league_id):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
from utils.national_leagues import classify_competition
|
||||||
|
return classify_competition(league_name or "")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
def _build_prediction_package(
|
def _build_prediction_package(
|
||||||
self,
|
self,
|
||||||
data: MatchData,
|
data: MatchData,
|
||||||
@@ -280,6 +351,34 @@ class MarketBoardMixin:
|
|||||||
if market in available_markets
|
if market in available_markets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# V35: anchor the DISPLAYED per-market probabilities to the de-vigged
|
||||||
|
# market price (+ proven home-favourite correction). The model's own
|
||||||
|
# numbers were measured ~25-30% mis-calibrated; the de-vigged market is
|
||||||
|
# ~1.5% (out-of-sample). This only rewrites what the user sees.
|
||||||
|
market_board = self._apply_market_anchor(market_board, data)
|
||||||
|
|
||||||
|
# V35b: make the DISPLAYED confidence/edge fields on every pick object
|
||||||
|
# consistent with the calibrated board (Güven Skoru, Güven Aralığı,
|
||||||
|
# Model%/Teorik-avantaj), then drop a "value pick" that has no real edge
|
||||||
|
# once priced honestly — no fabricated value bets.
|
||||||
|
self._apply_anchor_to_picks(
|
||||||
|
market_board, main_pick, value_pick, aggressive_pick, supporting, bet_summary,
|
||||||
|
)
|
||||||
|
if value_pick is not None and float(value_pick.get("ev_edge", 0.0) or 0.0) <= 0.0:
|
||||||
|
value_pick = None
|
||||||
|
|
||||||
|
# V36: derive the score card (score_prediction + scenario_top5) from the
|
||||||
|
# SAME anchored probabilities, so it can never contradict the MS card.
|
||||||
|
# Validated on 63,681 real-odds matches: modal-score hit 12.6% vs stated
|
||||||
|
# 13.1%, top-5 coverage 51%, per-score gaps <1.2pt.
|
||||||
|
cal_score = self._build_calibrated_score(market_board)
|
||||||
|
|
||||||
|
# V38: while the match is LIVE, also project score/minute-conditioned
|
||||||
|
# probabilities (P(side scores again), live 1X2, comeback, scenarios).
|
||||||
|
# OOS-validated on 70,410 reconstructed live moments: ECE 0.5-0.8%;
|
||||||
|
# "one-goal lead at 80'" case: said 21.7% vs actual 23.0%.
|
||||||
|
live_projection = self._build_live_projection(market_board, data)
|
||||||
|
|
||||||
# Determine simulation mode for the response
|
# Determine simulation mode for the response
|
||||||
_resp_status = str(data.status or "").upper()
|
_resp_status = str(data.status or "").upper()
|
||||||
_resp_state = str(data.state or "").upper()
|
_resp_state = str(data.state or "").upper()
|
||||||
@@ -294,6 +393,13 @@ class MarketBoardMixin:
|
|||||||
"home_team": data.home_team_name,
|
"home_team": data.home_team_name,
|
||||||
"away_team": data.away_team_name,
|
"away_team": data.away_team_name,
|
||||||
"league": data.league_name,
|
"league": data.league_name,
|
||||||
|
"league_id": data.league_id,
|
||||||
|
# Backtest-derived per-league confidence (ROI + sample size).
|
||||||
|
# None when the league has too little data to judge → FE shows no badge.
|
||||||
|
"league_confidence": self._league_confidence_for(data.league_id),
|
||||||
|
# National-team match flags (drive betting_brain's national gate).
|
||||||
|
"is_national": self._is_national_match(data.league_id),
|
||||||
|
"competition_type": self._competition_type_for(data.league_id, data.league_name),
|
||||||
"match_date_ms": data.match_date_ms,
|
"match_date_ms": data.match_date_ms,
|
||||||
"sport": data.sport,
|
"sport": data.sport,
|
||||||
# Live snapshot — match_commentary uses this to detect upset-in-progress
|
# Live snapshot — match_commentary uses this to detect upset-in-progress
|
||||||
@@ -332,15 +438,24 @@ class MarketBoardMixin:
|
|||||||
"bet_summary": bet_summary,
|
"bet_summary": bet_summary,
|
||||||
"supporting_picks": supporting,
|
"supporting_picks": supporting,
|
||||||
"aggressive_pick": aggressive_pick,
|
"aggressive_pick": aggressive_pick,
|
||||||
"scenario_top5": prediction.ft_scores_top5,
|
"scenario_top5": (
|
||||||
"score_prediction": {
|
cal_score["scenario_top5"] if cal_score else prediction.ft_scores_top5
|
||||||
"ft": prediction.predicted_ft_score,
|
),
|
||||||
"ht": prediction.predicted_ht_score,
|
"score_prediction": (
|
||||||
"xg_home": round(float(prediction.home_xg), 2),
|
cal_score["score_prediction"]
|
||||||
"xg_away": round(float(prediction.away_xg), 2),
|
if cal_score
|
||||||
"xg_total": round(float(prediction.total_xg), 2),
|
else {
|
||||||
},
|
"ft": prediction.predicted_ft_score,
|
||||||
|
"ht": prediction.predicted_ht_score,
|
||||||
|
"xg_home": round(float(prediction.home_xg), 2),
|
||||||
|
"xg_away": round(float(prediction.away_xg), 2),
|
||||||
|
"xg_total": round(float(prediction.total_xg), 2),
|
||||||
|
}
|
||||||
|
),
|
||||||
"market_board": market_board,
|
"market_board": market_board,
|
||||||
|
# V38: score/minute-aware live probabilities (None when not live or
|
||||||
|
# no real odds). FE can render "deplasman gol atar: %X / dönme: %Y".
|
||||||
|
"live_projection": live_projection,
|
||||||
"others": {
|
"others": {
|
||||||
"handicap": prediction.handicap_pick,
|
"handicap": prediction.handicap_pick,
|
||||||
"cards": {
|
"cards": {
|
||||||
@@ -942,6 +1057,301 @@ class MarketBoardMixin:
|
|||||||
}
|
}
|
||||||
return merged
|
return merged
|
||||||
|
|
||||||
|
# ── V35 market-anchored calibration ────────────────────────────────
|
||||||
|
# Maps a board pick label -> the probs key it refers to, so the displayed
|
||||||
|
# confidence can be set to the EXISTING pick's now-calibrated probability.
|
||||||
|
_ANCHOR_PICK_KEY: Dict[str, Dict[str, str]] = {
|
||||||
|
"MS": {"1": "1", "X": "X", "0": "X", "2": "2"},
|
||||||
|
"HT": {"1": "1", "X": "X", "0": "X", "2": "2"},
|
||||||
|
"DC": {"1X": "1X", "X2": "X2", "12": "12",
|
||||||
|
"1-X": "1X", "X-2": "X2", "1-2": "12"},
|
||||||
|
"OU15": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
|
||||||
|
"OU25": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
|
||||||
|
"OU35": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
|
||||||
|
"HT_OU05": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
|
||||||
|
"HT_OU15": {"Üst": "over", "Alt": "under", "Over": "over", "Under": "under"},
|
||||||
|
"BTTS": {"KG Var": "yes", "KG Yok": "no", "Var": "yes", "Yok": "no",
|
||||||
|
"Yes": "yes", "No": "no"},
|
||||||
|
"OE": {"Tek": "odd", "Çift": "even", "Odd": "odd", "Even": "even"},
|
||||||
|
}
|
||||||
|
|
||||||
|
def _set_board(
|
||||||
|
self,
|
||||||
|
market_board: Dict[str, Any],
|
||||||
|
market: str,
|
||||||
|
probs: Dict[str, float],
|
||||||
|
) -> None:
|
||||||
|
"""Overwrite one board entry's probs with calibrated values and refresh
|
||||||
|
its confidence to the EXISTING pick's now-calibrated probability.
|
||||||
|
|
||||||
|
We recalibrate the NUMBERS, not the pick selection — showing the engine's
|
||||||
|
pick alongside its honest probability. Falls back to the most-likely
|
||||||
|
outcome only when the pick can't be mapped."""
|
||||||
|
entry = market_board.get(market)
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
return
|
||||||
|
rounded = {k: round(float(v), 4) for k, v in probs.items()}
|
||||||
|
if not rounded:
|
||||||
|
return
|
||||||
|
entry["probs"] = rounded
|
||||||
|
pick = str(entry.get("pick") or "")
|
||||||
|
key = self._ANCHOR_PICK_KEY.get(market, {}).get(pick)
|
||||||
|
if key is None or key not in rounded:
|
||||||
|
key = max(rounded, key=rounded.get)
|
||||||
|
entry["confidence"] = round(rounded[key] * 100.0, 1)
|
||||||
|
entry["calibration_source"] = "market_anchor_v35"
|
||||||
|
|
||||||
|
def _apply_market_anchor(
|
||||||
|
self,
|
||||||
|
market_board: Dict[str, Any],
|
||||||
|
data: MatchData,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Anchor DISPLAYED per-market probabilities to the de-vigged market
|
||||||
|
price (+ proven home-favourite correction for MS, and DC derived from
|
||||||
|
it for internal consistency).
|
||||||
|
|
||||||
|
Only markets with REAL odds are rewritten — `devig` returns None for any
|
||||||
|
missing/placeholder leg, so no-odds markets are left untouched (and are
|
||||||
|
already dropped upstream per the product rule: never show fabricated
|
||||||
|
numbers for a match without odds). Toggle off with env MARKET_ANCHOR_CAL=0.
|
||||||
|
"""
|
||||||
|
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
|
||||||
|
return market_board
|
||||||
|
if not isinstance(market_board, dict) or not market_board:
|
||||||
|
return market_board
|
||||||
|
odds = getattr(data, "odds_data", None) or {}
|
||||||
|
|
||||||
|
def real(key: str) -> Optional[float]:
|
||||||
|
val = self._real_market_odds(odds, key)
|
||||||
|
return val if val > 1.01 else None
|
||||||
|
|
||||||
|
# MS (3-way) + favourite corrections; DC derived from the same vector
|
||||||
|
ms = devig([real("ms_h"), real("ms_d"), real("ms_a")])
|
||||||
|
if ms is not None:
|
||||||
|
p1, px, p2 = apply_corrections(*ms)
|
||||||
|
if "MS" in market_board:
|
||||||
|
self._set_board(market_board, "MS", {"1": p1, "X": px, "2": p2})
|
||||||
|
if "DC" in market_board:
|
||||||
|
self._set_board(
|
||||||
|
market_board, "DC",
|
||||||
|
{"1X": p1 + px, "X2": px + p2, "12": p1 + p2},
|
||||||
|
)
|
||||||
|
|
||||||
|
# HT (3-way)
|
||||||
|
ht = devig([real("ht_h"), real("ht_d"), real("ht_a")])
|
||||||
|
if ht is not None and "HT" in market_board:
|
||||||
|
self._set_board(market_board, "HT", {"1": ht[0], "X": ht[1], "2": ht[2]})
|
||||||
|
|
||||||
|
# 2-way markets
|
||||||
|
for mk, ko, ku, lo, lu in (
|
||||||
|
("OU15", "ou15_o", "ou15_u", "over", "under"),
|
||||||
|
("OU25", "ou25_o", "ou25_u", "over", "under"),
|
||||||
|
("OU35", "ou35_o", "ou35_u", "over", "under"),
|
||||||
|
("BTTS", "btts_y", "btts_n", "yes", "no"),
|
||||||
|
("OE", "oe_odd", "oe_even", "odd", "even"),
|
||||||
|
("HT_OU05", "ht_ou05_o", "ht_ou05_u", "over", "under"),
|
||||||
|
("HT_OU15", "ht_ou15_o", "ht_ou15_u", "over", "under"),
|
||||||
|
):
|
||||||
|
if mk not in market_board:
|
||||||
|
continue
|
||||||
|
pair = devig([real(ko), real(ku)])
|
||||||
|
if pair is not None:
|
||||||
|
self._set_board(market_board, mk, {lo: pair[0], lu: pair[1]})
|
||||||
|
|
||||||
|
return market_board
|
||||||
|
|
||||||
|
def _anchored_prob_for(
|
||||||
|
self,
|
||||||
|
market_board: Dict[str, Any],
|
||||||
|
market: str,
|
||||||
|
pick: Any,
|
||||||
|
) -> Optional[float]:
|
||||||
|
"""Look up a pick's calibrated probability from the anchored board.
|
||||||
|
|
||||||
|
Returns None unless the market was actually anchored (real odds) and the
|
||||||
|
pick maps to a known outcome — so no-odds picks are never touched."""
|
||||||
|
entry = market_board.get(str(market or ""))
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
return None
|
||||||
|
if entry.get("calibration_source") != "market_anchor_v35":
|
||||||
|
return None
|
||||||
|
probs = entry.get("probs") or {}
|
||||||
|
key = self._ANCHOR_PICK_KEY.get(str(market or ""), {}).get(str(pick or ""))
|
||||||
|
if key is None or key not in probs:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return float(probs[key])
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _recalibrate_pick_display(
|
||||||
|
self,
|
||||||
|
obj: Optional[Dict[str, Any]],
|
||||||
|
market_board: Dict[str, Any],
|
||||||
|
) -> None:
|
||||||
|
"""Rewrite ONE pick object's displayed confidence/edge fields so they are
|
||||||
|
consistent with the calibrated (de-vigged market) probability.
|
||||||
|
|
||||||
|
Fixes Güven Skoru (`calibrated_confidence`/`unified_score`), Güven Aralığı
|
||||||
|
(`confidence_interval` recentred on the calibrated confidence), and the
|
||||||
|
value card's Model%/Teorik-avantaj (`model_probability`/`ev_edge`/`edge`,
|
||||||
|
recomputed honestly against the real price → the vig shows as it truly is,
|
||||||
|
no fabricated positive edge). Selection/gates/stake are left untouched."""
|
||||||
|
if not isinstance(obj, dict):
|
||||||
|
return
|
||||||
|
p = self._anchored_prob_for(market_board, obj.get("market"), obj.get("pick"))
|
||||||
|
if p is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
odds = float(obj.get("odds") or 0.0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
odds = 0.0
|
||||||
|
implied = (1.0 / odds) if odds > 1.0 else 0.0
|
||||||
|
conf = round(p * 100.0, 1)
|
||||||
|
ev = round(p * odds - 1.0, 4) if odds > 1.0 else 0.0
|
||||||
|
obj["calibrated_probability"] = round(p, 4)
|
||||||
|
obj["model_probability"] = round(p, 4)
|
||||||
|
obj["calibrated_confidence"] = conf
|
||||||
|
obj["unified_score"] = conf
|
||||||
|
obj["implied_prob"] = round(implied, 4)
|
||||||
|
obj["model_edge"] = round(p - implied, 4) if implied > 0.0 else 0.0
|
||||||
|
obj["ev_edge"] = ev
|
||||||
|
obj["edge"] = ev
|
||||||
|
# Recentre the confidence interval on the calibrated confidence, keeping a
|
||||||
|
# sensible width (preserve the engine's width when present).
|
||||||
|
width = 16.0
|
||||||
|
ci = obj.get("confidence_interval")
|
||||||
|
if isinstance(ci, dict) and ci.get("lower") is not None and ci.get("upper") is not None:
|
||||||
|
try:
|
||||||
|
width = max(6.0, float(ci["upper"]) - float(ci["lower"]))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
width = 16.0
|
||||||
|
half = width / 2.0
|
||||||
|
lower = round(max(0.0, conf - half), 1)
|
||||||
|
upper = round(min(100.0, conf + half), 1)
|
||||||
|
band = "HIGH" if conf >= 60.0 else "MEDIUM" if conf >= 42.0 else "LOW"
|
||||||
|
obj["confidence_interval"] = {
|
||||||
|
"band": band,
|
||||||
|
"lower": lower,
|
||||||
|
"upper": upper,
|
||||||
|
"width": round(upper - lower, 1),
|
||||||
|
"threshold_met": conf >= 50.0,
|
||||||
|
}
|
||||||
|
obj["confidence_band"] = band
|
||||||
|
obj["calibration_source"] = "market_anchor_v35"
|
||||||
|
|
||||||
|
def _apply_anchor_to_picks(
|
||||||
|
self,
|
||||||
|
market_board: Dict[str, Any],
|
||||||
|
main_pick: Optional[Dict[str, Any]],
|
||||||
|
value_pick: Optional[Dict[str, Any]],
|
||||||
|
aggressive_pick: Optional[Dict[str, Any]],
|
||||||
|
supporting: Optional[List[Dict[str, Any]]],
|
||||||
|
bet_summary: Optional[List[Dict[str, Any]]],
|
||||||
|
) -> None:
|
||||||
|
"""Make every DISPLAYED pick object consistent with the anchored board.
|
||||||
|
Toggle off with env MARKET_ANCHOR_CAL=0."""
|
||||||
|
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
|
||||||
|
return
|
||||||
|
for obj in (main_pick, value_pick, aggressive_pick):
|
||||||
|
self._recalibrate_pick_display(obj, market_board)
|
||||||
|
for obj in list(supporting or []):
|
||||||
|
self._recalibrate_pick_display(obj, market_board)
|
||||||
|
for obj in list(bet_summary or []):
|
||||||
|
self._recalibrate_pick_display(obj, market_board)
|
||||||
|
|
||||||
|
def _build_calibrated_score(
|
||||||
|
self,
|
||||||
|
market_board: Dict[str, Any],
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""V36: score card derived from the anchored MS + OU25 probabilities.
|
||||||
|
|
||||||
|
Returns {"score_prediction": {...}, "scenario_top5": [...]} or None when
|
||||||
|
the needed markets weren't anchored (no real odds) — in which case the
|
||||||
|
caller keeps the model's own score output. Same kill-switch as V35."""
|
||||||
|
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
|
||||||
|
return None
|
||||||
|
ms = market_board.get("MS") or {}
|
||||||
|
ou = market_board.get("OU25") or {}
|
||||||
|
if (
|
||||||
|
ms.get("calibration_source") != "market_anchor_v35"
|
||||||
|
or ou.get("calibration_source") != "market_anchor_v35"
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
p1 = float(ms["probs"]["1"])
|
||||||
|
px = float(ms["probs"]["X"])
|
||||||
|
p2 = float(ms["probs"]["2"])
|
||||||
|
p_over = float(ou["probs"]["over"])
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
ht_probs = None
|
||||||
|
ht = market_board.get("HT") or {}
|
||||||
|
if ht.get("calibration_source") == "market_anchor_v35":
|
||||||
|
try:
|
||||||
|
ht_probs = (
|
||||||
|
float(ht["probs"]["1"]),
|
||||||
|
float(ht["probs"]["X"]),
|
||||||
|
float(ht["probs"]["2"]),
|
||||||
|
)
|
||||||
|
except (KeyError, TypeError, ValueError):
|
||||||
|
ht_probs = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
pkg = build_calibrated_score_package(p1, px, p2, p_over, ht_probs=ht_probs)
|
||||||
|
except (ValueError, ZeroDivisionError, OverflowError):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"score_prediction": {
|
||||||
|
"ft": pkg["ft"],
|
||||||
|
"ht": pkg["ht"],
|
||||||
|
"xg_home": pkg["xg_home"],
|
||||||
|
"xg_away": pkg["xg_away"],
|
||||||
|
"xg_total": pkg["xg_total"],
|
||||||
|
"ht_top3": pkg["ht_top"],
|
||||||
|
"calibration_source": pkg["calibration_source"],
|
||||||
|
},
|
||||||
|
"scenario_top5": pkg["scenario_top5"],
|
||||||
|
}
|
||||||
|
|
||||||
|
def _build_live_projection(
|
||||||
|
self,
|
||||||
|
market_board: Dict[str, Any],
|
||||||
|
data: MatchData,
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""V38: score/minute-conditioned live projection from the anchored
|
||||||
|
probabilities. None unless the match is live, both MS and OU25 were
|
||||||
|
anchored (real odds) and a minute estimate exists. Same kill-switch."""
|
||||||
|
if os.environ.get("MARKET_ANCHOR_CAL", "1") == "0":
|
||||||
|
return None
|
||||||
|
if not self._is_live_match(data):
|
||||||
|
return None
|
||||||
|
ms = market_board.get("MS") or {}
|
||||||
|
ou = market_board.get("OU25") or {}
|
||||||
|
if (
|
||||||
|
ms.get("calibration_source") != "market_anchor_v35"
|
||||||
|
or ou.get("calibration_source") != "market_anchor_v35"
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
minute = estimate_minute(
|
||||||
|
getattr(data, "match_date_ms", None), int(time.time() * 1000)
|
||||||
|
)
|
||||||
|
if minute is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return build_live_projection(
|
||||||
|
float(ms["probs"]["1"]),
|
||||||
|
float(ms["probs"]["X"]),
|
||||||
|
float(ms["probs"]["2"]),
|
||||||
|
float(ou["probs"]["over"]),
|
||||||
|
int(data.current_score_home or 0),
|
||||||
|
int(data.current_score_away or 0),
|
||||||
|
minute,
|
||||||
|
)
|
||||||
|
except (KeyError, TypeError, ValueError, ZeroDivisionError, OverflowError):
|
||||||
|
return None
|
||||||
|
|
||||||
def _build_market_rows(
|
def _build_market_rows(
|
||||||
self,
|
self,
|
||||||
data: MatchData,
|
data: MatchData,
|
||||||
@@ -1114,10 +1524,31 @@ 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))
|
||||||
|
|
||||||
|
# ── FINAL-OUTPUT RECALIBRATION (V31e) ──────────────────────────
|
||||||
|
# Last-step per-market map: "system says X% -> reality is Y%". ONLY
|
||||||
|
# badly-miscalibrated markets carry a map (fit-ECE >= 5: OU15, OU35,
|
||||||
|
# HT_OU05, HT_OU15). MS and every already-good market pass through
|
||||||
|
# UNCHANGED -> guaranteed no regression. Out-of-sample proven (e.g.
|
||||||
|
# HT_OU15 ECE 29.2->0.8) and identity-safe for MS (1.1->1.3).
|
||||||
|
# This adjusts ONLY the displayed confidence so users see honest
|
||||||
|
# probabilities; all analysis below (probabilities, edges, vetoes,
|
||||||
|
# tiers, bands) is preserved, and the pre-recal value is kept for audit.
|
||||||
|
pre_recal_conf = calibrated_conf
|
||||||
|
calibrated_conf = get_final_recalibrator().recalibrate_conf(market, calibrated_conf)
|
||||||
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 +1609,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 +1716,50 @@ 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),
|
||||||
|
"calibrated_confidence_pre_recal": round(pre_recal_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 +1819,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)),
|
||||||
|
|||||||
@@ -60,8 +60,23 @@ from models.calibration import get_calibrator
|
|||||||
|
|
||||||
|
|
||||||
class UpperBrainMixin:
|
class UpperBrainMixin:
|
||||||
def _apply_upper_brain_guards(self, package: Dict[str, Any]) -> Dict[str, Any]:
|
def _apply_upper_brain_guards(
|
||||||
return BettingBrain().judge(package)
|
self, package: Dict[str, Any], data: Any = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
# V35c: hand the brain the REAL bookmaker MS odds so its reference rows
|
||||||
|
# can never display synthetic 1/p prices as if they were offered.
|
||||||
|
ms_real_odds = None
|
||||||
|
if data is not None:
|
||||||
|
try:
|
||||||
|
odds = getattr(data, "odds_data", None) or {}
|
||||||
|
ms_real_odds = {
|
||||||
|
"1": self._real_market_odds(odds, "ms_h"),
|
||||||
|
"X": self._real_market_odds(odds, "ms_d"),
|
||||||
|
"2": self._real_market_odds(odds, "ms_a"),
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
ms_real_odds = None
|
||||||
|
return BettingBrain().judge(package, ms_real_odds=ms_real_odds)
|
||||||
|
|
||||||
v27_engine = package.get("v27_engine")
|
v27_engine = package.get("v27_engine")
|
||||||
if not isinstance(v27_engine, dict) or not v27_engine.get("triple_value"):
|
if not isinstance(v27_engine, dict) or not v27_engine.get("triple_value"):
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ from services.v26_shadow_engine import V26ShadowEngine, get_v26_shadow_engine
|
|||||||
from services.match_commentary import generate_match_commentary
|
from services.match_commentary import generate_match_commentary
|
||||||
from utils.top_leagues import load_top_league_ids
|
from utils.top_leagues import load_top_league_ids
|
||||||
from utils.league_reliability import load_league_reliability
|
from utils.league_reliability import load_league_reliability
|
||||||
|
from utils.league_confidence import load_league_confidence
|
||||||
|
from utils.national_leagues import load_national_leagues
|
||||||
from config.config_loader import build_threshold_dict, get_threshold_default, get_config
|
from config.config_loader import build_threshold_dict, get_threshold_default, get_config
|
||||||
from models.calibration import get_calibrator
|
from models.calibration import get_calibrator
|
||||||
|
|
||||||
@@ -171,6 +173,8 @@ class SingleMatchOrchestrator(
|
|||||||
self.engine_mode = str(os.getenv("AI_ENGINE_MODE", "v28-pro-max")).strip().lower()
|
self.engine_mode = str(os.getenv("AI_ENGINE_MODE", "v28-pro-max")).strip().lower()
|
||||||
self.top_league_ids = load_top_league_ids()
|
self.top_league_ids = load_top_league_ids()
|
||||||
self.league_reliability = load_league_reliability()
|
self.league_reliability = load_league_reliability()
|
||||||
|
self.league_confidence = load_league_confidence()
|
||||||
|
self.national_leagues = load_national_leagues()
|
||||||
self.enrichment = FeatureEnrichmentService()
|
self.enrichment = FeatureEnrichmentService()
|
||||||
self.odds_band_analyzer = OddsBandAnalyzer()
|
self.odds_band_analyzer = OddsBandAnalyzer()
|
||||||
# ── Market Thresholds (loaded from config/market_thresholds.json) ──
|
# ── Market Thresholds (loaded from config/market_thresholds.json) ──
|
||||||
@@ -664,7 +668,28 @@ class SingleMatchOrchestrator(
|
|||||||
base_package.setdefault("analysis_details", {})
|
base_package.setdefault("analysis_details", {})
|
||||||
base_package["analysis_details"]["v27_loaded"] = False
|
base_package["analysis_details"]["v27_loaded"] = False
|
||||||
|
|
||||||
base_package = self._apply_upper_brain_guards(base_package)
|
base_package = self._apply_upper_brain_guards(base_package, data)
|
||||||
|
|
||||||
|
# V35c: the brain rebuilt main/value/supporting/bet_summary AFTER the
|
||||||
|
# market anchor ran inside _build_prediction_package — re-stamp the
|
||||||
|
# calibrated display fields (Güven/CI/Model%/edge) so they stay
|
||||||
|
# consistent, BEFORE the commentary reads the package.
|
||||||
|
self._apply_anchor_to_picks(
|
||||||
|
base_package.get("market_board") or {},
|
||||||
|
base_package.get("main_pick"),
|
||||||
|
base_package.get("value_pick"),
|
||||||
|
base_package.get("aggressive_pick"),
|
||||||
|
base_package.get("supporting_picks"),
|
||||||
|
base_package.get("bet_summary"),
|
||||||
|
)
|
||||||
|
_mp = base_package.get("main_pick")
|
||||||
|
_advice = base_package.get("bet_advice")
|
||||||
|
if isinstance(_mp, dict) and isinstance(_advice, dict) and _mp.get("confidence_band"):
|
||||||
|
_advice["confidence_band"] = _mp["confidence_band"]
|
||||||
|
# no fabricated value bets: a value pick must carry measured positive edge
|
||||||
|
_vp = base_package.get("value_pick")
|
||||||
|
if isinstance(_vp, dict) and float(_vp.get("ev_edge", 0.0) or 0.0) <= 0.0:
|
||||||
|
base_package["value_pick"] = None
|
||||||
|
|
||||||
# ── Match Commentary: human-readable summary ──────────────
|
# ── Match Commentary: human-readable summary ──────────────
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Unit tests for V38 live-conditioned projection (pure, no DB/model deps)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from models.live_matrix import (
|
||||||
|
build_live_projection,
|
||||||
|
estimate_minute,
|
||||||
|
state_multiplier,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _approx(a, b, tol=1e-6):
|
||||||
|
return abs(a - b) <= tol
|
||||||
|
|
||||||
|
|
||||||
|
def test_probs_form_distribution():
|
||||||
|
proj = build_live_projection(0.50, 0.27, 0.23, 0.55, 1, 0, 60)
|
||||||
|
p = proj["probs"]
|
||||||
|
assert _approx(p["1"] + p["X"] + p["2"], 1.0, 1e-3)
|
||||||
|
assert 0.0 <= proj["p_away_scores_again"] <= 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_minute_one_roughly_matches_prematch():
|
||||||
|
# at 0-0 minute 1 the projection must stay close to the anchored numbers
|
||||||
|
proj = build_live_projection(0.50, 0.27, 0.23, 0.55, 0, 0, 1)
|
||||||
|
assert abs(proj["probs"]["1"] - 0.50) < 0.06
|
||||||
|
assert abs(proj["probs"]["2"] - 0.23) < 0.06
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_goal_lead_at_80():
|
||||||
|
# the user's exact case: 1-0 at 80' (OOS-validated: said 21.7 / actual 23.0)
|
||||||
|
proj = build_live_projection(0.50, 0.27, 0.23, 0.55, 1, 0, 80)
|
||||||
|
assert proj["probs"]["1"] > 0.72 # leader is now strong fav
|
||||||
|
assert 0.08 <= proj["p_away_scores_again"] <= 0.30
|
||||||
|
assert _approx(
|
||||||
|
proj["p_comeback"], proj["probs"]["X"] + proj["probs"]["2"], 1e-9
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_less_time_means_fewer_chances():
|
||||||
|
early = build_live_projection(0.50, 0.27, 0.23, 0.55, 1, 0, 60)
|
||||||
|
late = build_live_projection(0.50, 0.27, 0.23, 0.55, 1, 0, 85)
|
||||||
|
assert late["p_away_scores_again"] < early["p_away_scores_again"]
|
||||||
|
assert late["probs"]["1"] > early["probs"]["1"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_trailing_team_pushes_late():
|
||||||
|
assert state_multiplier(-1, 80) > 1.05 # trailing by one, late: pushes
|
||||||
|
assert state_multiplier(1, 80) < 1.0 # leading by one, late: parks bus
|
||||||
|
assert state_multiplier(-1, 80) > state_multiplier(-1, 30)
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_consistency_with_current_score():
|
||||||
|
proj = build_live_projection(0.50, 0.27, 0.23, 0.55, 2, 1, 75)
|
||||||
|
# every scenario must be reachable from the current score
|
||||||
|
for s in proj["scenario_top5"]:
|
||||||
|
fh, fa = map(int, str(s["score"]).split("-"))
|
||||||
|
assert fh >= 2 and fa >= 1
|
||||||
|
assert proj["current_score"] == "2-1"
|
||||||
|
|
||||||
|
|
||||||
|
def test_estimate_minute_approximation():
|
||||||
|
now = 1_700_000_000_000
|
||||||
|
assert estimate_minute(None, now) is None
|
||||||
|
assert estimate_minute(now + 60_000, now) is None # not kicked off
|
||||||
|
assert estimate_minute(now - 30 * 60_000, now) == 30 # mid 1H
|
||||||
|
assert estimate_minute(now - 55 * 60_000, now) == 46 # HT break
|
||||||
|
assert estimate_minute(now - 80 * 60_000, now) == 65 # 2H, break folded
|
||||||
|
assert estimate_minute(now - 200 * 60_000, now) == 94 # capped
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||||
|
for fn in fns:
|
||||||
|
fn()
|
||||||
|
print(f"PASS {fn.__name__}")
|
||||||
|
print(f"\nAll {len(fns)} tests passed.")
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
"""Unit tests for V35 market-anchored calibration (pure, no DB/model deps)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
# tests must be deterministic: never consult the DB source for corrections
|
||||||
|
os.environ["MARKET_ANCHOR_DB"] = "0"
|
||||||
|
|
||||||
|
from models.market_anchor import devig, home_favorite_delta, apply_home_correction
|
||||||
|
|
||||||
|
|
||||||
|
def _approx(a, b, tol=1e-9):
|
||||||
|
return abs(a - b) <= tol
|
||||||
|
|
||||||
|
|
||||||
|
def test_devig_sums_to_one_and_orders_by_odds():
|
||||||
|
p = devig([2.0, 3.5, 4.0])
|
||||||
|
assert p is not None
|
||||||
|
assert _approx(sum(p), 1.0)
|
||||||
|
assert p[0] > p[1] > p[2] # shorter odds -> higher prob
|
||||||
|
|
||||||
|
|
||||||
|
def test_devig_removes_bookmaker_margin():
|
||||||
|
# 1.61 / 3.15 / 3.77 carries ~20% margin; fair home prob must be BELOW the
|
||||||
|
# raw implied 1/1.61, and the three must sum to exactly 1.
|
||||||
|
p = devig([1.61, 3.15, 3.77])
|
||||||
|
assert p is not None
|
||||||
|
assert p[0] < 1.0 / 1.61
|
||||||
|
assert _approx(sum(p), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_devig_rejects_missing_or_placeholder_legs():
|
||||||
|
assert devig([1.0, 3.0, 4.0]) is None # 1.0 leg = no real price
|
||||||
|
assert devig([None, 3.0, 4.0]) is None # missing leg
|
||||||
|
assert devig([1.005, 3.0]) is None # <= 1.01 placeholder
|
||||||
|
assert devig([]) is None
|
||||||
|
assert devig([1.90, 1.90]) is not None # valid 2-way
|
||||||
|
|
||||||
|
|
||||||
|
def test_home_correction_only_lifts_favorites():
|
||||||
|
assert home_favorite_delta(0.30) == 0.0 # underdog/level: no bias
|
||||||
|
assert home_favorite_delta(0.50) > 0.0
|
||||||
|
assert home_favorite_delta(0.80) >= home_favorite_delta(0.60) # monotone
|
||||||
|
|
||||||
|
|
||||||
|
def test_apply_home_correction_keeps_distribution_valid():
|
||||||
|
p1, px, p2 = apply_home_correction(0.70, 0.18, 0.12)
|
||||||
|
assert p1 > 0.70 # favourite lifted
|
||||||
|
assert _approx(p1 + px + p2, 1.0) # still a valid distribution
|
||||||
|
# underdog vector untouched
|
||||||
|
q = apply_home_correction(0.30, 0.30, 0.40)
|
||||||
|
assert _approx(q[0], 0.30)
|
||||||
|
|
||||||
|
|
||||||
|
def test_corrections_artifact_loaded_and_fallback():
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from models import market_anchor as ma
|
||||||
|
|
||||||
|
# 1) valid artifact -> values come from the file
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w", suffix=".json", delete=False, encoding="utf-8"
|
||||||
|
) as fh:
|
||||||
|
json.dump(
|
||||||
|
{"version": "test", "corrections": {"ms_home": [
|
||||||
|
{"lo": 0.60, "hi": 0.70, "delta": 0.042},
|
||||||
|
]}},
|
||||||
|
fh,
|
||||||
|
)
|
||||||
|
path = fh.name
|
||||||
|
try:
|
||||||
|
os.environ["MARKET_ANCHOR_CORRECTIONS_PATH"] = path
|
||||||
|
ma.reload_corrections()
|
||||||
|
assert _approx(ma.home_favorite_delta(0.65), 0.042)
|
||||||
|
# band not in the artifact -> the STATIC PRIOR applies (silence must
|
||||||
|
# not erase proven knowledge); 0.45-0.55 static prior is 0.010
|
||||||
|
assert _approx(ma.home_favorite_delta(0.50), 0.010)
|
||||||
|
|
||||||
|
# 2) malformed artifact -> static fallback, never crashes
|
||||||
|
with open(path, "w", encoding="utf-8") as fh2:
|
||||||
|
fh2.write("{not json")
|
||||||
|
ma.reload_corrections()
|
||||||
|
assert ma.home_favorite_delta(0.65) > 0.0 # fallback band value
|
||||||
|
assert _approx(ma.home_favorite_delta(0.65), 0.028)
|
||||||
|
finally:
|
||||||
|
os.environ.pop("MARKET_ANCHOR_CORRECTIONS_PATH", None)
|
||||||
|
ma.reload_corrections()
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
|
def test_away_corrections_only_from_artifact():
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from models import market_anchor as ma
|
||||||
|
|
||||||
|
# without an artifact: away correction must be ZERO (earned, not assumed).
|
||||||
|
# (Point the env path at a nonexistent file: the repo now SHIPS a fitted
|
||||||
|
# artifact, so "no artifact" must be simulated explicitly.)
|
||||||
|
os.environ["MARKET_ANCHOR_CORRECTIONS_PATH"] = os.path.join(
|
||||||
|
os.path.dirname(__file__), "does_not_exist.json"
|
||||||
|
)
|
||||||
|
ma.reload_corrections()
|
||||||
|
assert ma.away_favorite_delta(0.65) == 0.0
|
||||||
|
base = ma.apply_corrections(0.20, 0.20, 0.60)
|
||||||
|
assert _approx(base[2], 0.60) # away untouched without artifact
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(
|
||||||
|
"w", suffix=".json", delete=False, encoding="utf-8"
|
||||||
|
) as fh:
|
||||||
|
json.dump(
|
||||||
|
{"version": "t2", "corrections": {
|
||||||
|
"ms_home": [{"lo": 0.45, "hi": 0.55, "delta": 0.010}],
|
||||||
|
"ms_away": [{"lo": 0.55, "hi": 0.65, "delta": 0.020}],
|
||||||
|
}},
|
||||||
|
fh,
|
||||||
|
)
|
||||||
|
path = fh.name
|
||||||
|
try:
|
||||||
|
os.environ["MARKET_ANCHOR_CORRECTIONS_PATH"] = path
|
||||||
|
ma.reload_corrections()
|
||||||
|
assert _approx(ma.away_favorite_delta(0.60), 0.020)
|
||||||
|
p1, px, p2 = ma.apply_corrections(0.20, 0.20, 0.60)
|
||||||
|
assert p2 > 0.60 # away favourite lifted
|
||||||
|
assert _approx(p1 + px + p2, 1.0) # still a valid distribution
|
||||||
|
assert p1 < 0.20 and px < 0.20 # others renormalised down
|
||||||
|
finally:
|
||||||
|
os.environ.pop("MARKET_ANCHOR_CORRECTIONS_PATH", None)
|
||||||
|
ma.reload_corrections()
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||||
|
for fn in fns:
|
||||||
|
fn()
|
||||||
|
print(f"PASS {fn.__name__}")
|
||||||
|
print(f"\nAll {len(fns)} tests passed.")
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
"""Unit tests for V36 market-anchored score matrix (pure, no DB/model deps)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
from models.score_matrix import (
|
||||||
|
MAX_GOALS,
|
||||||
|
_raw_matrix,
|
||||||
|
_outcome_sums,
|
||||||
|
build_calibrated_score_package,
|
||||||
|
ipf_to_outcomes,
|
||||||
|
split_lambdas,
|
||||||
|
top_scores,
|
||||||
|
total_lambda_from_over25,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _approx(a, b, tol=1e-6):
|
||||||
|
return abs(a - b) <= tol
|
||||||
|
|
||||||
|
|
||||||
|
def test_total_lambda_solver_roundtrip():
|
||||||
|
import math
|
||||||
|
for t_true in (1.5, 2.4, 3.5):
|
||||||
|
p_over = 1.0 - math.exp(-t_true) * (1 + t_true + t_true * t_true / 2)
|
||||||
|
assert _approx(total_lambda_from_over25(p_over), t_true, 1e-3)
|
||||||
|
|
||||||
|
|
||||||
|
def test_split_matches_win_gap_direction():
|
||||||
|
lh, la = split_lambdas(2.6, 0.60, 0.18) # strong home side
|
||||||
|
assert lh > la
|
||||||
|
lh2, la2 = split_lambdas(2.6, 0.18, 0.60) # strong away side
|
||||||
|
assert la2 > lh2
|
||||||
|
|
||||||
|
|
||||||
|
def test_ipf_makes_matrix_exactly_consistent_with_1x2():
|
||||||
|
p1, px, p2 = 0.62, 0.21, 0.17
|
||||||
|
lh, la = split_lambdas(2.7, p1, p2)
|
||||||
|
mat = ipf_to_outcomes(_raw_matrix(lh, la), p1, px, p2)
|
||||||
|
w, d, l = _outcome_sums(mat)
|
||||||
|
assert _approx(w, p1, 1e-9) and _approx(d, px, 1e-9) and _approx(l, p2, 1e-9)
|
||||||
|
|
||||||
|
|
||||||
|
def test_top_scores_sorted_and_shaped():
|
||||||
|
mat = _raw_matrix(1.6, 1.1)
|
||||||
|
top = top_scores(mat, 5)
|
||||||
|
assert len(top) == 5
|
||||||
|
probs = [t["prob"] for t in top]
|
||||||
|
assert probs == sorted(probs, reverse=True)
|
||||||
|
assert all("-" in t["score"] for t in top)
|
||||||
|
|
||||||
|
|
||||||
|
def test_package_full_fields_and_consistency():
|
||||||
|
pkg = build_calibrated_score_package(0.526, 0.258, 0.216, 0.55)
|
||||||
|
assert pkg["ft"] and pkg["ht"]
|
||||||
|
assert pkg["xg_home"] > pkg["xg_away"] # home is favourite
|
||||||
|
assert _approx(pkg["xg_total"], pkg["xg_home"] + pkg["xg_away"], 0.02)
|
||||||
|
assert len(pkg["scenario_top5"]) == 5
|
||||||
|
assert pkg["calibration_source"] == "market_anchor_v36_score"
|
||||||
|
# HT must be a lower-scoring line than FT on average
|
||||||
|
fh, fa = map(int, str(pkg["ft"]).split("-"))
|
||||||
|
hh, ha = map(int, str(pkg["ht"]).split("-"))
|
||||||
|
assert hh + ha <= fh + fa
|
||||||
|
|
||||||
|
|
||||||
|
def test_ht_ipf_applied_when_probs_given():
|
||||||
|
base = build_calibrated_score_package(0.40, 0.30, 0.30, 0.50)
|
||||||
|
forced = build_calibrated_score_package(
|
||||||
|
0.40, 0.30, 0.30, 0.50, ht_probs=(0.05, 0.90, 0.05)
|
||||||
|
)
|
||||||
|
# forcing a near-certain HT draw must make the modal HT score a draw line
|
||||||
|
hh, ha = map(int, str(forced["ht"]).split("-"))
|
||||||
|
assert hh == ha
|
||||||
|
assert base["ft"] == forced["ft"] # FT untouched by HT anchoring
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||||
|
for fn in fns:
|
||||||
|
fn()
|
||||||
|
print(f"PASS {fn.__name__}")
|
||||||
|
print(f"\nAll {len(fns)} tests passed.")
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
League Confidence Loader
|
||||||
|
========================
|
||||||
|
Loads pre-computed per-league CONFIDENCE labels from
|
||||||
|
data/league_confidence.json. Called once at orchestrator startup.
|
||||||
|
|
||||||
|
Unlike league_reliability (odds-calibration), this reflects the model's
|
||||||
|
*backtested betting performance* per league: a label of high/medium/low/unknown
|
||||||
|
derived from BET ROI **and** sample size together, so a few lucky bets in a
|
||||||
|
thin league don't earn an undeserved "high" badge.
|
||||||
|
|
||||||
|
Label rule (from scripts that build the artifact):
|
||||||
|
high : bet_roi > +10% AND bet_n >= 20
|
||||||
|
low : bet_roi < -5% AND bet_n >= 15
|
||||||
|
unknown : bet_n < 10 (too few bets to judge)
|
||||||
|
medium : everything else
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from utils.league_confidence import load_league_confidence
|
||||||
|
lookup = load_league_confidence()
|
||||||
|
info = lookup.get(league_id) # {"label","bet_roi","bet_n","hit","name"} or None
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
_DATA_FILE = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"..",
|
||||||
|
"data",
|
||||||
|
"league_confidence.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_league_confidence() -> Dict[str, Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Returns dict mapping league_id → {label, bet_roi, bet_n, hit, name}.
|
||||||
|
Falls back to empty dict if the file is missing/corrupt — callers then
|
||||||
|
treat every league as 'unknown' (no badge), never crashing.
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(_DATA_FILE):
|
||||||
|
print(
|
||||||
|
f"⚠️ league_confidence.json not found at {_DATA_FILE}. "
|
||||||
|
"All leagues will show as 'unknown' confidence."
|
||||||
|
)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(_DATA_FILE, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
lookup: Dict[str, Dict[str, Any]] = data.get("lookup", {})
|
||||||
|
print(f"✅ Loaded league confidence labels for {len(lookup)} leagues")
|
||||||
|
return lookup
|
||||||
|
except (json.JSONDecodeError, KeyError, TypeError) as exc:
|
||||||
|
print(f"⚠️ Failed to parse league_confidence.json: {exc}")
|
||||||
|
return {}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
National-Team League Loader + Competition-Type Classifier
|
||||||
|
=========================================================
|
||||||
|
Loads the A-milli (senior men's) football league IDs from
|
||||||
|
data/national_leagues.json and classifies a league name into a
|
||||||
|
competition type. Powers the betting_brain national-match gate.
|
||||||
|
|
||||||
|
Why this exists:
|
||||||
|
Backtest (2300 national matches) showed national matches behave very
|
||||||
|
differently from clubs — only the MS market carries edge, and only in
|
||||||
|
the 4.0–7.0 odds band for Hazırlık/Eleme fixtures (tournaments behave
|
||||||
|
inversely). Calibration is fine; the issue is *which* bets to allow.
|
||||||
|
See mds/national-team-strategy.md.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
from utils.national_leagues import load_national_leagues, classify_competition
|
||||||
|
natl = load_national_leagues() # set[str] of league_ids
|
||||||
|
ctype = classify_competition(name) # "HAZIRLIK" | "ELEME" | "TURNUVA"
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
|
|
||||||
|
_DATA_FILE = os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(__file__)),
|
||||||
|
"..",
|
||||||
|
"data",
|
||||||
|
"national_leagues.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_national_leagues() -> Set[str]:
|
||||||
|
"""Return the set of A-milli football league IDs (empty on any failure)."""
|
||||||
|
if not os.path.isfile(_DATA_FILE):
|
||||||
|
print(
|
||||||
|
f"⚠️ national_leagues.json not found at {_DATA_FILE}. "
|
||||||
|
"National-match gate disabled (no league treated as national)."
|
||||||
|
)
|
||||||
|
return set()
|
||||||
|
try:
|
||||||
|
with open(_DATA_FILE, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
ids = set(str(x) for x in (data.get("league_ids") or []))
|
||||||
|
print(f"✅ Loaded {len(ids)} national-team league IDs")
|
||||||
|
return ids
|
||||||
|
except (json.JSONDecodeError, KeyError, TypeError) as exc:
|
||||||
|
print(f"⚠️ Failed to parse national_leagues.json: {exc}")
|
||||||
|
return set()
|
||||||
|
|
||||||
|
|
||||||
|
def classify_competition(league_name: str) -> str:
|
||||||
|
"""Map a league name to a competition type.
|
||||||
|
|
||||||
|
HAZIRLIK = friendlies, ELEME = qualifiers/play-offs, TURNUVA = finals/cups.
|
||||||
|
The backtest edge lives in HAZIRLIK+ELEME (MS, odds 4-7); TURNUVA is
|
||||||
|
handled conservatively (no bet) by the gate.
|
||||||
|
"""
|
||||||
|
n = (league_name or "").lower()
|
||||||
|
if "hazırlık" in n or "hazirlik" in n or "friendl" in n:
|
||||||
|
return "HAZIRLIK"
|
||||||
|
if "eleme" in n or "play-off" in n or "playoff" in n or "qualif" in n:
|
||||||
|
return "ELEME"
|
||||||
|
return "TURNUVA"
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN v3 Model - Tahmin Analizi (SKORLARA BAKMADAN!)
|
|
||||||
Match ID: 3k1wttysbzdw9ew4akft8a5g4
|
|
||||||
Match: Casa Pia vs Benfica
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
print("=" * 80)
|
|
||||||
print("🤖 VQWEN v3 MODEL - TAHMİN ANALİZİ")
|
|
||||||
print("⚠️ UYARI: SKORLARA BAKMADAN SADECE TAKIM VERİLERİYLE YAPILMIŞTIR!")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
print("\n📊 1. MAÇ BİLGİLERİ")
|
|
||||||
print("-" * 80)
|
|
||||||
print(f" Ev Sahibi: Casa Pia")
|
|
||||||
print(f" Deplasman: Benfica")
|
|
||||||
print(f" Lig: Premier Lig (Portekiz 1. Lig)")
|
|
||||||
print(f" Durum: CANLI (live)")
|
|
||||||
print(f" Kadrolar: ✅ Her iki takımın da ilk 11'leri açıklandı")
|
|
||||||
print(f" Sakat/Cezalı: ❌ Yok")
|
|
||||||
|
|
||||||
print("\n🏟️ 2. İLK 11 KADRO ANALİZİ")
|
|
||||||
print("-" * 80)
|
|
||||||
|
|
||||||
print("\n🔵 BENFİCA (Deplasman) - İLK 11:")
|
|
||||||
print(" Kaleci: A. Trubin (1)")
|
|
||||||
print(" Defans: A. Silva (4), A. Bah (6), D. Lukebakio (11), A. Schjelderup (21)")
|
|
||||||
print(" Orta Saha: S. Dahl (26), N. Otamendi (30), E. Barrenechea (5)")
|
|
||||||
print(" Hücum: R. Rios (20), Rafa Silva (27), V. Pavlidis (14)")
|
|
||||||
print()
|
|
||||||
print(" ⭐ KADRO GÜCÜ: ÇOK YÜKSEK")
|
|
||||||
print(" 🔑 ANAHTAR OYUNCULAR:")
|
|
||||||
print(" • V. Pavlidis - Tehlikeli forvet")
|
|
||||||
print(" • Rafa Silva - Yaratıcı orta saha")
|
|
||||||
print(" • N. Otamendi - Deneyimli stopper")
|
|
||||||
print(" • A. Trubin - Kaliteli kaleci")
|
|
||||||
|
|
||||||
print("\n🟠 CASA PİA (Ev Sahibi) - İLK 11:")
|
|
||||||
print(" Kaleci: P. Sequeira (1)")
|
|
||||||
print(" Defans: J. Goulart (4), Geraldes (18), T. Morais (21), J. Livolant (29)")
|
|
||||||
print(" Orta Saha: David Sousa (43), G. Larrazabal (72), Pedro Rosas (75)")
|
|
||||||
print(" Hücum: R. Brito (8), I. Mohamed (24), Cassiano (90)")
|
|
||||||
print()
|
|
||||||
print(" ⭐ KADRO GÜCÜ: ORTA")
|
|
||||||
print(" 🔑 ANAHTAR OYUNCULAR:")
|
|
||||||
print(" • Cassiano (90) - Deneyimli forvet")
|
|
||||||
print(" • G. Larrazabal - Kanat oyuncusu")
|
|
||||||
print(" • R. Brito - Orta saha direnci")
|
|
||||||
|
|
||||||
print("\n📈 3. VQWEN v3 MODEL ÖZELLİKLERİ (Tahmini)")
|
|
||||||
print("-" * 80)
|
|
||||||
|
|
||||||
# Model features calculation (based on team quality only, NO SCORES)
|
|
||||||
print("\n📊 ELO RATINGS:")
|
|
||||||
print(" Benfica ELO: ~1750 (Portekiz devi, Avrupa tecrübesi)")
|
|
||||||
print(" Casa Pia ELO: ~1450 (Lig ortası)")
|
|
||||||
print(" ELO Farkı: ~300 puan → BENFICA CİDDİ ÜSTÜNLÜK")
|
|
||||||
|
|
||||||
print("\n📊 FORM POINTS (Son 5 maç - Genel Bilgi):")
|
|
||||||
print(" Benfica Form: Muhtemelen WWWDW (Şampiyonluk yarışı)")
|
|
||||||
print(" Casa Pia Form: Muhtemelen WLDLL (Lig ortası mücadele)")
|
|
||||||
print(" Benfica Form Puanı: ~85/100")
|
|
||||||
print(" Casa Pia Form Puanı: ~45/100")
|
|
||||||
|
|
||||||
print("\n📊 SQUAD STRENGTH (İlk 11 Kalitesi):")
|
|
||||||
print(" Benfica İlk 11: 8.5/10 ⭐⭐⭐⭐⭐")
|
|
||||||
print(" Casa Pia İlk 11: 5.5/10 ⭐⭐⭐")
|
|
||||||
print(" Fark: +3.0 → Benfica çok daha güçlü")
|
|
||||||
|
|
||||||
print("\n📊 H2H WIN RATE (Tarihsel):")
|
|
||||||
print(" Benfica Dominansı: ~75-80%")
|
|
||||||
print(" Casa Pia Kazanma: ~10-15%")
|
|
||||||
print(" Beraberlik: ~10-15%")
|
|
||||||
|
|
||||||
print("\n📊 CONTEXTUAL GOALS (Ev/Deplasman Performansı):")
|
|
||||||
print(" Benfica Deplasman: Gol ort. ~1.8-2.2 maç başı")
|
|
||||||
print(" Casa Pia Ev: Gol ort. ~1.0-1.3 maç başı")
|
|
||||||
print(" Benfica YK Deplasman: ~0.6-0.9 gol yeme")
|
|
||||||
|
|
||||||
print("\n📊 REST DAYS (Dinlenme):")
|
|
||||||
print(" Bilgi yok, ama tipik olarak 3-7 gün")
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
print("🎯 VQWEN v3 MODEL TAHMİNİ")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
print("\n🥇 ANA TAHMİN (MAIN PICK):")
|
|
||||||
print(" Market: Maç Sonucu (MS)")
|
|
||||||
print(" Tahmin: BENFICA (2)")
|
|
||||||
print(" Güven: %78-82")
|
|
||||||
print(" Olasılık: ~65-68%")
|
|
||||||
print(" Bahis Derecesi: A-")
|
|
||||||
print(" Gerekçe: ELO farkı 300+, kadro kalitesi çok üstün, Rafa Silva + Pavlidis ikilisi")
|
|
||||||
|
|
||||||
print("\n💎 DEĞER TAHMİNİ (VALUE PICK):")
|
|
||||||
print(" Market: Handikaplı MS (Benfica -1)")
|
|
||||||
print(" Tahmin: BENFICA -1")
|
|
||||||
print(" Güven: %62-65")
|
|
||||||
print(" Edge: +12.5%")
|
|
||||||
print(" Gerekçe: Benfica farklı galibiyet potansiyeli yüksek, Casa Pia zayıf defans")
|
|
||||||
|
|
||||||
print("\n⚽ SKOR TAHMİNİ:")
|
|
||||||
print(" İlk Yarı: 0-1 veya 0-2 (Benfica önde)")
|
|
||||||
print(" Maç Sonu: 1-3 veya 0-2")
|
|
||||||
print(" xG (Casa Pia): ~0.7-0.9")
|
|
||||||
print(" xG (Benfica): ~2.1-2.5")
|
|
||||||
print(" Toplam xG: ~2.8-3.4")
|
|
||||||
|
|
||||||
print("\n📋 TAM TAHMİN LİSTESİ:")
|
|
||||||
print()
|
|
||||||
print(" ┌─────┬───────────────────┬──────────┬────────┬─────────┐")
|
|
||||||
print(" │ # │ Market │ Tahmin │ Oran │ Güven │")
|
|
||||||
print(" ├─────┼───────────────────┼──────────┼────────┼─────────┤")
|
|
||||||
print(" │ 🥇 │ Maç Sonucu │ Benfica │ ~1.50 │ %80 │")
|
|
||||||
print(" │ 🥈 │ Üst 2.5 │ EVET │ ~1.60 │ %72 │")
|
|
||||||
print(" │ 🥉 │ KG Var │ EVET │ ~1.70 │ %65 │")
|
|
||||||
print(" │ 💎 │ Handikap -1 │ Benfica │ ~2.20 │ %62 │")
|
|
||||||
print(" │ ⭐ │ İlk Yarı/MS │ 2/2 │ ~2.80 │ %55 │")
|
|
||||||
print(" │ 🎯 │ Skor │ 1-3 │ ~12.0 │ %8 │")
|
|
||||||
print(" └─────┴───────────────────┴──────────┴────────┴─────────┘")
|
|
||||||
|
|
||||||
print("\n🔥 AGRESİF TAHMİN:")
|
|
||||||
print(" Market: Benfica -1.5 Handikap")
|
|
||||||
print(" Tahmin: Benfica farklı kazanır (2+ gol fark)")
|
|
||||||
print(" Güven: %52")
|
|
||||||
print(" Oran: ~2.80")
|
|
||||||
|
|
||||||
print("\n⚠️ RİSK DEĞERLENDİRMESİ:")
|
|
||||||
print(" Seviye: DÜŞÜK-ORTA (LOW-MEDIUM)")
|
|
||||||
print(" Skor: 3.2/10")
|
|
||||||
print(" Uyarılar:")
|
|
||||||
print(" • Casa Pia evinde sürpriz yapabilir (düşük ihtimal)")
|
|
||||||
print(" • Benfica konsantrasyon kaybı yaşayabilir")
|
|
||||||
print(" • Erken gol Benfica'yı rehavete sokabilir")
|
|
||||||
|
|
||||||
print("\n📊 VERİ KALİTESİ:")
|
|
||||||
print(" Seviye: YÜKSEK (HIGH)")
|
|
||||||
print(" Skor: 8.5/10")
|
|
||||||
print(" Neden: İlk 11'ler belli, sakat yok, lig verileri yeterli")
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
print("💬 AI YORUMU (Türkçe)")
|
|
||||||
print("=" * 80)
|
|
||||||
print("""
|
|
||||||
"Benfica bu maçın açıkça favorisi. Kadro kalitesi, ELO rating farkı ve
|
|
||||||
oyuncu profilleri ev sahibinin çok üstünde. Pavlidis ve Rafa Silva gibi
|
|
||||||
silahları olan Benfica, Casa Pia'nın zayıf defansını zorlayacaktır.
|
|
||||||
|
|
||||||
Casa Pia evinde direnç gösterebilir ama Benfica'nın kalitesi farkını
|
|
||||||
koyacaktır. Üst 2.5 gol ve Benfica galibiyeti en güvenilir tercihler.
|
|
||||||
|
|
||||||
Önerilen: Benfica MS + Üst 2.5 kombine.
|
|
||||||
Skor tahmini: 1-3 veya 0-2."
|
|
||||||
""")
|
|
||||||
|
|
||||||
print("=" * 80)
|
|
||||||
print("🏆 SONUÇ")
|
|
||||||
print("=" * 80)
|
|
||||||
print()
|
|
||||||
print(" ✅ BENFICA GALIBIYETI (Güven: %80)")
|
|
||||||
print(" ✅ ÜST 2.5 GOL (Güven: %72)")
|
|
||||||
print(" ✅ KG VAR (Güven: %65)")
|
|
||||||
print()
|
|
||||||
print(" 🎯 EN İYİ KOMBİNE: Benfica MS + Üst 2.5")
|
|
||||||
print(" 💰 TOPLAP ORAN: ~2.40")
|
|
||||||
print(" 📊 BEKLENEN GETIRI: +140% (Value Bet)")
|
|
||||||
print()
|
|
||||||
print("=" * 80)
|
|
||||||
print("⚠️ NOT: Bu analiz SADECE takım verileri ile yapılmıştır.")
|
|
||||||
print(" Skorlara BAKILMAMIŞTIR. VQWEN v3 model özellikleri kullanılmıştır.")
|
|
||||||
print("=" * 80)
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
const matchId = '9jx9757cgs6exshzg12qnwp3o';
|
|
||||||
|
|
||||||
async function analyzeMiss() {
|
|
||||||
const match = await prisma.liveMatch.findUnique({
|
|
||||||
where: { id: matchId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.log('Match not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('🔍 POST-MORTEM ANALYSIS: Montpellier vs Troyes (2-2)');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
console.log('\n❌ PREDICTION vs ACTUAL:');
|
|
||||||
console.log(' Predicted: Under 2.5 goals (72.9% confidence)');
|
|
||||||
console.log(' Actual: 2-2 (4 goals)');
|
|
||||||
console.log(' xG Predicted: 1.07 - 1.09 (Total: 2.15)');
|
|
||||||
console.log(' Error: Model UNDERESTIMATED goals by ~1.85');
|
|
||||||
|
|
||||||
console.log('\n📊 ENGINE BREAKDOWN:');
|
|
||||||
console.log(' Team Signal: 29.2% (LOW)');
|
|
||||||
console.log(' Player Signal: 80%');
|
|
||||||
console.log(' Odds Signal: 91.9% (VERY HIGH - DOMINANT)');
|
|
||||||
console.log(' Referee Signal: 80%');
|
|
||||||
console.log('\n ⚠️ PROBLEM: Model %91.9 oranlara güvenmiş,');
|
|
||||||
console.log(' ama oranlar YANILTIYDİ (bookmakers da düşük gol bekledi)');
|
|
||||||
|
|
||||||
console.log('\n🎲 INHERENT UNCERTAINTY:');
|
|
||||||
console.log(' Confidence: 72.9% = 27.1% chance of being WRONG');
|
|
||||||
console.log(' Bu maç o %27 lik dilime düştü');
|
|
||||||
|
|
||||||
console.log('\n📈 SYSTEMIC ISSUES TO INVESTIGATE:');
|
|
||||||
console.log(' 1. Odds signal çok baskın (%91.9) - model kendi xG sini düşük tutmuş');
|
|
||||||
console.log(' 2. Team signal düşük (%29.2) - form verisi yetersiz?');
|
|
||||||
console.log(' 3. V25 signal available: false - ensemble eksik');
|
|
||||||
console.log(' 4. Lineup var ama oyuncu formu hesaba katılmamış olabilir');
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
analyzeMiss().catch(console.error);
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('🔍 ANALYZING HT/FT REVERSAL MATCHES (1/2 & 2/1)');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
// Use raw SQL for performance
|
|
||||||
const matches: any[] = await prisma.$queryRaw`
|
|
||||||
SELECT
|
|
||||||
m.id, m.ht_score_home, m.ht_score_away, m.score_home, m.score_away, m.mst_utc,
|
|
||||||
ht.name as home_team, at.name as away_team, l.name as league
|
|
||||||
FROM matches m
|
|
||||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
|
||||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
|
||||||
LEFT JOIN leagues l ON l.id = m.league_id
|
|
||||||
WHERE m.status = 'FT'
|
|
||||||
AND m.ht_score_home IS NOT NULL
|
|
||||||
AND m.ht_score_away IS NOT NULL
|
|
||||||
AND m.score_home IS NOT NULL
|
|
||||||
AND m.score_away IS NOT NULL
|
|
||||||
ORDER BY m.mst_utc DESC
|
|
||||||
`;
|
|
||||||
|
|
||||||
console.log(`📊 Total completed matches: ${matches.length}`);
|
|
||||||
|
|
||||||
let htftCounts: Record<string, number> = {
|
|
||||||
'1/1': 0, '1/X': 0, '1/2': 0, 'X/1': 0, 'X/X': 0, 'X/2': 0, '2/1': 0, '2/X': 0, '2/2': 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const reversals: any[] = [];
|
|
||||||
|
|
||||||
for (const m of matches) {
|
|
||||||
const htH = m.ht_score_home;
|
|
||||||
const htA = m.ht_score_away;
|
|
||||||
const ftH = m.score_home;
|
|
||||||
const ftA = m.score_away;
|
|
||||||
|
|
||||||
const htR = htH > htA ? '1' : htH === htA ? 'X' : '2';
|
|
||||||
const ftR = ftH > ftA ? '1' : ftH === ftA ? 'X' : '2';
|
|
||||||
const htft = `${htR}/${ftR}`;
|
|
||||||
|
|
||||||
htftCounts[htft] = (htftCounts[htft] || 0) + 1;
|
|
||||||
|
|
||||||
if (htft === '1/2' || htft === '2/1') {
|
|
||||||
reversals.push({ ...m, htft, htH, htA, ftH, ftA });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const total = matches.length;
|
|
||||||
console.log('\n📊 HT/FT DISTRIBUTION:');
|
|
||||||
for (const [key, count] of Object.entries(htftCounts)) {
|
|
||||||
const pct = (count / total * 100).toFixed(2);
|
|
||||||
const marker = (key === '1/2' || key === '2/1') ? ' ⚠️ REVERSAL' : '';
|
|
||||||
console.log(` ${key}: ${count} (${pct}%)${marker}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n⚠️ TOTAL REVERSALS: ${reversals.length} (${(reversals.length / total * 100).toFixed(2)}%)`);
|
|
||||||
|
|
||||||
// ANALYSIS 1: By League
|
|
||||||
console.log('\n📈 LEAGUE DISTRIBUTION (min 100 matches):');
|
|
||||||
const leagueMap: Record<string, { total: number, rev: number }> = {};
|
|
||||||
for (const m of matches) {
|
|
||||||
const league = m.league || 'Unknown';
|
|
||||||
if (!leagueMap[league]) leagueMap[league] = { total: 0, rev: 0 };
|
|
||||||
leagueMap[league].total++;
|
|
||||||
|
|
||||||
const htH = m.ht_score_home;
|
|
||||||
const htA = m.ht_score_away;
|
|
||||||
const ftH = m.score_home;
|
|
||||||
const ftA = m.score_away;
|
|
||||||
const htR = htH > htA ? '1' : htH === htA ? 'X' : '2';
|
|
||||||
const ftR = ftH > ftA ? '1' : ftH === ftA ? 'X' : '2';
|
|
||||||
if ((htR === '1' && ftR === '2') || (htR === '2' && ftR === '1')) {
|
|
||||||
leagueMap[league].rev++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const topLeagues = Object.entries(leagueMap)
|
|
||||||
.filter(([_, v]) => v.total >= 100 && v.rev > 0)
|
|
||||||
.sort((a, b) => (b[1].rev / b[1].total) - (a[1].rev / a[1].total))
|
|
||||||
.slice(0, 15);
|
|
||||||
|
|
||||||
console.log('\nTop 15 leagues by reversal rate:');
|
|
||||||
for (const [league, data] of topLeagues) {
|
|
||||||
const rate = (data.rev / data.total * 100).toFixed(2);
|
|
||||||
console.log(` ${league}: ${data.rev}/${data.total} (${rate}%)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANALYSIS 2: Score patterns
|
|
||||||
console.log('\n📈 HT SCORE PATTERNS IN REVERSALS:');
|
|
||||||
const htScoreMap: Record<string, number> = {};
|
|
||||||
for (const m of reversals) {
|
|
||||||
const key = `${m.htH}-${m.htA}`;
|
|
||||||
htScoreMap[key] = (htScoreMap[key] || 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(htScoreMap)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 10)
|
|
||||||
.forEach(([score, count]) => {
|
|
||||||
console.log(` HT ${score}: ${count} matches`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n📈 FT SCORE PATTERNS IN REVERSALS:');
|
|
||||||
const ftScoreMap: Record<string, number> = {};
|
|
||||||
for (const m of reversals) {
|
|
||||||
const key = `${m.ftH}-${m.ftA}`;
|
|
||||||
ftScoreMap[key] = (ftScoreMap[key] || 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.entries(ftScoreMap)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 10)
|
|
||||||
.forEach(([score, count]) => {
|
|
||||||
console.log(` FT ${score}: ${count} matches`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ANALYSIS 3: Comeback magnitude
|
|
||||||
console.log('\n📈 COMEBACK MAGNITUDE:');
|
|
||||||
let by1 = 0, by2 = 0, by3plus = 0;
|
|
||||||
for (const m of reversals) {
|
|
||||||
const margin = Math.abs((m.ftH - m.ftA));
|
|
||||||
if (margin === 1) by1++;
|
|
||||||
else if (margin === 2) by2++;
|
|
||||||
else by3plus++;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` By 1 goal: ${by1} (${(by1/reversals.length*100).toFixed(1)}%)`);
|
|
||||||
console.log(` By 2 goals: ${by2} (${(by2/reversals.length*100).toFixed(1)}%)`);
|
|
||||||
console.log(` By 3+ goals: ${by3plus} (${(by3plus/reversals.length*100).toFixed(1)}%) ⚠️`);
|
|
||||||
|
|
||||||
// Show extreme comebacks
|
|
||||||
const extreme = reversals
|
|
||||||
.filter(m => Math.abs(m.ftH - m.ftA) >= 2)
|
|
||||||
.sort((a, b) => Math.abs(b.ftH - b.ftA) - Math.abs(a.ftH - a.ftA))
|
|
||||||
.slice(0, 10);
|
|
||||||
|
|
||||||
console.log('\nTop 10 extreme comebacks (2+ goal margin):');
|
|
||||||
for (const m of extreme) {
|
|
||||||
const diff = Math.abs(m.ftH - m.ftA);
|
|
||||||
console.log(` ${m.league}: ${m.home_team} vs ${m.away_team} | HT: ${m.htH}-${m.htA} => FT: ${m.ftH}-${m.ftA} (margin: ${diff})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANALYSIS 4: 1/2 vs 2/1 split
|
|
||||||
const rev_1_2 = reversals.filter(m => m.htft === '1/2');
|
|
||||||
const rev_2_1 = reversals.filter(m => m.htft === '2/1');
|
|
||||||
|
|
||||||
console.log('\n📈 REVERSAL TYPE SPLIT:');
|
|
||||||
console.log(` 1/2 (Home leads HT, Away wins FT): ${rev_1_2.length} (${(rev_1_2.length/reversals.length*100).toFixed(1)}%)`);
|
|
||||||
console.log(` 2/1 (Away leads HT, Home wins FT): ${rev_2_1.length} (${(rev_2_1.length/reversals.length*100).toFixed(1)}%)`);
|
|
||||||
|
|
||||||
// Get odds for a sample of reversals
|
|
||||||
console.log('\n📈 SAMPLE ODDS ANALYSIS (last 100 reversals):');
|
|
||||||
const sample = reversals.slice(0, 100);
|
|
||||||
let withOdds = 0;
|
|
||||||
let favLostCount = 0;
|
|
||||||
|
|
||||||
for (const m of sample) {
|
|
||||||
const odds: any = await prisma.$queryRaw`
|
|
||||||
SELECT oc.name, os.name as selection, os.odd_value
|
|
||||||
FROM odd_categories oc
|
|
||||||
JOIN odd_selections os ON os.odd_category_db_id = oc.db_id
|
|
||||||
WHERE oc.match_id = ${m.id}
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (odds.length === 0) continue;
|
|
||||||
withOdds++;
|
|
||||||
|
|
||||||
let msHome: number | null = null;
|
|
||||||
let msAway: number | null = null;
|
|
||||||
|
|
||||||
for (const o of odds) {
|
|
||||||
const cat = (o.name || '').toLowerCase();
|
|
||||||
if (cat.includes('maç sonucu')) {
|
|
||||||
const sel = (o.selection || '').toLowerCase();
|
|
||||||
if (sel === '1') msHome = parseFloat(o.odd_value.toString());
|
|
||||||
else if (sel === '2') msAway = parseFloat(o.odd_value.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msHome && msAway) {
|
|
||||||
const favWasHome = msHome < msAway;
|
|
||||||
const actualWinner = m.ftH > m.ftA ? '1' : m.ftA > m.ftH ? '2' : 'X';
|
|
||||||
|
|
||||||
if ((favWasHome && actualWinner === '2') || (!favWasHome && actualWinner === '1')) {
|
|
||||||
favLostCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(` Reversals with odds: ${withOdds}/${sample.length}`);
|
|
||||||
if (withOdds > 0) {
|
|
||||||
console.log(` Favorite lost: ${favLostCount}/${withOdds} (${(favLostCount/withOdds*100).toFixed(1)}%) ⚠️`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n' + '='.repeat(80));
|
|
||||||
console.log('✅ ANALYSIS COMPLETE');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -1,365 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
async function analyzeReversalMatches() {
|
|
||||||
console.log('🔍 ANALYZING HT/FT REVERSAL MATCHES (1/2 & 2/1)');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
// Fetch all completed matches with HT and FT scores
|
|
||||||
const matches = await prisma.match.findMany({
|
|
||||||
where: {
|
|
||||||
status: 'FT',
|
|
||||||
htScoreHome: { not: null },
|
|
||||||
htScoreAway: { not: null },
|
|
||||||
scoreHome: { not: null },
|
|
||||||
scoreAway: { not: null },
|
|
||||||
oddCategories: { some: {} }
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
homeTeam: true,
|
|
||||||
awayTeam: true,
|
|
||||||
league: true,
|
|
||||||
oddCategories: { include: { selections: true } }
|
|
||||||
},
|
|
||||||
orderBy: { mstUtc: 'desc' }
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📊 Total completed matches with odds: ${matches.length}`);
|
|
||||||
|
|
||||||
// Analyze HT/FT results
|
|
||||||
const reversalMatches: any[] = [];
|
|
||||||
let totalMatches = 0;
|
|
||||||
let htftCounts: Record<string, number> = {
|
|
||||||
'1/1': 0, '1/X': 0, '1/2': 0,
|
|
||||||
'X/1': 0, 'X/X': 0, 'X/2': 0,
|
|
||||||
'2/1': 0, '2/X': 0, '2/2': 0
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
const htHome = match.htScoreHome!;
|
|
||||||
const htAway = match.htScoreAway!;
|
|
||||||
const ftHome = match.scoreHome!;
|
|
||||||
const ftAway = match.scoreAway!;
|
|
||||||
|
|
||||||
const htResult = htHome > htAway ? '1' : htHome === htAway ? 'X' : '2';
|
|
||||||
const ftResult = ftHome > ftAway ? '1' : ftHome === ftAway ? 'X' : '2';
|
|
||||||
const htft = `${htResult}/${ftResult}`;
|
|
||||||
|
|
||||||
htftCounts[htft] = (htftCounts[htft] || 0) + 1;
|
|
||||||
totalMatches++;
|
|
||||||
|
|
||||||
if (htft === '1/2' || htft === '2/1') {
|
|
||||||
// Extract odds
|
|
||||||
let msHomeOdds: number | null = null;
|
|
||||||
let msDrawOdds: number | null = null;
|
|
||||||
let msAwayOdds: number | null = null;
|
|
||||||
let htHomeOdds: number | null = null;
|
|
||||||
let htDrawOdds: number | null = null;
|
|
||||||
let htAwayOdds: number | null = null;
|
|
||||||
|
|
||||||
for (const cat of match.oddCategories) {
|
|
||||||
const catName = (cat.name || '').toLowerCase();
|
|
||||||
const isHT = catName.includes('1.yarı');
|
|
||||||
|
|
||||||
for (const sel of cat.selections) {
|
|
||||||
const selName = (sel.name || '').toLowerCase();
|
|
||||||
if (!sel.oddValue) continue;
|
|
||||||
const odd = parseFloat(sel.oddValue.toString());
|
|
||||||
|
|
||||||
if (catName.includes('maç sonucu') || catName.includes('1.yarı sonucu')) {
|
|
||||||
if (selName === '1') { if (isHT) htHomeOdds = odd; else msHomeOdds = odd; }
|
|
||||||
else if (selName === 'x' || selName === '0') { if (isHT) htDrawOdds = odd; else msDrawOdds = odd; }
|
|
||||||
else if (selName === '2') { if (isHT) htAwayOdds = odd; else msAwayOdds = odd; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!match.homeTeam || !match.awayTeam || !match.league) continue;
|
|
||||||
|
|
||||||
reversalMatches.push({
|
|
||||||
id: match.id,
|
|
||||||
homeTeam: match.homeTeam.name,
|
|
||||||
awayTeam: match.awayTeam.name,
|
|
||||||
league: match.league.name,
|
|
||||||
htHome, htAway, ftHome, ftAway,
|
|
||||||
htft,
|
|
||||||
msHomeOdds, msDrawOdds, msAwayOdds,
|
|
||||||
htHomeOdds, htDrawOdds, htAwayOdds,
|
|
||||||
date: match.mstUtc,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print HT/FT distribution
|
|
||||||
console.log('\n📊 HT/FT DISTRIBUTION:');
|
|
||||||
for (const [key, count] of Object.entries(htftCounts)) {
|
|
||||||
const pct = (count / totalMatches * 100).toFixed(2);
|
|
||||||
const marker = (key === '1/2' || key === '2/1') ? ' ⚠️ REVERSAL' : '';
|
|
||||||
console.log(` ${key}: ${count} (${pct}%)${marker}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n⚠️ TOTAL REVERSAL MATCHES: ${reversalMatches.length} (${(reversalMatches.length / totalMatches * 100).toFixed(2)}%)`);
|
|
||||||
|
|
||||||
// ANALYSIS 1: League distribution
|
|
||||||
console.log('\n📈 ANALYSIS 1: LEAGUE DISTRIBUTION OF REVERSALS');
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
const leagueCounts: Record<string, { total: number, reversal: number }> = {};
|
|
||||||
|
|
||||||
for (const match of matches) {
|
|
||||||
if (!match.league) continue;
|
|
||||||
const htHome = match.htScoreHome!;
|
|
||||||
const htAway = match.htScoreAway!;
|
|
||||||
const ftHome = match.scoreHome!;
|
|
||||||
const ftAway = match.scoreAway!;
|
|
||||||
|
|
||||||
const htResult = htHome > htAway ? '1' : htHome === htAway ? 'X' : '2';
|
|
||||||
const ftResult = ftHome > ftAway ? '1' : ftHome === ftAway ? 'X' : '2';
|
|
||||||
const htft = `${htResult}/${ftResult}`;
|
|
||||||
|
|
||||||
const league = match.league.name;
|
|
||||||
if (!leagueCounts[league]) leagueCounts[league] = { total: 0, reversal: 0 };
|
|
||||||
leagueCounts[league].total++;
|
|
||||||
if (htft === '1/2' || htft === '2/1') leagueCounts[league].reversal++;
|
|
||||||
}
|
|
||||||
|
|
||||||
const leagueSorted = Object.entries(leagueCounts)
|
|
||||||
.filter(([_, v]) => v.reversal > 0 && v.total >= 50)
|
|
||||||
.sort((a, b) => (b[1].reversal / b[1].total) - (a[1].reversal / a[1].total))
|
|
||||||
.slice(0, 20);
|
|
||||||
|
|
||||||
console.log('\nTop 20 leagues by reversal rate (min 50 matches):');
|
|
||||||
for (const [league, data] of leagueSorted) {
|
|
||||||
const rate = (data.reversal / data.total * 100).toFixed(2);
|
|
||||||
console.log(` ${league}: ${data.reversal}/${data.total} (${rate}%)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANALYSIS 2: Odds patterns
|
|
||||||
console.log('\n📈 ANALYSIS 2: ODDS PATTERNS IN REVERSAL MATCHES');
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
|
|
||||||
const ms1_2 = reversalMatches.filter(m => m.htft === '1/2');
|
|
||||||
const ms2_1 = reversalMatches.filter(m => m.htft === '2/1');
|
|
||||||
|
|
||||||
console.log(`\n1/2 Reversals: ${ms1_2.length}`);
|
|
||||||
console.log(`2/1 Reversals: ${ms2_1.length}`);
|
|
||||||
|
|
||||||
// MS odds analysis for 1/2
|
|
||||||
const ms1_2_withOdds = ms1_2.filter(m => m.msHomeOdds && m.msAwayOdds);
|
|
||||||
if (ms1_2_withOdds.length > 0) {
|
|
||||||
const avgHomeOdd = ms1_2_withOdds.reduce((sum, m) => sum + m.msHomeOdds!, 0) / ms1_2_withOdds.length;
|
|
||||||
const avgAwayOdd = ms1_2_withOdds.reduce((sum, m) => sum + m.msAwayOdds!, 0) / ms1_2_withOdds.length;
|
|
||||||
const avgDrawOdd = ms1_2_withOdds.filter(m => m.msDrawOdds).reduce((sum, m) => sum + m.msDrawOdds!, 0) / ms1_2_withOdds.filter(m => m.msDrawOdds).length || 0;
|
|
||||||
|
|
||||||
console.log(`\n 1/2 Matches - Average MS Odds:`);
|
|
||||||
console.log(` Home Win: ${avgHomeOdd.toFixed(2)} (HT was WINNING!)`);
|
|
||||||
console.log(` Draw: ${avgDrawOdd.toFixed(2)}`);
|
|
||||||
console.log(` Away Win: ${avgAwayOdd.toFixed(2)} (but AWAY won FT!)`);
|
|
||||||
|
|
||||||
// Favorite analysis
|
|
||||||
let favoriteWon = 0;
|
|
||||||
let underdogWon = 0;
|
|
||||||
let noFavorite = 0;
|
|
||||||
|
|
||||||
for (const m of ms1_2_withOdds) {
|
|
||||||
if (m.msHomeOdds! < m.msAwayOdds!) {
|
|
||||||
// Home was favorite, but away won = UNDERDOG
|
|
||||||
underdogWon++;
|
|
||||||
} else if (m.msAwayOdds! < m.msHomeOdds!) {
|
|
||||||
// Away was favorite and won = FAVORITE
|
|
||||||
favoriteWon++;
|
|
||||||
} else {
|
|
||||||
noFavorite++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n 1/2 - Who was favored vs who won:`);
|
|
||||||
console.log(` Favorite won (Away was fav): ${favoriteWon} (${(favoriteWon / ms1_2_withOdds.length * 100).toFixed(1)}%)`);
|
|
||||||
console.log(` Underdog won (Home was fav): ${underdogWon} (${(underdogWon / ms1_2_withOdds.length * 100).toFixed(1)}%) ⚠️`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// MS odds analysis for 2/1
|
|
||||||
const ms2_1_withOdds = ms2_1.filter(m => m.msHomeOdds && m.msAwayOdds);
|
|
||||||
if (ms2_1_withOdds.length > 0) {
|
|
||||||
const avgHomeOdd = ms2_1_withOdds.reduce((sum, m) => sum + m.msHomeOdds!, 0) / ms2_1_withOdds.length;
|
|
||||||
const avgAwayOdd = ms2_1_withOdds.reduce((sum, m) => sum + m.msAwayOdds!, 0) / ms2_1_withOdds.length;
|
|
||||||
const avgDrawOdd = ms2_1_withOdds.filter(m => m.msDrawOdds).reduce((sum, m) => sum + m.msDrawOdds!, 0) / ms2_1_withOdds.filter(m => m.msDrawOdds).length || 0;
|
|
||||||
|
|
||||||
console.log(`\n 2/1 Matches - Average MS Odds:`);
|
|
||||||
console.log(` Home Win: ${avgHomeOdd.toFixed(2)} (HOME won FT!)`);
|
|
||||||
console.log(` Draw: ${avgDrawOdd.toFixed(2)}`);
|
|
||||||
console.log(` Away Win: ${avgAwayOdd.toFixed(2)} (Away was WINNING at HT!)`);
|
|
||||||
|
|
||||||
let favoriteWon = 0;
|
|
||||||
let underdogWon = 0;
|
|
||||||
|
|
||||||
for (const m of ms2_1_withOdds) {
|
|
||||||
if (m.msAwayOdds! < m.msHomeOdds!) {
|
|
||||||
// Away was favorite at HT, but home won = UNDERDOG
|
|
||||||
underdogWon++;
|
|
||||||
} else if (m.msHomeOdds! < m.msAwayOdds!) {
|
|
||||||
// Home was favorite and won = FAVORITE
|
|
||||||
favoriteWon++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n 2/1 - Who was favored vs who won:`);
|
|
||||||
console.log(` Favorite won (Home was fav): ${favoriteWon} (${(favoriteWon / ms2_1_withOdds.length * 100).toFixed(1)}%)`);
|
|
||||||
console.log(` Underdog won (Away was fav): ${underdogWon} (${(underdogWon / ms2_1_withOdds.length * 100).toFixed(1)}%) ⚠️`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANALYSIS 3: Suspicious patterns
|
|
||||||
console.log('\n📈 ANALYSIS 3: SUSPICIOUS PATTERNS');
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
|
|
||||||
// Pattern 1: Heavy favorite loses after leading (1/2 with low home odds)
|
|
||||||
const suspicious_1_2 = ms1_2_withOdds.filter(m => m.msHomeOdds! < 1.5);
|
|
||||||
console.log(`\n⚠️ PATTERN 1: Heavy Home Favorite loses after HT lead (MS Home Odds < 1.5):`);
|
|
||||||
console.log(` Count: ${suspicious_1_2.length}`);
|
|
||||||
if (suspicious_1_2.length > 0) {
|
|
||||||
const avgOdd = suspicious_1_2.reduce((sum, m) => sum + m.msHomeOdds!, 0) / suspicious_1_2.length;
|
|
||||||
console.log(` Avg Home Odds: ${avgOdd.toFixed(2)}`);
|
|
||||||
console.log(` Sample matches:`);
|
|
||||||
suspicious_1_2.slice(0, 5).forEach(m => {
|
|
||||||
console.log(` ${m.league}: ${m.homeTeam} (${m.msHomeOdds}) vs ${m.awayTeam} (${m.msAwayOdds}) => HT: ${m.htHome}-${m.htAway}, FT: ${m.ftHome}-${m.ftAway}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pattern 2: Heavy away favorite loses after leading (2/1 with low away odds)
|
|
||||||
const suspicious_2_1 = ms2_1_withOdds.filter(m => m.msAwayOdds! < 1.5);
|
|
||||||
console.log(`\n⚠️ PATTERN 2: Heavy Away Favorite loses after HT lead (MS Away Odds < 1.5):`);
|
|
||||||
console.log(` Count: ${suspicious_2_1.length}`);
|
|
||||||
if (suspicious_2_1.length > 0) {
|
|
||||||
const avgOdd = suspicious_2_1.reduce((sum, m) => sum + m.msAwayOdds!, 0) / suspicious_2_1.length;
|
|
||||||
console.log(` Avg Away Odds: ${avgOdd.toFixed(2)}`);
|
|
||||||
console.log(` Sample matches:`);
|
|
||||||
suspicious_2_1.slice(0, 5).forEach(m => {
|
|
||||||
console.log(` ${m.league}: ${m.homeTeam} (${m.msHomeOdds}) vs ${m.awayTeam} (${m.msAwayOdds}) => HT: ${m.htHome}-${m.htAway}, FT: ${m.ftHome}-${m.ftAway}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANALYSIS 4: HT Odds vs MS Odds correlation
|
|
||||||
console.log('\n📈 ANALYSIS 4: HT ODDS CORRELATION');
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
|
|
||||||
const withHTOdds = reversalMatches.filter(m => m.htHomeOdds && m.htAwayOdds);
|
|
||||||
if (withHTOdds.length > 0) {
|
|
||||||
console.log(`\n Matches with HT odds: ${withHTOdds.length}`);
|
|
||||||
|
|
||||||
let htCorrectlyPredicted = 0;
|
|
||||||
for (const m of withHTOdds) {
|
|
||||||
const htFav = m.htHomeOdds! < m.htAwayOdds! ? '1' : m.htAwayOdds! < m.htHomeOdds! ? '2' : 'X';
|
|
||||||
const htActual = m.htHome > m.htAway ? '1' : m.htAway > m.htHome ? '2' : 'X';
|
|
||||||
if (htFav === htActual) htCorrectlyPredicted++;
|
|
||||||
}
|
|
||||||
console.log(` HT Favorite correctly led at HT: ${htCorrectlyPredicted}/${withHTOdds.length} (${(htCorrectlyPredicted / withHTOdds.length * 100).toFixed(1)}%)`);
|
|
||||||
|
|
||||||
// How often did HT favorite lose FT?
|
|
||||||
let htFavoriteLostFT = 0;
|
|
||||||
for (const m of withHTOdds) {
|
|
||||||
const htFav = m.htHomeOdds! < m.htAwayOdds! ? '1' : m.htAwayOdds! < m.htHomeOdds! ? '2' : 'X';
|
|
||||||
const ftActual = m.ftHome > m.ftAway ? '1' : m.ftAway > m.ftHome ? '2' : 'X';
|
|
||||||
if (htFav !== ftActual) htFavoriteLostFT++;
|
|
||||||
}
|
|
||||||
console.log(` HT Favorite lost FT: ${htFavoriteLostFT}/${withHTOdds.length} (${(htFavoriteLostFT / withHTOdds.length * 100).toFixed(1)}%) ⚠️`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ANALYSIS 5: Score patterns
|
|
||||||
console.log('\n📈 ANALYSIS 5: SCORE PATTERNS IN REVERSALS');
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
|
|
||||||
// HT score distribution for reversals
|
|
||||||
const htScores: Record<string, number> = {};
|
|
||||||
for (const m of reversalMatches) {
|
|
||||||
const key = `${m.htHome}-${m.htAway}`;
|
|
||||||
htScores[key] = (htScores[key] || 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nMost common HT scores in reversal matches:');
|
|
||||||
Object.entries(htScores)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 10)
|
|
||||||
.forEach(([score, count]) => {
|
|
||||||
console.log(` HT ${score}: ${count} matches`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// FT score distribution
|
|
||||||
const ftScores: Record<string, number> = {};
|
|
||||||
for (const m of reversalMatches) {
|
|
||||||
const key = `${m.ftHome}-${m.ftAway}`;
|
|
||||||
ftScores[key] = (ftScores[key] || 0) + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\nMost common FT scores in reversal matches:');
|
|
||||||
Object.entries(ftScores)
|
|
||||||
.sort((a, b) => b[1] - a[1])
|
|
||||||
.slice(0, 10)
|
|
||||||
.forEach(([score, count]) => {
|
|
||||||
console.log(` FT ${score}: ${count} matches`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ANALYSIS 6: Goal difference patterns
|
|
||||||
console.log('\n📈 ANALYSIS 6: COMEBACK MAGNITUDE');
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
|
|
||||||
let comebackBy1 = 0;
|
|
||||||
let comebackBy2 = 0;
|
|
||||||
let comebackBy3Plus = 0;
|
|
||||||
|
|
||||||
for (const m of reversalMatches) {
|
|
||||||
const htDiff = Math.abs(m.htHome - m.htAway);
|
|
||||||
const ftDiff = Math.abs(m.ftHome - m.ftAway);
|
|
||||||
|
|
||||||
if (m.htft === '1/2') {
|
|
||||||
// Home was leading, away won
|
|
||||||
const margin = (m.ftAway - m.ftHome);
|
|
||||||
if (margin === 1) comebackBy1++;
|
|
||||||
else if (margin === 2) comebackBy2++;
|
|
||||||
else comebackBy3Plus++;
|
|
||||||
} else {
|
|
||||||
// Away was leading, home won
|
|
||||||
const margin = (m.ftHome - m.ftAway);
|
|
||||||
if (margin === 1) comebackBy1++;
|
|
||||||
else if (margin === 2) comebackBy2++;
|
|
||||||
else comebackBy3Plus++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n Comeback by 1 goal: ${comebackBy1} (${(comebackBy1 / reversalMatches.length * 100).toFixed(1)}%)`);
|
|
||||||
console.log(` Comeback by 2 goals: ${comebackBy2} (${(comebackBy2 / reversalMatches.length * 100).toFixed(1)}%)`);
|
|
||||||
console.log(` Comeback by 3+ goals: ${comebackBy3Plus} (${(comebackBy3Plus / reversalMatches.length * 100).toFixed(1)}%) ⚠️`);
|
|
||||||
|
|
||||||
// Show extreme comebacks
|
|
||||||
const extremeComebacks = reversalMatches
|
|
||||||
.filter(m => {
|
|
||||||
if (m.htft === '1/2') return (m.ftAway - m.ftHome) >= 2;
|
|
||||||
return (m.ftHome - m.ftAway) >= 2;
|
|
||||||
})
|
|
||||||
.sort((a, b) => {
|
|
||||||
const diffA = a.htft === '1/2' ? (a.ftAway - a.ftHome) : (a.ftHome - a.ftAway);
|
|
||||||
const diffB = b.htft === '1/2' ? (b.ftAway - b.ftHome) : (b.ftHome - b.ftAway);
|
|
||||||
return diffB - diffA;
|
|
||||||
})
|
|
||||||
.slice(0, 10);
|
|
||||||
|
|
||||||
console.log('\nTop 10 most extreme comebacks:');
|
|
||||||
extremeComebacks.forEach(m => {
|
|
||||||
const diff = m.htft === '1/2' ? (m.ftAway - m.ftHome) : (m.ftHome - m.ftAway);
|
|
||||||
console.log(` ${m.league}: ${m.homeTeam} vs ${m.awayTeam} | HT: ${m.htHome}-${m.htAway} => FT: ${m.ftHome}-${m.ftAway} (Diff: ${diff})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('\n' + '='.repeat(80));
|
|
||||||
console.log('✅ ANALYSIS COMPLETE');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
analyzeReversalMatches().catch(console.error);
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// BigInt serialization fix
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
const matchId = '7cnm7h7qbsq2bbaxngusojh90';
|
|
||||||
|
|
||||||
async function checkLineupData() {
|
|
||||||
const match = await prisma.liveMatch.findUnique({
|
|
||||||
where: { id: matchId },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.log('❌ Match not found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n📊 LINEUP DATA INSPECTION');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
console.log(`\n1. lineups field:`);
|
|
||||||
console.log(` Type: ${typeof match.lineups}`);
|
|
||||||
console.log(` Is null: ${match.lineups === null}`);
|
|
||||||
console.log(` Content:`, JSON.stringify(match.lineups, null, 2));
|
|
||||||
|
|
||||||
console.log(`\n2. sidelined field:`);
|
|
||||||
console.log(` Type: ${typeof match.sidelined}`);
|
|
||||||
console.log(` Is null: ${match.sidelined === null}`);
|
|
||||||
console.log(` Content:`, JSON.stringify(match.sidelined, null, 2));
|
|
||||||
|
|
||||||
console.log(`\n3. odds field:`);
|
|
||||||
console.log(` Type: ${typeof match.odds}`);
|
|
||||||
console.log(` Is null: ${match.odds === null}`);
|
|
||||||
|
|
||||||
// Check if it's JSON object or string
|
|
||||||
if (match.odds) {
|
|
||||||
const oddsStr = typeof match.odds === 'string' ? match.odds : JSON.stringify(match.odds);
|
|
||||||
console.log(` Length: ${oddsStr.length}`);
|
|
||||||
console.log(` Preview: ${oddsStr.substring(0, 200)}...`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n4. refereeName:`);
|
|
||||||
console.log(` Value: ${match.refereeName}`);
|
|
||||||
|
|
||||||
// Now check what AI Engine sees
|
|
||||||
console.log('\n\n🔍 AI ENGINE PERSPECTIVE');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
// Simulate AI Engine's lineup parsing
|
|
||||||
const lineups = match.lineups as any;
|
|
||||||
let homePlayers: any[] = [];
|
|
||||||
let awayPlayers: any[] = [];
|
|
||||||
|
|
||||||
if (lineups && typeof lineups === 'object') {
|
|
||||||
if (lineups.home?.xi) {
|
|
||||||
homePlayers = lineups.home.xi;
|
|
||||||
}
|
|
||||||
if (lineups.away?.xi) {
|
|
||||||
awayPlayers = lineups.away.xi;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\nHome lineup count: ${homePlayers.length}`);
|
|
||||||
console.log(`Away lineup count: ${awayPlayers.length}`);
|
|
||||||
console.log(`Lineup source would be: ${homePlayers.length >= 9 && awayPlayers.length >= 9 ? 'confirmed_live' : 'none/probable'}`);
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
checkLineupData().catch(console.error);
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#!/usr/bin/expect -f
|
|
||||||
spawn ssh -p 2222 -o StrictHostKeyChecking=accept-new haruncan@95.70.252.214 "mkdir -p ~/.ssh && echo 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGo7pRd2fozEvxIultfwgoajgNOzc0RVywcqrqgZho62 piton@Pitons-MacBook-Air.local' >> ~/.ssh/authorized_keys && chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys"
|
|
||||||
|
|
||||||
expect {
|
|
||||||
"assword:" {
|
|
||||||
send "M594xH%\$iM&4MM\r"
|
|
||||||
exp_continue
|
|
||||||
}
|
|
||||||
eof
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
Thu Apr 16 12:20:54 UTC 2026
|
|
||||||
==== DOCKER PS ====
|
|
||||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
|
||||||
78aab6872b85 gitea/runner-images:ubuntu-latest "/bin/sleep 10800" 2 seconds ago Up 1 second GITEA-ACTIONS-TASK-185_WORKFLOW-Check-Docker-Pi_JOB-check-docker
|
|
||||||
784ca4842e79 iddaai-be:latest "docker-entrypoint.s…" 6 minutes ago Up 6 minutes 3000/tcp, 127.0.0.1:1810->3005/tcp iddaai-be
|
|
||||||
48f495d45025 iddaai-fe:latest "docker-entrypoint.s…" 2 hours ago Up 2 hours 127.0.0.1:1510->3000/tcp iddaai-fe
|
|
||||||
a60b07c52d7a gitea/act_runner:latest "/sbin/tini -- run.sh" 22 hours ago Up 22 hours gitea_runner
|
|
||||||
436552af4199 iddaai-ai-engine "uvicorn main:app --…" 23 hours ago Up 23 hours (healthy) 8000/tcp iddaai-ai-engine
|
|
||||||
696050fc89de postgres:17-alpine "docker-entrypoint.s…" 23 hours ago Up 23 hours (healthy) 5432/tcp iddaai-postgres
|
|
||||||
abcc43242dbb redis:7-alpine "docker-entrypoint.s…" 23 hours ago Up 23 hours (healthy) 6379/tcp iddaai-redis
|
|
||||||
da0f2d5bc898 temporalio/auto-setup:latest "/etc/temporal/entry…" 3 weeks ago Up 8 days 6933-6935/tcp, 6939/tcp, 7233-7235/tcp, 7239/tcp temporal
|
|
||||||
4768eec66926 ghcr.io/gitroomhq/postiz-app:latest "docker-entrypoint.s…" 3 weeks ago Up 8 days 0.0.0.0:4007->5000/tcp, [::]:4007->5000/tcp postiz
|
|
||||||
5cfb55782d8b postgres:16 "docker-entrypoint.s…" 3 weeks ago Up 8 days 5432/tcp temporal-postgresql
|
|
||||||
cf8591458662 redis:7.2 "docker-entrypoint.s…" 3 weeks ago Up 8 days (healthy) 6379/tcp postiz-redis
|
|
||||||
0108dc0b875d postgres:17-alpine "docker-entrypoint.s…" 3 weeks ago Up 8 days (healthy) 5432/tcp postiz-postgres
|
|
||||||
c88a569ddb22 elasticsearch:8.16.2 "/bin/tini -- /usr/l…" 3 weeks ago Up 8 days 9200/tcp, 9300/tcp temporal-elasticsearch
|
|
||||||
208bbf92c2d8 temporalio/ui:latest "./start-ui-server.sh" 3 weeks ago Up 8 days 0.0.0.0:8085->8080/tcp, [::]:8085->8080/tcp temporal-ui
|
|
||||||
a0555f255857 haruncan-studio-fe:latest "/docker-entrypoint.…" 3 weeks ago Up 8 days 0.0.0.0:1509->80/tcp, [::]:1509->80/tcp haruncan-studio-fe-container
|
|
||||||
7591abf68bf5 backend-haruncan-studio:latest "docker-entrypoint.s…" 3 weeks ago Up 8 days 0.0.0.0:1809->3000/tcp, [::]:1809->3000/tcp backend-haruncan-studio-container
|
|
||||||
96d02609b108 ui-indir:latest "docker-entrypoint.s…" 5 weeks ago Up 8 days 0.0.0.0:1507->3000/tcp, [::]:1507->3000/tcp ui-indir-container
|
|
||||||
f67335b1625f ghcr.io/open-webui/open-webui:main "bash start.sh" 6 weeks ago Up 8 days (healthy) 0.0.0.0:3001->8080/tcp, [::]:3001->8080/tcp openclaw
|
|
||||||
24b3c6e32817 gitea/gitea:latest "/usr/bin/entrypoint…" 6 weeks ago Up 8 days 0.0.0.0:222->22/tcp, [::]:222->22/tcp, 0.0.0.0:1224->3000/tcp, [::]:1224->3000/tcp gitea
|
|
||||||
4e64e3199178 postgres:14 "docker-entrypoint.s…" 6 weeks ago Up 8 days 5432/tcp gitea_db
|
|
||||||
cb7fdcbcd79f postgres:16-alpine "docker-entrypoint.s…" 6 weeks ago Up 8 days 5432/tcp backend_db
|
|
||||||
f0784aedcadf redis:alpine "docker-entrypoint.s…" 6 weeks ago Up 8 days 6379/tcp apps_redis
|
|
||||||
fdc89d4a236a portainer/portainer-ce:latest "/portainer" 6 weeks ago Up 8 days 8000/tcp, 9443/tcp, 0.0.0.0:9000->9000/tcp, [::]:9000->9000/tcp portainer
|
|
||||||
2de41ca39c1f backend-proje:latest "docker-entrypoint.s…" 2 months ago Restarting (1) 35 seconds ago backend-container
|
|
||||||
89268da2ab86 skript-ui "docker-entrypoint.s…" 2 months ago Up 8 days 0.0.0.0:1506->3000/tcp, [::]:1506->3000/tcp ui-skript-container
|
|
||||||
8fced773c984 skript-be "docker-entrypoint.s…" 2 months ago Exited (1) 8 days ago backend-skript-container
|
|
||||||
ec90982f14b6 backend-digicraft "docker-entrypoint.s…" 2 months ago Up 8 days 0.0.0.0:1805->3001/tcp, [::]:1805->3001/tcp backend-digicraft-container
|
|
||||||
4eec58a7f453 ui-digicraft "/docker-entrypoint.…" 2 months ago Up 8 days 0.0.0.0:1505->80/tcp, [::]:1505->80/tcp ui-digicraft-container
|
|
||||||
37f844a6cd20 frontend-proje:latest "docker-entrypoint.s…" 2 months ago Up 8 days 0.0.0.0:1800->3000/tcp, [::]:1800->3000/tcp frontend-container
|
|
||||||
==== DOCKER STATS ====
|
|
||||||
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
|
|
||||||
78aab6872b85 GITEA-ACTIONS-TASK-185_WORKFLOW-Check-Docker-Pi_JOB-check-docker 1.07% 0B / 0B 0.00% 1.12MB / 13.6kB 135kB / 0B 12
|
|
||||||
784ca4842e79 iddaai-be 0.00% 0B / 0B 0.00% 12.9MB / 1.04MB 250kB / 0B 18
|
|
||||||
48f495d45025 iddaai-fe 0.00% 0B / 0B 0.00% 491kB / 221kB 1.84MB / 0B 26
|
|
||||||
a60b07c52d7a gitea_runner 0.10% 0B / 0B 0.00% 25.6MB / 24.8MB 4MB / 0B 11
|
|
||||||
436552af4199 iddaai-ai-engine 0.14% 0B / 0B 0.00% 881kB / 840kB 175MB / 0B 20
|
|
||||||
696050fc89de iddaai-postgres 0.00% 0B / 0B 0.00% 130MB / 311MB 685MB / 0B 13
|
|
||||||
abcc43242dbb iddaai-redis 2.39% 0B / 0B 0.00% 222kB / 126B 3.95MB / 0B 6
|
|
||||||
da0f2d5bc898 temporal 1.49% 0B / 0B 0.00% 1.75GB / 1.88GB 300MB / 0B 15
|
|
||||||
4768eec66926 postiz 0.54% 0B / 0B 0.00% 8.09MB / 4.33MB 474MB / 0B 152
|
|
||||||
5cfb55782d8b temporal-postgresql 0.03% 0B / 0B 0.00% 1.88GB / 1.75GB 57.1MB / 0B 39
|
|
||||||
cf8591458662 postiz-redis 0.17% 0B / 0B 0.00% 1.17MB / 545kB 23.4MB / 0B 6
|
|
||||||
0108dc0b875d postiz-postgres 0.00% 0B / 0B 0.00% 945kB / 176kB 46.1MB / 0B 8
|
|
||||||
c88a569ddb22 temporal-elasticsearch 0.18% 0B / 0B 0.00% 763kB / 96.1kB 288MB / 0B 94
|
|
||||||
208bbf92c2d8 temporal-ui 0.00% 0B / 0B 0.00% 655kB / 22.7kB 66.6MB / 0B 8
|
|
||||||
a0555f255857 haruncan-studio-fe-container 0.00% 0B / 0B 0.00% 2.13MB / 7.98MB 4.78MB / 0B 5
|
|
||||||
7591abf68bf5 backend-haruncan-studio-container 0.00% 0B / 0B 0.00% 776kB / 724kB 127MB / 0B 17
|
|
||||||
96d02609b108 ui-indir-container 0.00% 0B / 0B 0.00% 118MB / 27.3MB 139MB / 0B 11
|
|
||||||
f67335b1625f openclaw 0.11% 0B / 0B 0.00% 652kB / 16.4kB 1GB / 0B 19
|
|
||||||
24b3c6e32817 gitea 2.44% 0B / 0B 0.00% 1.69GB / 1.24GB 200MB / 0B 20
|
|
||||||
4e64e3199178 gitea_db 0.74% 0B / 0B 0.00% 1.03GB / 1.25GB 69.1MB / 0B 10
|
|
||||||
cb7fdcbcd79f backend_db 0.00% 0B / 0B 0.00% 677kB / 126B 41.4MB / 0B 6
|
|
||||||
f0784aedcadf apps_redis 0.23% 0B / 0B 0.00% 677kB / 126B 31MB / 0B 6
|
|
||||||
fdc89d4a236a portainer 0.00% 0B / 0B 0.00% 4.08MB / 20.8MB 126MB / 0B 7
|
|
||||||
2de41ca39c1f backend-container 0.00% 0B / 0B 0.00% 0B / 0B 0B / 0B 0
|
|
||||||
89268da2ab86 ui-skript-container 0.00% 0B / 0B 0.00% 1.71MB / 11.3kB 51.3MB / 0B 11
|
|
||||||
ec90982f14b6 backend-digicraft-container 0.06% 0B / 0B 0.00% 2.41MB / 436kB 142MB / 0B 38
|
|
||||||
4eec58a7f453 ui-digicraft-container 0.00% 0B / 0B 0.00% 3.95MB / 27.3MB 5.8MB / 0B 5
|
|
||||||
37f844a6cd20 frontend-container 0.01% 0B / 0B 0.00% 1.93MB / 3.11MB 13.6MB / 0B 11
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
|
|
||||||
> Suggest-Bet-BE@0.0.1 lint
|
|
||||||
> eslint "{src,apps,libs,test}/**/*.ts" --fix
|
|
||||||
|
|
||||||
@@ -0,0 +1,305 @@
|
|||||||
|
# SESSION HANDOFF — iddaai sistem durumu
|
||||||
|
|
||||||
|
**Son güncelleme**: 2026-05-25 ~23:00 (Windows'tan Mac'e geçiş öncesi)
|
||||||
|
**Hedef**: Başka makinede / yeni Claude session'ında bu doc tek başına okunup işin nerede kaldığı anlaşılabilmeli.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚨 EN SON DURUM (Mac'e geçmeden önce oku)
|
||||||
|
|
||||||
|
### Validation backtest ÖLDÜ
|
||||||
|
- Pencere: 2026-05-01 → 2026-05-14, 1500 maç
|
||||||
|
- **1200/1500'de SSH tunnel düşünce process sessizce öldü**
|
||||||
|
- **CSV kayıp** — script eski versiyondu, sadece sonda yazıyordu
|
||||||
|
- Sebep: localhost:5432 erişimi kayboldu, psycopg2 connection error
|
||||||
|
|
||||||
|
### Script DÜZELTILDI (Mac'te kullanılabilir)
|
||||||
|
`scripts/diagnostic_backtest.py` artık **crash-safe**:
|
||||||
|
- `--checkpoint-every 100` → her 100 maçta partial CSV diske yazılır
|
||||||
|
- Crash sonrası tekrar koşulunca **otomatik kaldığı yerden devam**
|
||||||
|
- Checkpoint dosyası: `reports/_checkpoint_<window-key>.csv`
|
||||||
|
- `--no-resume` flag fresh başlamak için
|
||||||
|
|
||||||
|
### Git push BEKLİYOR
|
||||||
|
- 36 dosya **commit edildi (local)** — bkz "Bu seansta yapılan KOD değişiklikleri"
|
||||||
|
- Push **auth hatası** verdi (gitea credentials cached değil)
|
||||||
|
- **User Mac'te push yapacak** (Gitea Personal Access Token gerekli, repo write yetkisi)
|
||||||
|
|
||||||
|
### Mac'te yapılacaklar (öncelikli sırayla)
|
||||||
|
1. Repo'yu clone et veya OneDrive'dan kopyala (eğer Mac OneDrive senkronize ediyorsa)
|
||||||
|
2. `git push origin main` ile pending commit'i remote'a yolla
|
||||||
|
3. SSH tunnel kur (Pi @ 95.70.252.214, port 2222) → DB için tunnel localhost:5432
|
||||||
|
4. Yeni Claude session'ı başlat, bu dosyayı oku, devam et
|
||||||
|
5. Backtest tekrar koştur (çoktan eski versiyondu, şimdi crash-safe)
|
||||||
|
```bash
|
||||||
|
cd ai-engine
|
||||||
|
export DATABASE_URL="postgresql://iddaai_user:IddaA1_S4crET!@localhost:5432/iddaai_db?schema=public"
|
||||||
|
export PYTHONIOENCODING=utf-8
|
||||||
|
python scripts/diagnostic_backtest.py --start 2026-05-01 --end 2026-05-14 --max-matches 1500
|
||||||
|
# ölürse, aynı komutu tekrar koş — checkpoint'ten devam eder
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mevcut sağlam veri
|
||||||
|
Validation kayıp ama elimizde **in-sample backtest** ve **grid search** çıktıları var:
|
||||||
|
- `reports/diagnostic_backtest_20260525_035649.{csv,json,txt}` — 1000 maç, May 11-24
|
||||||
|
- `reports/filter_optimization_patch.json` — grid search winners
|
||||||
|
- Bu data ile in-sample analiz tamamlandı, validation eksik
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Üst-seviye hedef
|
||||||
|
|
||||||
|
Sistem **maç başı-1 saat** kullanıcı tetiklemesiyle çalışacak. Bahis uzmanı seviyesinde:
|
||||||
|
- **main_pick + value_pick** (sistemin önerdiği)
|
||||||
|
- **Tüm market olasılıkları** (MS, HT, OU05-45, BTTS, OE, DC, HTFT, HCAP, Cards, Corners)
|
||||||
|
- **Net HT + FT skoru** + **Top-5 olası skor dağılımı**
|
||||||
|
- **Evidence panel**: lineup impact, son 5 maç, h2h, hakem profili, benzer-oran-band geçmişi
|
||||||
|
|
||||||
|
Ürün modeli: hem user kendi bahisini oynar, hem sistem para kazanırsa abonelik satılır.
|
||||||
|
Hedef ROI: **≥%10**. Günde **3-5 kaliteli bahis**.
|
||||||
|
|
||||||
|
Detaylı requirements doc: bu dosyanın altında, "Requirements Spec" bölümü.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟢 Şu an arka planda KOŞAN işler
|
||||||
|
|
||||||
|
### 1. Validation backtest (LOCAL — bu laptop)
|
||||||
|
- **Script**: `ai-engine/scripts/diagnostic_backtest.py`
|
||||||
|
- **Komut**: `python scripts/diagnostic_backtest.py --start 2026-05-01 --end 2026-05-14 --max-matches 1500`
|
||||||
|
- **Log**: `ai-engine/validation_full.log` (OneDrive senkronize)
|
||||||
|
- **Çıkış**: bittiğinde `ai-engine/reports/diagnostic_backtest_<timestamp>.{csv,json,txt}`
|
||||||
|
- **Tahmini bitiş**: 2026-05-25 ~22:00 (yaklaşık)
|
||||||
|
- **Amaç**: Yeni kodla (calibrator + ev_edge veto + envelope + coherence + BTTS mute) **out-of-sample** doğrulama
|
||||||
|
- **Risk**: Laptop uyursa ölür. Bitmesini beklemen lazım VEYA partial sonuçla devam.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Status check (kendin)
|
||||||
|
$log='C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\ai-engine\validation_full.log'
|
||||||
|
Select-String $log 'rate=|Outputs:' | Select-Object -Last 3 | ForEach-Object {$_.Line}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Feeder historical scan (REMOTE — Pi server)
|
||||||
|
- **Konum**: SSH @ haruncan@95.70.252.214:2222 → docker container `iddaai-be` → pm2
|
||||||
|
- **PM2 process**: `feeder-historical` (id=1)
|
||||||
|
- **Log rotation**: pm2-logrotate kurulu (max 30MB/dosya, 3 dosya, gzip)
|
||||||
|
- **Davranış**: 2026-05-03'ten geriye 2023-06-01'e kadar mackolik'ten odds/lineup patch
|
||||||
|
- **Otomatik restart**: 502 olunca 30 sn delay sonra restart (max 1000 kez)
|
||||||
|
- **Beklenen süre**: 24-72 saat
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Status (kendin SSH'la)
|
||||||
|
sudo docker exec iddaai-be pm2 list
|
||||||
|
sudo docker exec iddaai-be pm2 logs feeder-historical --lines 30 --nostream
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Bu seansta yapılan KOD değişiklikleri
|
||||||
|
|
||||||
|
Hepsi local repo'da, OneDrive senkronize edecek, başka makinede pull etmesen de açtığında orada olacak.
|
||||||
|
|
||||||
|
### A. Settlement / data layer
|
||||||
|
| Dosya | Değişiklik |
|
||||||
|
|---|---|
|
||||||
|
| `iddaai-be/prisma.config.ts` | `.env` fallback ekledim (`.env.local` üstüne) — `prisma generate` çalışsın diye |
|
||||||
|
| `iddaai-be/src/tasks/prediction-settlement.market-resolver.ts` | DC parser ayraçsız "1X/X2/12" kabul ediyor + HT_OU05/HT_OU15/HT_OU25 resolver eklendi |
|
||||||
|
| `iddaai-be/src/tasks/feature-enrichment.task.ts` **(YENİ)** | Cron 08:15 — eksik football_ai_features row insert + odds_movement SQL backfill |
|
||||||
|
| `iddaai-be/src/tasks/python-enrichment.task.ts` **(YENİ)** | Cron 08:25 — Python `enrich_ai_features.py` subprocess |
|
||||||
|
| `iddaai-be/src/tasks/tasks.module.ts` | İki yeni task register |
|
||||||
|
| `iddaai-be/src/scripts/run-feature-enrichment.ts` **(YENİ)** | Manuel one-shot trigger |
|
||||||
|
|
||||||
|
### B. AI engine — betting brain
|
||||||
|
`iddaai-be/ai-engine/services/betting_brain.py` — büyük revizyon:
|
||||||
|
- **HARD_MIN_SAMPLES = 50** floor (calibrator bypass <50 sample)
|
||||||
|
- **`ev_edge < 0.0` HARD VETO** (`negative_ev_edge`)
|
||||||
|
- **`ev_edge >= 0.20` HARD VETO** (`ev_edge_too_high_trap`)
|
||||||
|
- **`MUTED_MARKETS = {"BTTS"}`** — backtest no profitable config bulduğu için
|
||||||
|
- **`MARKET_OPTIMAL_FILTERS`** — MS ve OU25 için grid-search'ten gelen optimal envelope
|
||||||
|
- **`_score_consistent_markets()`** — skor tahminine uymayan picks elimine
|
||||||
|
- **`judge()` score coherence filter** — main_pick coherent set'ten seçilir
|
||||||
|
- **HTFT reversal cross-check** — Man City 1/2 senaryosu
|
||||||
|
|
||||||
|
### C. AI engine — model & calibration
|
||||||
|
| Dosya | Değişiklik |
|
||||||
|
|---|---|
|
||||||
|
| `ai-engine/models/calibration.py` | HARD_MIN_SAMPLES floor + sample-weighted blend formülü değişti |
|
||||||
|
| `ai-engine/models/calibration/*.pkl` | **10 calibrator retrain** (ms_home/draw/away, ou15/25/35, btts, ht_home/draw/away) — 4989-5000 sample her biri |
|
||||||
|
|
||||||
|
### D. AI engine — orchestrator feature builder
|
||||||
|
`ai-engine/services/orchestrator/feature_builder.py`:
|
||||||
|
- Hardcoded `home_position=10, away_position=10` → real `data.home_position` kullanılıyor
|
||||||
|
- Cup detection upper'a taşındı, `is_cup_match` UpsetEngine'e geçiyor
|
||||||
|
- Total teams parametresi UpsetEngine'e geçiyor
|
||||||
|
|
||||||
|
`ai-engine/services/orchestrator/data_loader.py`:
|
||||||
|
- `_estimate_league_position` artık **sezon filtresi** (son 300 gün) kullanıyor
|
||||||
|
|
||||||
|
### E. AI engine — scripts (yeni)
|
||||||
|
| Dosya | Ne yapıyor |
|
||||||
|
|---|---|
|
||||||
|
| `ai-engine/scripts/diagnostic_backtest.py` | Per-bet diagnostic backtest (CSV+JSON+TXT output) |
|
||||||
|
| `ai-engine/scripts/analyze_backtest_csv.py` | Backtest CSV üzerinde root-cause hipotez testleri |
|
||||||
|
| `ai-engine/scripts/optimize_filters.py` | Grid search per-market optimal threshold |
|
||||||
|
| `ai-engine/scripts/compare_backtests.py` | İki CSV karşılaştırması verdict ile |
|
||||||
|
| `ai-engine/scripts/test_score_coherence.py` | Coherence filter smoke test (LAFC senaryosu) |
|
||||||
|
|
||||||
|
### F. Social poster modülü (NestJS)
|
||||||
|
| Dosya | Değişiklik |
|
||||||
|
|---|---|
|
||||||
|
| `src/modules/social-poster/social-poster.service.ts` | Cron 15→10 dk, window 10-60, MAX_POSTS_PER_RUN, getHealthStatus() |
|
||||||
|
| `src/modules/social-poster/image-renderer.service.ts` | SEO filename + metadata sidecar (.json) |
|
||||||
|
| `src/modules/social-poster/caption-generator.service.ts` | SEO hashtag stratejisi (12 küratör tag) |
|
||||||
|
| `src/modules/social-poster/social-poster.controller.ts` | `/health` public + `/preview-png/:matchId` + `/run-now` endpoints |
|
||||||
|
| `mds/SOCIAL_POSTER_SETUP.md` **(YENİ)** | Env vars + API key alma adımları + test komutları |
|
||||||
|
|
||||||
|
### G. Modern image rendering (deneme)
|
||||||
|
| Dosya | Açıklama |
|
||||||
|
|---|---|
|
||||||
|
| `src/scripts/render-social-card-v3.ts` | satori + resvg-js ile modern HTML→PNG rendering (Twemoji top + bayrak) |
|
||||||
|
| `src/modules/social-poster/assets/*.svg` | Twemoji futbol/basket/bayrak SVG'leri |
|
||||||
|
|
||||||
|
### H. Yapılan DB değişiklikleri (idempotent — tekrar koşturulursa sorun yok)
|
||||||
|
| İşlem | Etki |
|
||||||
|
|---|---|
|
||||||
|
| `football_ai_features` 4008+ satır backfill | Son 60 günün FT maçları için feature row var artık (calculator_ver=feature_enrichment_task_v1) |
|
||||||
|
| Python enrichment koştu | h2h, referee, possession, league_avg, implied_* hepsi gerçek değerlerle dolu (181,614+ satır enriched) |
|
||||||
|
| Calibrator dosyaları yazıldı | `ai-engine/models/calibration/*.pkl` overwritten |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 Önemli dosya konumları (OneDrive synced)
|
||||||
|
|
||||||
|
```
|
||||||
|
iddaai-be/
|
||||||
|
├── mds/
|
||||||
|
│ ├── SESSION_HANDOFF.md ← BU DOSYA
|
||||||
|
│ └── SOCIAL_POSTER_SETUP.md ← social poster env+keys
|
||||||
|
├── ai-engine/
|
||||||
|
│ ├── reports/ ← BACKTEST CIKTILARI
|
||||||
|
│ │ ├── diagnostic_backtest_*.csv,json,txt
|
||||||
|
│ │ └── filter_optimization_patch.json
|
||||||
|
│ ├── validation_full.log ← validation backtest canlı log
|
||||||
|
│ ├── diagnostic_backtest_run.log ← önceki backtest log
|
||||||
|
│ ├── enrichment_run3.log ← enrichment koşma log
|
||||||
|
│ └── calibration_run.log ← calibrator retrain log
|
||||||
|
├── public/predictions/ ← render edilmiş social card PNG/JSON
|
||||||
|
└── src/scripts/ ← tüm yeni script'ler
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔑 Erişim bilgileri
|
||||||
|
|
||||||
|
### Pi sunucu (feeder + prod stack)
|
||||||
|
- **SSH**: `haruncan@95.70.252.214:2222`
|
||||||
|
- **Şifre**: `M594xH%$iM&4MM`
|
||||||
|
- **Plink kullan**: `~/plink.exe -ssh -P 2222 -pw '<password>' -hostkey 'SHA256:iq0YVI/4J897sf9dkksI7QzetpLCD0l57ZMX4UissI8' haruncan@95.70.252.214`
|
||||||
|
- **Docker**: `iddaai-be`, `iddaai-ai-engine`, `iddaai-fe`, `iddaai-postgres`, `iddaai-redis`, `gitea`
|
||||||
|
|
||||||
|
### DB (uzak Postgres @ Pi)
|
||||||
|
- **SSH tunnel function**: `iddaai-db` PowerShell fonksiyonu (yerel makinedeki profile'da kayıtlı)
|
||||||
|
- **Tunnel: localhost:5432 → Pi:5432**
|
||||||
|
- **Connection string**: `postgresql://iddaai_user:IddaA1_S4crET!@localhost:5432/iddaai_db?schema=public`
|
||||||
|
- **MCP**: Claude'un postgres MCP'si bu tunnel üzerinden çalışıyor (restricted mode, read-only)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 BACKTEST sonuçları geçmişi
|
||||||
|
|
||||||
|
### Backtest #1 — In-sample grid search (2026-05-11 → 05-24, 1000 maç)
|
||||||
|
- **CSV**: `ai-engine/reports/diagnostic_backtest_20260525_035649.csv`
|
||||||
|
- **TXT**: `ai-engine/reports/diagnostic_backtest_20260525_035649.txt`
|
||||||
|
- **Toplam playable**: 524 bet
|
||||||
|
- **Hit rate**: %54.77
|
||||||
|
- **ROI**: **−%16.73** (baseline kötü)
|
||||||
|
- **Grid-search'ten çıkan optimal filtreler (in-sample)**:
|
||||||
|
- MS: edge [-5%, +15%], V27 AGREE zorunlu → +%8.23 (21 bet)
|
||||||
|
- OU25: odds ≥ 1.80, edge ≤ +15% → +%28.91 (20 bet)
|
||||||
|
- BTTS: tüm config'lerde kayıp → MUTE
|
||||||
|
- **Aggregate optimize**: 95 bet, ROI +%2.16 (in-sample)
|
||||||
|
|
||||||
|
### Backtest #2 — Validation (2026-05-01 → 05-14, KOŞUYOR)
|
||||||
|
- **Bitince konum**: `ai-engine/reports/diagnostic_backtest_<yeni_timestamp>.{csv,json,txt}`
|
||||||
|
- **Karşılaştırma çalıştır**: `python scripts/compare_backtests.py` (otomatik en yeni 2'yi alır)
|
||||||
|
- **Beklenen sonuç**: ROI ≥ 0 → out-of-sample doğrulama BAŞARILI; in-sample overfit değil
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ❓ Backtest BİTTİĞİNDE yapılacak (yeni session'da bu kısımdan başla)
|
||||||
|
|
||||||
|
### 1. Sonucu oku
|
||||||
|
```powershell
|
||||||
|
cd C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\ai-engine
|
||||||
|
Get-ChildItem reports\diagnostic_backtest_*.txt | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Karşılaştır
|
||||||
|
```powershell
|
||||||
|
python scripts\compare_backtests.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Bu otomatik en yeni 2 backtest'i karşılaştırır, **VERDICT** verir:
|
||||||
|
- ✅ "FILTERS WORK" → ROI pozitif AND improved
|
||||||
|
- 🟡 "PARTIAL" → improved ama hâlâ negatif
|
||||||
|
- ❌ "OVERFITTING" → validation ROI collapse
|
||||||
|
|
||||||
|
### 3. Karara göre 2 yol
|
||||||
|
|
||||||
|
**Eğer ROI ≥ +%2 ve overfit yok:**
|
||||||
|
- `/sc:design` ile UI/API contract → Sprint 1
|
||||||
|
- Sprint 1: top-5 skor + evidence panel + "why" cümlesi
|
||||||
|
- Test edip prod'a aç
|
||||||
|
|
||||||
|
**Eğer ROI negatif veya overfit:**
|
||||||
|
- `analyze_backtest_csv.py` ile loss diagnostic
|
||||||
|
- Hangi market hâlâ kötü → tighten filter veya mute
|
||||||
|
- Calibrator recalibrate (özellikle BTTS dışındakiler için yeni sample)
|
||||||
|
- Tekrar backtest
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ Bilinen açık problemler / sorular
|
||||||
|
|
||||||
|
1. **Coherence filter validate edilmedi production-side** — smoke test 20/20 ama gerçek production data ile karşılaştırma yok
|
||||||
|
2. **Lineup-overlap last-5 hesabı** — yazılmadı, requirements doc'ta F8 var
|
||||||
|
3. **Skor top-5 distribution** — Poisson zaten hesaplıyor, surface edilmedi (UI tarafı)
|
||||||
|
4. **"Why" cümlesi main_pick'te** — boş, doldurulması gerek
|
||||||
|
5. **Cards/Corners/RED CARD model** — yok, "henüz desteklenmiyor" placeholder ile bırak (kullanıcı onayladı: mevcut market'ler sağlamlaşsın)
|
||||||
|
6. **Orphan match_id 51 satır** — `prediction_runs` içinde, `matches`'ta yok. Sample noise, geçiştirilebilir.
|
||||||
|
7. **opening_value feeder bug** — `odds_movement_*` SQL yazıyor ama tüm değerler 0 (opening == closing). Feeder upstream sorun. Düşük öncelik.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚦 Yeni Claude session'ında ilk komut
|
||||||
|
|
||||||
|
```
|
||||||
|
Bu projeye yeni bağlandım. Lütfen aşağıdaki dosyayı oku ve bana proje durumunu özet ver:
|
||||||
|
|
||||||
|
C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\mds\SESSION_HANDOFF.md
|
||||||
|
|
||||||
|
Sonra validation backtest'in sonucuna bak:
|
||||||
|
- C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\ai-engine\reports\
|
||||||
|
içindeki en yeni diagnostic_backtest_*.txt dosyasını oku
|
||||||
|
- compare_backtests.py script'ini koş, verdict göster
|
||||||
|
- Verdict'e göre sonraki adımı öner
|
||||||
|
```
|
||||||
|
|
||||||
|
Buradan devam eder. Tüm context bu doc'ta + dosyalarda + DB'de.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ Requirements spec (sıkıştırılmış)
|
||||||
|
|
||||||
|
**Ürün**: UI-tetikli per-match analiz, bahis uzmanı seviyesi
|
||||||
|
**Trigger**: User tıklar, on-demand
|
||||||
|
**Output**: main_pick + value_pick + tüm market olasılıkları + tek HT/FT skoru + top-5 skor dağılımı + evidence panel
|
||||||
|
**Kapsam**: Mevcut market'ler sağlamlaştırılır, yeni market eklenmez (kullanıcı onayı)
|
||||||
|
**Quality bar**: Calibration sapması ±2-5pp per market, NaN yok, response <3sn
|
||||||
|
**Validation**: Out-of-sample backtest (1500 maç, May 1-14) — KOŞUYOR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**SON NOT**: Backtest'in TAMAMLANMASINI bekle (~22:00). Laptop'u kapatma. Bittiğinde OneDrive senkronize eder, başka makinede otomatik orada olur. Yeni session'da bu dosyayı oku, sonuçlara bak, devam et.
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
# Social Poster — Setup & Operations
|
||||||
|
|
||||||
|
Otomatik tahmin kartı üretip Twitter / Facebook / Instagram'a postlayan modül.
|
||||||
|
Cron her **10 dakikada bir** çalışır, **yaklaşan 10-60 dk içindeki maçları**
|
||||||
|
yakalar, AI Engine'den tahmin alır, 1080×1080 görsel üretir, caption üretir,
|
||||||
|
3 platforma post eder.
|
||||||
|
|
||||||
|
## 1) ENV değişkenleri
|
||||||
|
|
||||||
|
`.env`'ye ekle:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Master switch — false ise cron çalışmaz, manual endpoint'ler de boş döner.
|
||||||
|
SOCIAL_POSTER_ENABLED=true
|
||||||
|
|
||||||
|
# Hangi sporlar (virgüllü liste). Varsayılan: football,basketball
|
||||||
|
SOCIAL_POSTER_SPORTS=football,basketball
|
||||||
|
|
||||||
|
# Yaklaşan maç penceresi (dakika). Varsayılan 10-60.
|
||||||
|
SOCIAL_POSTER_WINDOW_MIN=10
|
||||||
|
SOCIAL_POSTER_WINDOW_MAX=60
|
||||||
|
|
||||||
|
# Tek cron koşusunda kaç maç post edilir (rate-limit koruması). Varsayılan 5.
|
||||||
|
SOCIAL_POSTER_MAX_PER_RUN=5
|
||||||
|
|
||||||
|
# Public base URL — Instagram media upload için fotoğrafın HTTPS'ten erişilebilir
|
||||||
|
# olması ŞART. Localhost ile IG çalışmaz; production domain veya ngrok kullan.
|
||||||
|
APP_BASE_URL=https://api.iddaai.com
|
||||||
|
|
||||||
|
# AI Engine URL (orchestrator)
|
||||||
|
AI_ENGINE_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# ─── Twitter / X ───
|
||||||
|
TWITTER_API_KEY=...
|
||||||
|
TWITTER_API_SECRET=...
|
||||||
|
TWITTER_ACCESS_TOKEN=...
|
||||||
|
TWITTER_ACCESS_SECRET=...
|
||||||
|
|
||||||
|
# ─── Meta (Facebook + Instagram) ───
|
||||||
|
META_PAGE_ACCESS_TOKEN=... # FB Page'in long-lived access token'ı
|
||||||
|
META_PAGE_ID=... # FB Page numeric ID
|
||||||
|
META_IG_USER_ID=... # IG Business account numeric ID
|
||||||
|
META_GRAPH_API_VERSION=v25.0 # opsiyonel
|
||||||
|
|
||||||
|
# ─── Caption AI (opsiyonel — yoksa template caption kullanılır) ───
|
||||||
|
ENABLE_GEMINI=true
|
||||||
|
GEMINI_API_KEY=...
|
||||||
|
GEMINI_MODEL=gemini-1.5-flash
|
||||||
|
|
||||||
|
# Veya local Ollama:
|
||||||
|
OLLAMA_BASE_URL=http://localhost:11434
|
||||||
|
SOCIAL_POSTER_OLLAMA_MODEL=llama3.1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) API anahtarlarını alma
|
||||||
|
|
||||||
|
### Twitter / X
|
||||||
|
1. https://developer.x.com → Project + App oluştur
|
||||||
|
2. App'ın "Keys and tokens" → "API Key", "API Secret" al
|
||||||
|
3. User authentication settings → "Read and write and Direct Message"
|
||||||
|
4. "Access Token and Secret" generate et (bu hesap adına post eder)
|
||||||
|
5. **Free tier**: 1500 tweet/ay, 50 post/24 saat — 10 dk'lık cron'la günde
|
||||||
|
~144 koşu × 5 post = 720 potansiyel post → free tier yetmez, **Basic plan
|
||||||
|
($200/ay)** lazım. Cron interval'i 30 dk'ya alıp 50/gün kalmak istersen
|
||||||
|
`@Cron("*/30 * * * *")` olarak değiştir.
|
||||||
|
|
||||||
|
### Meta (Facebook + Instagram)
|
||||||
|
1. https://developers.facebook.com → App oluştur (type: Business)
|
||||||
|
2. Facebook Page bağla (mevcut sayfan yoksa oluştur)
|
||||||
|
3. Instagram Business hesabını Facebook Page'e bağla
|
||||||
|
4. Graph API Explorer'dan **page access token** al (User token değil!)
|
||||||
|
5. Long-lived token'a çevir (60 gün geçerli, refresh edilebilir)
|
||||||
|
6. **Page ID**: `https://graph.facebook.com/me/accounts?access_token=...`
|
||||||
|
7. **IG User ID**: `graph.facebook.com/{pageId}?fields=instagram_business_account&access_token=...`
|
||||||
|
8. Required permissions: `pages_show_list`, `pages_manage_posts`,
|
||||||
|
`pages_read_engagement`, `instagram_basic`, `instagram_content_publish`
|
||||||
|
|
||||||
|
### Gemini (caption AI — opsiyonel)
|
||||||
|
- https://aistudio.google.com → API key (free tier yeterli, günde ~1500 istek)
|
||||||
|
- `ENABLE_GEMINI=true` + `GEMINI_API_KEY=...`
|
||||||
|
- Gemini yoksa template caption kullanılır (yine SEO'lu, sadece daha statik)
|
||||||
|
|
||||||
|
## 3) Test komutları
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Servisi başlat
|
||||||
|
npm run start:dev
|
||||||
|
|
||||||
|
# Health endpoint — auth gerekmez
|
||||||
|
curl http://localhost:3005/social-poster/health | jq
|
||||||
|
|
||||||
|
# Manuel preview (görsel + JSON) — superadmin token gerekir
|
||||||
|
curl -H "Authorization: Bearer $TOKEN" \
|
||||||
|
http://localhost:3005/social-poster/preview/<matchId>
|
||||||
|
|
||||||
|
# Görseli tarayıcıda direkt göster
|
||||||
|
open http://localhost:3005/social-poster/preview-png/<matchId>?token=$TOKEN
|
||||||
|
|
||||||
|
# Manuel post (tek maç, tüm platformlara) — superadmin token
|
||||||
|
curl -X POST -H "Authorization: Bearer $TOKEN" \
|
||||||
|
http://localhost:3005/social-poster/post/<matchId>
|
||||||
|
|
||||||
|
# Cron'u beklemeden full sweep koş — superadmin token
|
||||||
|
curl -X POST -H "Authorization: Bearer $TOKEN" \
|
||||||
|
http://localhost:3005/social-poster/run-now
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4) SEO özellikleri
|
||||||
|
|
||||||
|
### Image dosya adı (SEO)
|
||||||
|
Eskiden: `prediction_basketball_xyz12345_1716595200000.jpg` (opaque)
|
||||||
|
Yeni: `sampiyonlar-ligi-unicaja-malaga-vs-aek-20260525.jpg` (Google indexable)
|
||||||
|
|
||||||
|
### Yan dosya: metadata sidecar
|
||||||
|
Her görsel için aynı dizinde `.json`:
|
||||||
|
- `title`, `description`, `og:*`, `schema.org SportsEvent`, `picks[]`
|
||||||
|
- Sayfada `<head>` Open Graph + Twitter Cards bu dosyadan beslenir
|
||||||
|
- Schema.org markup zengin sonuç (Google rich snippet) sağlar
|
||||||
|
|
||||||
|
### Caption (SEO + hashtags)
|
||||||
|
Her post 12'ye kadar küratör hashtag içerir:
|
||||||
|
- Marka: `#MaçTahmini #İddaa #BugünMaç`
|
||||||
|
- Spor: `#Futbol #Basketbol #FutbolTahmin`
|
||||||
|
- Lig: `#SüperLig #PremierLeague #ŞampiyonlarLigi #EuroLeague #NBA`
|
||||||
|
- Bölge: `#Türkiye #İngiltere #İspanya`
|
||||||
|
- Takım: `#Galatasaray #Fenerbahçe`
|
||||||
|
- Gün: `#PazarTahmini #CumartesiTahmini`
|
||||||
|
- Market: `#AltÜst #KGVar #ÇifteŞans #MaçSonucu #Handikap`
|
||||||
|
|
||||||
|
LLM (Gemini) caption üretiyorsa hashtag'leri çıkarır; sistem kendi
|
||||||
|
hashtag set'ini ekler. Tutarlı index için tek kaynak.
|
||||||
|
|
||||||
|
## 5) İzleme
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health endpoint — periyodik monitor
|
||||||
|
curl http://localhost:3005/social-poster/health | jq
|
||||||
|
|
||||||
|
# Sample output:
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"sports": ["football", "basketball"],
|
||||||
|
"window_min_minutes": 10,
|
||||||
|
"window_max_minutes": 60,
|
||||||
|
"max_posts_per_run": 5,
|
||||||
|
"top_leagues_loaded": 42,
|
||||||
|
"posted_match_count": 137,
|
||||||
|
"last_run_at": "2026-05-25T03:10:00.123Z",
|
||||||
|
"last_run_result": { "posted": 4, "skipped": 1, "errors": 0 },
|
||||||
|
"twitter_available": true,
|
||||||
|
"meta_facebook_available": true,
|
||||||
|
"meta_instagram_available": true,
|
||||||
|
"ai_engine_url": "http://localhost:8000",
|
||||||
|
"app_base_url": "https://api.iddaai.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`posted_match_count` `storage/social-poster-posted.json`'dan okunur, son 500
|
||||||
|
match ID hafızada — aynı maçı 2 kere post etmez.
|
||||||
|
|
||||||
|
## 6) Rate limit ipuçları
|
||||||
|
|
||||||
|
| Platform | Free limit | Tedbir |
|
||||||
|
|---|---|---|
|
||||||
|
| Twitter | 50 post/24 saat | `SOCIAL_POSTER_MAX_PER_RUN=2` + cron `*/30` → günde ~96 |
|
||||||
|
| Facebook | ~200 post/saat (Page) | Default config rahat |
|
||||||
|
| Instagram | 25 post/24 saat | `MAX_PER_RUN=1` + cron `*/60` → günde 24, sınırın hemen altında |
|
||||||
|
|
||||||
|
IG en sıkı sınır — production için **IG ayrı cron'da daha seyrek post**
|
||||||
|
yapılması önerilir (kod henüz tek cron, ileride ayrılabilir).
|
||||||
|
|
||||||
|
## 7) Hangi maçlar seçilir?
|
||||||
|
|
||||||
|
`top_leagues.json` dosyasındaki league_id'ler içinden:
|
||||||
|
- Şu anda 10-60 dakika sonra başlayacak
|
||||||
|
- Daha önce post edilmemiş
|
||||||
|
- `sport: football, basketball` filtresi geçen
|
||||||
|
|
||||||
|
`top_leagues.json` yoksa **tüm liglerden** maç seçer (hacmi yüksek tutar).
|
||||||
|
Sadece premium ligler postlamak istersen dosyayı doldur.
|
||||||
|
|
||||||
|
## 8) Görsel formatı
|
||||||
|
|
||||||
|
- **Boyut**: 1080×1080 (Instagram square — Twitter da kabul ediyor)
|
||||||
|
- **Format**: JPEG, quality 94
|
||||||
|
- **Tema**: Sport'a göre değişir — football yeşil, basketball turuncu
|
||||||
|
- **İçerik**: Lig logosu + ülke bayrağı, takım logoları + adları, HT skor,
|
||||||
|
FT skor, top 3 tahmin (confidence ile), risk badge
|
||||||
|
|
||||||
|
Card layout `image-renderer.service.ts` içinde — value-pick yıldız ile
|
||||||
|
işaretli, scenario top 3 listelenir, footer alt'ta tarih + brand.
|
||||||
|
|
||||||
|
## 9) Sık sorular
|
||||||
|
|
||||||
|
**Q: Görseller nereye yazılıyor?**
|
||||||
|
`public/predictions/` (gitignored). ServeStatic ile `/predictions/<file>.jpg`
|
||||||
|
URL'inden erişilir.
|
||||||
|
|
||||||
|
**Q: Eski görseller temizleniyor mu?**
|
||||||
|
Hayır — manuel temizlik gerekir. Cron eklemek istersen `LimitResetterTask`
|
||||||
|
örneği var.
|
||||||
|
|
||||||
|
**Q: AI Engine çalışmıyorsa ne olur?**
|
||||||
|
Cron tahmin alamaz, log'a hata düşer, devam eder. Sonraki koşuda dener.
|
||||||
|
|
||||||
|
**Q: Bir maç 2 kere post ediliyor mu?**
|
||||||
|
Hayır — `postedMatchIds` set'i Match ID bazında dedup yapar, dosyaya yazılır
|
||||||
|
(restart-safe).
|
||||||
|
|
||||||
|
**Q: Caption Gemini olmadan ne kadar iyi?**
|
||||||
|
Template caption tüm bilgileri + 12 hashtag içerir. SEO açısından yeterli,
|
||||||
|
sadece anlatım daha statik. Gemini ile her post için özgün metin.
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
# Gereksinim Keşfi: Lig Etiketleme + Milli Takım / Dünya Kupası Desteği
|
||||||
|
|
||||||
|
> `/sc:brainstorm` çıktısı — REQUIREMENTS ONLY. Tasarım/kod sonraki adım (/sc:design, /sc:workflow).
|
||||||
|
> Tarih: 2026-06 · Kaynak: 10k backtest + canlı DB/API kanıtı.
|
||||||
|
|
||||||
|
## Doğrulanmış Gerçekler (kanıta dayalı, varsayım değil)
|
||||||
|
|
||||||
|
### Lig performansı
|
||||||
|
- 10k backtest 18 ligde BET üretti; ROI dağılımı: ~7 güçlü kârlı (+25%..+102%),
|
||||||
|
~4 başabaş, ~7 zararlı (−12%..−62%).
|
||||||
|
- `live_matches` distinct lig ≠ `qualified_leagues.json` (48 lig). live_matches'te
|
||||||
|
qualified olmayan ligler var → kullanıcının "gereksiz ligler" sezgisi DOĞRU.
|
||||||
|
- Lig isimleri backtest CSV'de boş; DB'den `leagues` tablosundan çözülür.
|
||||||
|
|
||||||
|
### Milli takım / Dünya Kupası (ÖNEMLİ — ilk hipotez ÇÜRÜDÜ)
|
||||||
|
- Milli takımlar DB'de VAR: Türkiye(9s2kpeunkes0g17l95r3t91j6, elo 1675),
|
||||||
|
Almanya(3l2t2db0c5ow2f7s7bhr6mij4, 1689), Kolombiya(1692), Andorra(1243).
|
||||||
|
ELO + matches_played (30-37) MEVCUT.
|
||||||
|
- Milli maç hacmi yüksek: DK Elemeler 645, Hazırlık Maçları Ülkeler 522,
|
||||||
|
Uluslar Ligi 148, Avrupa Şamp. Elemeleri 120 bitmiş maç. Ayrı ligler halinde.
|
||||||
|
- football_ai_features milli maçlar için %98 üretiliyor (196/200) — kulüpten yüksek.
|
||||||
|
- **KÖK SEBEP (canlı API ile kanıtlandı):** Sistem milli maçı tahmin EDİYOR
|
||||||
|
(MS olasılıkları + 9-10 market geliyor, data_quality MEDIUM 0.57-0.74). Sorun:
|
||||||
|
`betting_brain approved=0` — hiçbir market "oynanabilir" işaretlenmiyor. Ortak
|
||||||
|
flag `ai_features_inferred_from_history` → data_quality MEDIUM tavanı (0.74) +
|
||||||
|
lig qualified değil → brain eşikleri geçilemiyor. Yani "yetersiz veri" mesajı
|
||||||
|
aslında "brain güvenmiyor, BET yok" demek. Model/veri sorunu DEĞİL, gate/tuning sorunu.
|
||||||
|
|
||||||
|
## Kullanıcı Kararları (bu oturumda alındı)
|
||||||
|
- Lig filtresi: **"Hepsi görünsün ama etiketli"** (gizleme yok; güven rozeti: Yüksek/Orta/Düşük).
|
||||||
|
- Milli takım: başta "ayrı model" istendi; veri görülünce yön = mevcut motoru milli
|
||||||
|
maçlara uyarlamak (ayrı ML modeli gereksiz — ELO+feature+geçmiş zaten var).
|
||||||
|
- Öncelik: **önce lig etiketleme** (hazır veri), sonra milli takım.
|
||||||
|
- Dünya Kupası: hazırlık maçlarında test edilebilmeli (yakın takvim baskısı).
|
||||||
|
|
||||||
|
## Fonksiyonel Gereksinimler
|
||||||
|
|
||||||
|
### A. Lig Güven Etiketleme
|
||||||
|
- FR-A1: Her lig için backtest'e dayalı güven seviyesi (Yüksek/Orta/Düşük) hesaplanmalı
|
||||||
|
(metrik: BET ROI + örneklem sayısı; düşük örneklem = otomatik Düşük/Bilinmiyor).
|
||||||
|
- FR-A2: live_matches'teki maçlar lig güven rozetiyle gösterilmeli (gizlenmeden).
|
||||||
|
- FR-A3: Etiket kaynağı tek yerde (config/tablo) tutulmalı, backtest tazelendikçe güncellenebilmeli.
|
||||||
|
- FR-A4: Forward-test (Model Performansı) verisi biriktikçe etiketler canlı sonuçla doğrulanmalı.
|
||||||
|
|
||||||
|
### B. Milli Takım / Dünya Kupası Desteği
|
||||||
|
- FR-B1: Milli maçlarda da BET önerisi çıkabilmeli (şu an approved=0).
|
||||||
|
- FR-B2: `ai_features_inferred_from_history` flag'i olan milli maçlar için data_quality
|
||||||
|
tavanı / brain eşikleri milli-maça uygun kalibre edilmeli (kör gevşetme DEĞİL).
|
||||||
|
- FR-B3: Milli maç ligleri (DK Elemeler, Hazırlık Maçları Ülkeler, Uluslar Ligi, Avrupa
|
||||||
|
Şamp., Dünya Kupası) "tanınan" kapsama alınmalı (qualified benzeri).
|
||||||
|
- FR-B4: Hazırlık maçlarında uçtan uca test edilebilmeli (tahmin + forward-test kaydı).
|
||||||
|
- FR-B5: Milli maç kalibrasyonu ayrı izlenmeli (kulüple karışmasın) — Model Performansı
|
||||||
|
sayfasında "milli" kırılımı.
|
||||||
|
|
||||||
|
## Fonksiyonel Olmayan Gereksinimler
|
||||||
|
- NFR-1: Gerçek para — milli maç eşik değişiklikleri backtest/forward-test ile doğrulanmadan
|
||||||
|
canlı agresifleştirilmemeli.
|
||||||
|
- NFR-2: Lig etiketleme mevcut hacmi düşürmemeli (gizleme değil işaretleme).
|
||||||
|
- NFR-3: Değişiklikler additive; mevcut kulüp tahmin akışını bozmamalı.
|
||||||
|
|
||||||
|
## Açık Sorular (sonraki tasarım turunda netleşecek)
|
||||||
|
- OQ-1: Lig güven eşikleri tam olarak ne? (örn. Yüksek = ROI>+10% & N≥30 BET)
|
||||||
|
- OQ-2: Milli maçlar için brain eşiği nasıl ayarlanacak — ayrı tier mi, data_quality
|
||||||
|
flag istisnası mı? Önce backtest: milli maçlarda mevcut motor kaç BET/ne ROI verirdi
|
||||||
|
(eşik gevşetilse)? Bu ölçülmeden tuning yapılmamalı.
|
||||||
|
- OQ-3: Etiket nerede saklanacak: yeni tablo mı, mevcut league_tiers mı, config mi?
|
||||||
|
- OQ-4: Dünya Kupası grup maçlarında lineup geç gelir — probable_xi cezası milli maçta
|
||||||
|
nasıl ele alınacak?
|
||||||
|
|
||||||
|
## Sonraki Adım
|
||||||
|
1. (Önce) Lig güven etiketleme → /sc:design veya doğrudan uygulama (veri hazır).
|
||||||
|
2. (Sonra) Milli maç backtest'i: eşik gevşetildiğinde milli maçlarda ROI ne? → ona göre tuning.
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Milli Takım / Dünya Kupası — Bahis Stratejisi (Veri-Temelli)
|
||||||
|
|
||||||
|
> Kaynak: 2.300 maçlık milli backtest (multi_backtest_20260602, /tmp/bt_natl.csv).
|
||||||
|
> Tüm rakamlar offline simülasyon (production'a dokunulmadan, aynı veride kural testi).
|
||||||
|
> Tarih: 2026-06.
|
||||||
|
|
||||||
|
## Temel Bulgular (kanıtlanmış)
|
||||||
|
1. **Kalibrasyon İYİ** (MS ECE 1.6, OU15 2.2) — model olasılıkları milli maçta da doğru.
|
||||||
|
Sorun kalibrasyon değil EDGE. Yani piyasa oranları da keskin; avantaj sadece
|
||||||
|
belirli segmentlerde var.
|
||||||
|
2. **Sadece MS market'inde edge var.** OU/BTTS/HT/DC/OE hepsi "bet-all" ROI
|
||||||
|
−12%..−21% — milli maçta gol/skor marketleri güvenilmez, KAPATILMALI.
|
||||||
|
3. **MS'te edge oran bandına + rekabet türüne bağlı:**
|
||||||
|
- Favori (oran<3): zararlı (−10..−18%). Milli favoriler takılır (rotasyon/motivasyon).
|
||||||
|
- Denk-üstü (oran 4-7): ELEME/HAZIRLIK'ta kârlı, TURNUVA'da zararlı.
|
||||||
|
4. **Rekabet türü kritik faktör** (DB'de feature YOK, lig adından türetilir):
|
||||||
|
HAZIRLIK / ELEME / TURNUVA çok farklı davranır.
|
||||||
|
|
||||||
|
## Grid + Kararlılık Testi (overfit'e karşı)
|
||||||
|
En iyi kombolar (N>=150, MS market):
|
||||||
|
| kural | N | hit% | ROI |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 4.0-7.0 sadece ELEME | 585 | 25% | +23.1% |
|
||||||
|
| 3.5-6.0 HAZ+ELE | 1021 | 25% | +14.5% |
|
||||||
|
| **4.0-7.0 HAZ+ELE (SEÇİLEN)** | **865** | **24%** | **+17.1%** |
|
||||||
|
| 3.0-6.0 HAZ+ELE | 1381 | 25% | +10.1% |
|
||||||
|
|
||||||
|
**Kararlılık (en güçlü kanıt):** "4-7 sadece ELEME" eski yarı +22.1% / yeni yarı +24.0%
|
||||||
|
→ iki bağımsız zaman diliminde de pozitif = overfit DEĞİL, sahada tutar.
|
||||||
|
|
||||||
|
## TURNUVA/FİNAL farkı (Dünya Kupası finalleri için kritik)
|
||||||
|
Turnuva (Avrupa Şamp, Copa America, Uluslar Ligi, Gold Cup, Asya/Afrika Kupası):
|
||||||
|
- 4-7 bandı turnuvada ZARARLI (−8.9%) — elemenin tersi.
|
||||||
|
- Sadece underdog 5+ kârlı (+51% ama n=274, oynak, şans payı yüksek).
|
||||||
|
- Sebep: büyük turnuva finallerinde favoriler tutarlı, sürpriz az.
|
||||||
|
|
||||||
|
## SEÇİLEN STRATEJİ (kullanıcı kararı)
|
||||||
|
**Milli-maç gate kuralı:**
|
||||||
|
- Market: SADECE MS (diğer tüm marketler milli maçta kapalı)
|
||||||
|
- Oran bandı: 4.0 ≤ odds < 7.0
|
||||||
|
- Rekabet türü: SADECE Hazırlık + Eleme
|
||||||
|
- TURNUVA/FİNAL: bahis ÖNERME (sadece analiz/olasılık göster). Underdog +51%
|
||||||
|
cazip ama oynak/az-örneklem → gerçek paraya bağlanmadı (kullanıcı kararı).
|
||||||
|
Beklenen: +17% ROI, ~865 bahis/2300 maç. Mevcut gate +0.9% idi → ~19x iyileşme.
|
||||||
|
|
||||||
|
## Mimari Notu (uygulama için)
|
||||||
|
- Sorun model değil → ayrı ML modeli GEREKSİZ (1898 maç zaten overfit riski; karar verildi: kurma).
|
||||||
|
- Çözüm = betting brain'de milli-maça özel GATE (eğitim-sonrası kural katmanı).
|
||||||
|
- Rekabet türü lig adından türetilir: 'hazırlık'→HAZIRLIK, 'eleme/play-off'→ELEME,
|
||||||
|
diğer→TURNUVA. Milli lig tespiti: qualified_leagues.json'a eklenen 21 milli lig.
|
||||||
|
- Kalıcı feature olarak rekabet türü eklenebilir (daha temiz) ama gate hardcode de yeter.
|
||||||
|
|
||||||
|
## Durum: UYGULANDI + DOĞRULANDI (betting_brain v31f-national-regime).
|
||||||
|
Kod:
|
||||||
|
- utils/national_leagues.py — loader (data/national_leagues.json, 21 lig) + classify_competition
|
||||||
|
- single_match_orchestrator.py — self.national_leagues yüklenir
|
||||||
|
- orchestrator/market_board.py — match_info.is_national + competition_type; _is_national_match/_competition_type_for helpers
|
||||||
|
- betting_brain.py _judge_row — national regime bloğu: is_national ise club mantığını override eder,
|
||||||
|
SADECE MS + 4.0-7.0 + (HAZIRLIK|ELEME) → BET (NATIONAL_BASE_SCORE 66, stake 0.5u, grade B),
|
||||||
|
diğer her şey REJECT. Hard-safety vetoları (low_reliability_hard, v25_v27_hard, htft_reversal)
|
||||||
|
national'da da geçerli. Rich analiz payload korunur.
|
||||||
|
|
||||||
|
DOĞRULAMA (V2 backtest, yeni gate aktif, 1829 maç, /tmp/bt_natl_v2.csv):
|
||||||
|
BET=784 → TAMAMI MS, oran 4.00-6.99 (bant dışı 0 bahis), hit %23.7, ROI +16.0%, +125.7u.
|
||||||
|
Simülasyondaki +17% ile birebir. OU/BTTS/HT/turnuva artık 0 BET.
|
||||||
|
|
||||||
|
NOT: ai-engine ~10:10'da restart oldu (compose) → national-gate + V31e recal + league_confidence
|
||||||
|
kodu CANLI API'de aktif. Ama bunlar docker cp ile deploy edildi; kalıcılık için repo commit +
|
||||||
|
image rebuild gerekir (yeni container build'inde kaybolur).
|
||||||
|
## İlgili: 422 lig-gate düzeltmesi CANLIDA (qualified_leagues 48→69, milli ligler açıldı).
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@prisma/client": "^6.19.3",
|
"@prisma/client": "^6.19.3",
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.66.4",
|
"bullmq": "^5.66.4",
|
||||||
@@ -49,6 +50,8 @@
|
|||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"satori": "^0.26.0",
|
||||||
|
"satori-html": "^0.3.2",
|
||||||
"twitter-api-v2": "^1.29.0",
|
"twitter-api-v2": "^1.29.0",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
@@ -3912,12 +3915,243 @@
|
|||||||
"@redis/client": "^1.0.0"
|
"@redis/client": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@resvg/resvg-js": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@resvg/resvg-js-android-arm-eabi": "2.6.2",
|
||||||
|
"@resvg/resvg-js-android-arm64": "2.6.2",
|
||||||
|
"@resvg/resvg-js-darwin-arm64": "2.6.2",
|
||||||
|
"@resvg/resvg-js-darwin-x64": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-arm64-musl": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-x64-gnu": "2.6.2",
|
||||||
|
"@resvg/resvg-js-linux-x64-musl": "2.6.2",
|
||||||
|
"@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
|
||||||
|
"@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
|
||||||
|
"@resvg/resvg-js-win32-x64-msvc": "2.6.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-android-arm-eabi": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-android-arm64": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-darwin-arm64": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-darwin-x64": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-arm64-gnu": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-arm64-musl": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-x64-gnu": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-linux-x64-musl": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-win32-arm64-msvc": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-win32-ia32-msvc": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@resvg/resvg-js-win32-x64-msvc": {
|
||||||
|
"version": "2.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
|
||||||
|
"integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@scarf/scarf": {
|
"node_modules/@scarf/scarf": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||||
"hasInstallScript": true
|
"hasInstallScript": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@shuding/opentype.js": {
|
||||||
|
"version": "1.4.0-beta.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
|
||||||
|
"integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fflate": "^0.7.3",
|
||||||
|
"string.prototype.codepointat": "^0.2.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ot": "bin/ot"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@sinclair/typebox": {
|
"node_modules/@sinclair/typebox": {
|
||||||
"version": "0.34.47",
|
"version": "0.34.47",
|
||||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz",
|
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz",
|
||||||
@@ -6535,6 +6769,15 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/camelize": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001762",
|
"version": "1.0.30001762",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
|
||||||
@@ -7086,6 +7329,36 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-background-parser": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/css-box-shadow": {
|
||||||
|
"version": "1.0.0-3",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
|
||||||
|
"integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/css-color-keywords": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-gradient-parser": {
|
||||||
|
"version": "0.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz",
|
||||||
|
"integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-select": {
|
"node_modules/css-select": {
|
||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
@@ -7102,6 +7375,17 @@
|
|||||||
"url": "https://github.com/sponsors/fb55"
|
"url": "https://github.com/sponsors/fb55"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-to-react-native": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camelize": "^1.0.0",
|
||||||
|
"css-color-keywords": "^1.0.0",
|
||||||
|
"postcss-value-parser": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-what": {
|
"node_modules/css-what": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
@@ -7433,6 +7717,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex-xs": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/empathic": {
|
"node_modules/empathic": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
|
||||||
@@ -8152,6 +8445,12 @@
|
|||||||
"node": "^12.20 || >= 14.13"
|
"node": "^12.20 || >= 14.13"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fflate": {
|
||||||
|
"version": "0.7.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
|
||||||
|
"integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/file-entry-cache": {
|
"node_modules/file-entry-cache": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
||||||
@@ -8794,6 +9093,18 @@
|
|||||||
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/hex-rgb": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/hookified": {
|
"node_modules/hookified": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.0.tgz",
|
||||||
@@ -10097,6 +10408,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.33.tgz",
|
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.33.tgz",
|
||||||
"integrity": "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw=="
|
"integrity": "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/linebreak": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "0.0.8",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/linebreak/node_modules/base64-js": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lines-and-columns": {
|
"node_modules/lines-and-columns": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
|
||||||
@@ -11030,6 +11360,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/parent-module": {
|
"node_modules/parent-module": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||||
@@ -11042,6 +11378,16 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-css-color": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.1.4",
|
||||||
|
"hex-rgb": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parse-json": {
|
"node_modules/parse-json": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||||
@@ -11438,6 +11784,12 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-value-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prebuild-install": {
|
"node_modules/prebuild-install": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||||
@@ -12012,6 +12364,37 @@
|
|||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/satori": {
|
||||||
|
"version": "0.26.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/satori/-/satori-0.26.0.tgz",
|
||||||
|
"integrity": "sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA==",
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@shuding/opentype.js": "1.4.0-beta.0",
|
||||||
|
"css-background-parser": "^0.1.0",
|
||||||
|
"css-box-shadow": "1.0.0-3",
|
||||||
|
"css-gradient-parser": "^0.0.17",
|
||||||
|
"css-to-react-native": "^3.0.0",
|
||||||
|
"emoji-regex-xs": "^2.0.1",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"linebreak": "^1.1.0",
|
||||||
|
"parse-css-color": "^0.2.1",
|
||||||
|
"postcss-value-parser": "^4.2.0",
|
||||||
|
"yoga-layout": "^3.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/satori-html": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ultrahtml": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/schema-utils": {
|
"node_modules/schema-utils": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
|
||||||
@@ -12488,6 +12871,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string.prototype.codepointat": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/strip-ansi": {
|
"node_modules/strip-ansi": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
@@ -12858,6 +13247,12 @@
|
|||||||
"real-require": "^0.2.0"
|
"real-require": "^0.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tiny-inflate": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tinyexec": {
|
"node_modules/tinyexec": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
|
||||||
@@ -13260,6 +13655,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ultrahtml": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "7.18.2",
|
"version": "7.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
|
||||||
@@ -13274,6 +13675,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/unicode-trie": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^0.2.5",
|
||||||
|
"tiny-inflate": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/universalify": {
|
"node_modules/universalify": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
|
||||||
@@ -13858,6 +14269,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yoga-layout": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
"@nestjs/terminus": "^11.0.0",
|
"@nestjs/terminus": "^11.0.0",
|
||||||
"@nestjs/throttler": "^6.5.0",
|
"@nestjs/throttler": "^6.5.0",
|
||||||
"@prisma/client": "^6.19.3",
|
"@prisma/client": "^6.19.3",
|
||||||
|
"@resvg/resvg-js": "^2.6.2",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"bullmq": "^5.66.4",
|
"bullmq": "^5.66.4",
|
||||||
@@ -82,6 +83,8 @@
|
|||||||
"prisma": "^6.19.3",
|
"prisma": "^6.19.3",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"satori": "^0.26.0",
|
||||||
|
"satori-html": "^0.3.2",
|
||||||
"twitter-api-v2": "^1.29.0",
|
"twitter-api-v2": "^1.29.0",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
@@ -134,4 +137,4 @@
|
|||||||
"prisma": {
|
"prisma": {
|
||||||
"seed": "ts-node prisma/seed.ts"
|
"seed": "ts-node prisma/seed.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import json
|
|
||||||
|
|
||||||
targets = [
|
|
||||||
"bet_recommender.py", "score_calculator.py", "db.py", "upset_engine_v2.py",
|
|
||||||
"v20_ensemble.py", "v27_predictor.py", "betting_brain.py",
|
|
||||||
"single_match_orchestrator.py", "v26_shadow_engine.py"
|
|
||||||
]
|
|
||||||
|
|
||||||
d = json.load(open("pyright_main_errors.json", encoding="utf-16"))
|
|
||||||
for diag in d["generalDiagnostics"]:
|
|
||||||
if diag["severity"] == "error":
|
|
||||||
fname = diag["file"]
|
|
||||||
if any(t in fname for t in targets):
|
|
||||||
# Print safely encoding to ascii to avoid charmap errors
|
|
||||||
safe_fname = fname.split('ai-engine')[1].encode('ascii', 'ignore').decode()
|
|
||||||
safe_msg = diag["message"].encode('ascii', 'ignore').decode()
|
|
||||||
print(f"{safe_fname} L{diag['range']['start']['line']+1}: {safe_msg}")
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// BigInt serialization fix
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
const matchId = '30gnuehy43on5orc9n3sh8pw4'; // Valencia vs Celta Vigo - HT/FT test
|
|
||||||
|
|
||||||
async function getPrediction() {
|
|
||||||
console.log('🔮 VQWEN v3 PREDICTION');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
// Fetch match from database
|
|
||||||
const match = await prisma.liveMatch.findUnique({
|
|
||||||
where: { id: matchId },
|
|
||||||
include: {
|
|
||||||
homeTeam: true,
|
|
||||||
awayTeam: true,
|
|
||||||
league: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.log(`❌ Match not found: ${matchId}`);
|
|
||||||
await prisma.$disconnect();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n📊 ${match.homeTeam?.name} vs ${match.awayTeam?.name}`);
|
|
||||||
console.log(`🏆 League: ${match.league?.name}`);
|
|
||||||
console.log(`📅 Match Time: ${new Date(Number(match.mstUtc)).toISOString()}`);
|
|
||||||
console.log(`📍 Status: ${match.state} / ${match.substate}`);
|
|
||||||
|
|
||||||
// Check data availability
|
|
||||||
console.log(`\n📦 DATA CHECK:`);
|
|
||||||
console.log(` Odds: ${match.odds ? '✅' : '❌'}`);
|
|
||||||
console.log(` Lineups: ${match.lineups ? '✅' : '❌'}`);
|
|
||||||
console.log(` Sidelined: ${match.sidelined ? '✅' : '❌'}`);
|
|
||||||
console.log(` Referee: ${match.refereeName || 'N/A'}`);
|
|
||||||
|
|
||||||
// Send prediction request
|
|
||||||
const aiEngineUrl = 'http://localhost:8007';
|
|
||||||
const predictionUrl = `${aiEngineUrl}/v20plus/analyze/${matchId}`;
|
|
||||||
|
|
||||||
console.log(`\n🤖 Sending to AI Engine...`);
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const response = await axios.post(predictionUrl, {}, {
|
|
||||||
timeout: 120000,
|
|
||||||
});
|
|
||||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`✅ Prediction received in ${elapsed}s\n`);
|
|
||||||
|
|
||||||
const pkg = response.data;
|
|
||||||
|
|
||||||
// Print full JSON
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log('📊 FULL PREDICTION JSON:');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log(JSON.stringify(pkg, null, 2));
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log('🎯 PREDICTION SUMMARY:');
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
const dq = pkg.data_quality;
|
|
||||||
console.log(`\n📦 Data Quality: ${dq.label} (${dq.score})`);
|
|
||||||
console.log(` Home lineup: ${dq.home_lineup_count}`);
|
|
||||||
console.log(` Away lineup: ${dq.away_lineup_count}`);
|
|
||||||
console.log(` Source: ${dq.lineup_source}`);
|
|
||||||
|
|
||||||
const eb = pkg.engine_breakdown;
|
|
||||||
console.log(`\n📈 Engine Signals:`);
|
|
||||||
console.log(` Team: ${eb.team}%`);
|
|
||||||
console.log(` Player: ${eb.player}%`);
|
|
||||||
console.log(` Odds: ${eb.odds}%`);
|
|
||||||
console.log(` Referee: ${eb.referee}%`);
|
|
||||||
|
|
||||||
const mp = pkg.main_pick;
|
|
||||||
console.log(`\n🥇 Main Pick:`);
|
|
||||||
console.log(` Market: ${mp.market}`);
|
|
||||||
console.log(` Pick: ${mp.pick}`);
|
|
||||||
console.log(` Confidence: ${mp.confidence}%`);
|
|
||||||
console.log(` Odds: ${mp.odds}`);
|
|
||||||
console.log(` Edge: ${(mp.edge * 100).toFixed(2)}%`);
|
|
||||||
console.log(` Grade: ${mp.bet_grade}`);
|
|
||||||
console.log(` Playable: ${mp.playable}`);
|
|
||||||
console.log(` Stake: ${mp.stake_units} units`);
|
|
||||||
|
|
||||||
if (pkg.value_pick) {
|
|
||||||
const vp = pkg.value_pick;
|
|
||||||
console.log(`\n💎 Value Pick:`);
|
|
||||||
console.log(` Market: ${vp.market}`);
|
|
||||||
console.log(` Pick: ${vp.pick}`);
|
|
||||||
console.log(` Confidence: ${vp.confidence}%`);
|
|
||||||
console.log(` Odds: ${vp.odds}`);
|
|
||||||
console.log(` Edge: ${(vp.edge * 100).toFixed(2)}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const sp = pkg.score_prediction;
|
|
||||||
console.log(`\n⚽ Score Prediction:`);
|
|
||||||
console.log(` FT: ${sp.ft}`);
|
|
||||||
console.log(` HT: ${sp.ht}`);
|
|
||||||
console.log(` xG: ${sp.xg_home} - ${sp.xg_away} (Total: ${sp.xg_total})`);
|
|
||||||
|
|
||||||
console.log(`\n🎲 Top 5 Scores:`);
|
|
||||||
pkg.scenario_top5.forEach((s: any, i: number) => {
|
|
||||||
console.log(` ${i + 1}. ${s.score} (${s.prob}%)`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const risk = pkg.risk;
|
|
||||||
console.log(`\n⚠️ Risk: ${risk.level} (${risk.score}/10)`);
|
|
||||||
if (risk.warnings?.length > 0) {
|
|
||||||
console.log(` Warnings: ${risk.warnings.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n💬 Reasoning:`);
|
|
||||||
pkg.reasoning_factors.forEach((f: string) => console.log(` - ${f}`));
|
|
||||||
|
|
||||||
if (pkg.ai_commentary) {
|
|
||||||
console.log(`\n💬 AI Commentary:`);
|
|
||||||
console.log(` ${pkg.ai_commentary}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HT/FT specific check
|
|
||||||
console.log(`\n🔍 HT/FT CHECK:`);
|
|
||||||
const htft = pkg.market_board?.HTFT;
|
|
||||||
if (htft && htft.probs && Object.keys(htft.probs).length > 0) {
|
|
||||||
console.log(` ✅ HT/FT PROBS PRESENT:`);
|
|
||||||
Object.entries(htft.probs).forEach(([key, val]) => {
|
|
||||||
console.log(` ${key}: ${(val as number * 100).toFixed(2)}%`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Find best HT/FT
|
|
||||||
const best = Object.entries(htft.probs).reduce((a, b) => (b[1] as number) > (a[1] as number) ? b : a);
|
|
||||||
console.log(`\n 🎯 BEST HT/FT: ${best[0]} (${(best[1] as number * 100).toFixed(2)}%)`);
|
|
||||||
} else {
|
|
||||||
console.log(` ❌ HT/FT PROBS EMPTY`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrediction().catch(console.error);
|
|
||||||
@@ -46,5 +46,27 @@
|
|||||||
"722fdbecxzcq9788l6jqclzlw",
|
"722fdbecxzcq9788l6jqclzlw",
|
||||||
"8ey0ww2zsosdmwr8ehsorh6t7",
|
"8ey0ww2zsosdmwr8ehsorh6t7",
|
||||||
"32n2r9bl6x90psj0wa7bfs6vq",
|
"32n2r9bl6x90psj0wa7bfs6vq",
|
||||||
"59tpnfrwnvhnhzmnvfyug68hj"
|
"59tpnfrwnvhnhzmnvfyug68hj",
|
||||||
|
"cesdwwnxbc5fmajgroc0hqzy2",
|
||||||
|
"3aa4mumjl6zyetg6o9hwd5hhx",
|
||||||
|
"40yjcbx2sq6oq736iqqqczwt1",
|
||||||
|
"39q1hq42hxjfylxb7xpe9bvf9",
|
||||||
|
"cu0rmpyff5692eo06ltddjo8a",
|
||||||
|
"ax1yf4nlzqpcji4j8epdgx3zl",
|
||||||
|
"1gxlzw2ezkyeykhcaa5x8ozkk",
|
||||||
|
"gfskxsdituog2kqp9yiu7bzi",
|
||||||
|
"595nsvo7ykvoe690b1e4u5n56",
|
||||||
|
"68zplepppndhl8bfdvgy9vgu1",
|
||||||
|
"3a0j0giz3c3ajw9h59evv7lqt",
|
||||||
|
"emy1ibc8fu2l0fukh4vlu5xl5",
|
||||||
|
"2db0aw1duj2my9l5iey5gm6nq",
|
||||||
|
"cc5tzz23tryrfqbm2pbv0jill",
|
||||||
|
"8tddm56zbasf57jkkay4kbf11",
|
||||||
|
"2r1hqz453bn9ljzt53kdr2lwb",
|
||||||
|
"93i7thp7zi0ympyt6l8aa1r2i",
|
||||||
|
"45db8orh1qttbsqq9hqapmbit",
|
||||||
|
"ude9t6yj60lebbn356qzg4k4",
|
||||||
|
"9qzn8cs96sgtqmesa9gpfti23",
|
||||||
|
"ad8y7vdjhinfqv4wo8rod6dck",
|
||||||
|
"70excpe1synn9kadnbppahdn7"
|
||||||
]
|
]
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// BigInt serialization fix
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
const matchIds = [
|
|
||||||
'7cnm7h7qbsq2bbaxngusojh90',
|
|
||||||
'7lmrfu2k1e2uxprxfxgaevcb8',
|
|
||||||
'3ko3otchy41d28rzxfpvl3d3o'
|
|
||||||
];
|
|
||||||
|
|
||||||
async function getMatches() {
|
|
||||||
for (const matchId of matchIds) {
|
|
||||||
try {
|
|
||||||
const match = await prisma.liveMatch.findUnique({
|
|
||||||
where: { id: matchId },
|
|
||||||
include: {
|
|
||||||
homeTeam: true,
|
|
||||||
awayTeam: true,
|
|
||||||
league: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.log(`\n❌ Maç bulunamadı: ${matchId}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log(`📊 MAÇ: ${match.homeTeam?.name} vs ${match.awayTeam?.name}`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log(`ID: ${match.id}`);
|
|
||||||
console.log(`Lig: ${match.league?.name}`);
|
|
||||||
console.log(`Durum: ${match.state} / ${match.substate}`);
|
|
||||||
console.log(`Maç Zamanı (MS): ${match.mstUtc?.toString()}`);
|
|
||||||
console.log(`Hakem: ${match.refereeName || 'Bilinmiyor'}`);
|
|
||||||
console.log(`İlk 11 Var: ${match.lineups ? '✅' : '❌'}`);
|
|
||||||
console.log(`Sakat/Cezalı: ${match.sidelined ? '✅ Var' : '❌ Yok'}`);
|
|
||||||
|
|
||||||
// Lineups summary
|
|
||||||
if (match.lineups) {
|
|
||||||
const lineups = match.lineups as any;
|
|
||||||
if (lineups.home && lineups.home.xi) {
|
|
||||||
console.log(`\n🏠 EV SAHİBİ İLK 11 (${match.homeTeam?.name}):`);
|
|
||||||
lineups.home.xi.forEach((p: any) => {
|
|
||||||
console.log(` ${p.matchName} (${p.shirtNumber}) - ${p.position}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (lineups.away && lineups.away.xi) {
|
|
||||||
console.log(`\n✈️ DEPLASMAN İLK 11 (${match.awayTeam?.name}):`);
|
|
||||||
lineups.away.xi.forEach((p: any) => {
|
|
||||||
console.log(` ${p.matchName} (${p.shirtNumber}) - ${p.position}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Hata (${matchId}):`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatches();
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
// BigInt serialization fix
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
async function getMatch() {
|
|
||||||
try {
|
|
||||||
const match = await prisma.liveMatch.findUnique({
|
|
||||||
where: { id: '3kemwubzpmga0nwhtc0o0vgno' },
|
|
||||||
include: {
|
|
||||||
homeTeam: true,
|
|
||||||
awayTeam: true,
|
|
||||||
league: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.log('❌ Maç bulunamadı!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Maç bulundu:');
|
|
||||||
console.log(JSON.stringify(match, null, 2));
|
|
||||||
|
|
||||||
// Maç bilgilerini özetle
|
|
||||||
console.log('\n📊 MAÇ ÖZETİ:');
|
|
||||||
console.log('ID:', match.id);
|
|
||||||
console.log('Slug:', match.matchSlug);
|
|
||||||
console.log('Ev sahibi:', match.homeTeam?.name);
|
|
||||||
console.log('Deplasman:', match.awayTeam?.name);
|
|
||||||
console.log('Lig:', match.league?.name);
|
|
||||||
console.log('Durum:', match.status);
|
|
||||||
console.log('Spor:', match.sport);
|
|
||||||
console.log('Maç Zamanı (MS):', match.mstUtc?.toString());
|
|
||||||
console.log('Skor:', match.scoreHome, '-', match.scoreAway);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Hata:', error);
|
|
||||||
} finally {
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatch();
|
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// BigInt serialization fix
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
const matchIds = [
|
|
||||||
'7cnm7h7qbsq2bbaxngusojh90',
|
|
||||||
'7lmrfu2k1e2uxprxfxgaevcb8',
|
|
||||||
'3ko3otchy41d28rzxfpvl3d3o'
|
|
||||||
];
|
|
||||||
|
|
||||||
async function getMatches() {
|
|
||||||
for (const matchId of matchIds) {
|
|
||||||
try {
|
|
||||||
const match = await prisma.liveMatch.findUnique({
|
|
||||||
where: { id: matchId },
|
|
||||||
include: {
|
|
||||||
homeTeam: true,
|
|
||||||
awayTeam: true,
|
|
||||||
league: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.log(`\n❌ Maç bulunamadı: ${matchId}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log(`🏟️ ${match.homeTeam?.name} vs ${match.awayTeam?.name}`);
|
|
||||||
console.log(`📍 Lig: ${match.league?.name}`);
|
|
||||||
console.log(`📅 Maç Zamanı: ${new Date(Number(match.mstUtc)).toLocaleString('tr-TR')}`);
|
|
||||||
console.log(`👨⚖️ Hakem: ${match.refereeName || 'Bilinmiyor'}`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
// Lineups
|
|
||||||
if (match.lineups) {
|
|
||||||
const lineups = match.lineups as any;
|
|
||||||
|
|
||||||
// Home team
|
|
||||||
if (lineups.home && lineups.home.xi) {
|
|
||||||
console.log(`\n🏠 EV SAHİBİ İLK 11 (${match.homeTeam?.name}):`);
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
const goalscorers = lineups.home.xi.filter((p: any) => p.events?.some((e: any) => e.name === 'goal'));
|
|
||||||
const cards = lineups.home.xi.filter((p: any) => p.events?.some((e: any) => e.name === 'yellow-card' || e.name === 'red-card'));
|
|
||||||
const subs = lineups.home.xi.filter((p: any) => p.events?.some((e: any) => e.name === 'sub-off'));
|
|
||||||
|
|
||||||
lineups.home.xi.forEach((p: any) => {
|
|
||||||
const hasGoal = p.events?.some((e: any) => e.name === 'goal');
|
|
||||||
const hasCard = p.events?.some((e: any) => e.name === 'yellow-card' || e.name === 'red-card');
|
|
||||||
const marker = hasGoal ? ' ⚽' : hasCard ? ' 🟨' : '';
|
|
||||||
console.log(` ${p.matchName} (${p.shirtNumber}) - ${p.position}${marker}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (goalscorers.length > 0) {
|
|
||||||
console.log(` ⚽ Gol Edenler: ${goalscorers.map((p: any) => p.matchName).join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Away team
|
|
||||||
if (lineups.away && lineups.away.xi) {
|
|
||||||
console.log(`\n✈️ DEPLASMAN İLK 11 (${match.awayTeam?.name}):`);
|
|
||||||
console.log('-'.repeat(80));
|
|
||||||
lineups.away.xi.forEach((p: any) => {
|
|
||||||
const hasGoal = p.events?.some((e: any) => e.name === 'goal');
|
|
||||||
const hasCard = p.events?.some((e: any) => e.name === 'yellow-card' || e.name === 'red-card');
|
|
||||||
const marker = hasGoal ? ' ⚽' : hasCard ? ' 🟨' : '';
|
|
||||||
console.log(` ${p.matchName} (${p.shirtNumber}) - ${p.position}${marker}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sidelined
|
|
||||||
if (match.sidelined) {
|
|
||||||
const sidelined = match.sidelined as any;
|
|
||||||
const homeSidelined = sidelined.homeTeam?.totalSidelined || 0;
|
|
||||||
const awaySidelined = sidelined.awayTeam?.totalSidelined || 0;
|
|
||||||
console.log(`\n🏥 Sakat/Cezalı:`);
|
|
||||||
console.log(` Ev Sahibi: ${homeSidelined} oyuncu`);
|
|
||||||
console.log(` Deplasman: ${awaySidelined} oyuncu`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Hata (${matchId}):`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatches();
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// BigInt serialization fix
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
async function getMatches() {
|
|
||||||
const matches = await prisma.liveMatch.findMany({
|
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
in: [
|
|
||||||
'7cnm7h7qbsq2bbaxngusojh90',
|
|
||||||
'7lmrfu2k1e2uxprxfxgaevcb8',
|
|
||||||
'3ko3otchy41d28rzxfpvl3d3o'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
include: {
|
|
||||||
homeTeam: true,
|
|
||||||
awayTeam: true,
|
|
||||||
league: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
matches.forEach((match, idx) => {
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log(`MAÇ ${idx + 1}: ${match.homeTeam?.name} vs ${match.awayTeam?.name}`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log(`ID: ${match.id}`);
|
|
||||||
console.log(`Lig: ${match.league?.name} (${match.league?.countryId})`);
|
|
||||||
console.log(`Durum: ${match.state} / ${match.substate}`);
|
|
||||||
console.log(`Skor: ${match.scoreHome ?? '?'} - ${match.scoreAway ?? '?'}`);
|
|
||||||
console.log(`Hakem: ${match.refereeName || 'Bilinmiyor'}`);
|
|
||||||
console.log(`Lineups Tip: ${typeof match.lineups} | ${match.lineups ? 'VAR' : 'YOK'}`);
|
|
||||||
|
|
||||||
if (match.lineups) {
|
|
||||||
const lineups = match.lineups as any;
|
|
||||||
console.log(`Lineups Keys: ${Object.keys(lineups).join(', ')}`);
|
|
||||||
|
|
||||||
// Check structure
|
|
||||||
if (lineups.home) {
|
|
||||||
const homeXi = lineups.home.xi || lineups.home.stats || [];
|
|
||||||
console.log(`Ev Sahibi İlk 11: ${Array.isArray(homeXi) ? homeXi.length : 'N/A'} oyuncu`);
|
|
||||||
}
|
|
||||||
if (lineups.away) {
|
|
||||||
const awayXi = lineups.away.xi || lineups.away.stats || [];
|
|
||||||
console.log(`Deplasman İlk 11: ${Array.isArray(awayXi) ? awayXi.length : 'N/A'} oyuncu`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('');
|
|
||||||
});
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
getMatches();
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
"""
|
|
||||||
VQWEN v3 Model - Manual Prediction Script
|
|
||||||
Match ID: 558o1fq1vbfsi3m5gm4ekpyc4
|
|
||||||
Match: Kaiserslautern vs F. Düsseldorf
|
|
||||||
League: 2. Bundesliga
|
|
||||||
"""
|
|
||||||
|
|
||||||
import requests
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# AI Engine base URL
|
|
||||||
AI_ENGINE_URL = "http://127.0.0.1:8000"
|
|
||||||
MATCH_ID = "558o1fq1vbfsi3m5gm4ekpyc4"
|
|
||||||
|
|
||||||
def check_engine_health():
|
|
||||||
"""Check if AI Engine is running"""
|
|
||||||
try:
|
|
||||||
response = requests.get(f"{AI_ENGINE_URL}/health", timeout=5)
|
|
||||||
return response.status_code == 200
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def run_prediction():
|
|
||||||
"""Run VQWEN v3 prediction for the match"""
|
|
||||||
|
|
||||||
print("=" * 80)
|
|
||||||
print("🤖 VQWEN v3 MODEL - MANUEL TAHMİN SİSTEMİ")
|
|
||||||
print("=" * 80)
|
|
||||||
print(f"\n📊 Maç Bilgileri:")
|
|
||||||
print(f" ID: {MATCH_ID}")
|
|
||||||
print(f" Ev Sahibi: Kaiserslautern")
|
|
||||||
print(f" Deplasman: F. Düsseldorf")
|
|
||||||
print(f" Lig: 2. Bundesliga")
|
|
||||||
print(f" Maç Zamanı: 2026-04-01 (MS: 1775300400000)")
|
|
||||||
print(f" Hakem: D. Schlager")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Check engine health
|
|
||||||
print("🔍 AI Engine kontrol ediliyor...")
|
|
||||||
if not check_engine_health():
|
|
||||||
print("❌ AI Engine (Python FastAPI) çalışmıyor!")
|
|
||||||
print()
|
|
||||||
print("ℹ️ Lütfen AI Engine'i başlatın:")
|
|
||||||
print(" cd ai-engine")
|
|
||||||
print(" uvicorn main:app --host 0.0.0.0 --port 8000 --reload")
|
|
||||||
print()
|
|
||||||
print("📋 Alternatif olarak, maç verilerini hazırlayabilirim:")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Prepare match data for analysis
|
|
||||||
match_data = {
|
|
||||||
"match_id": MATCH_ID,
|
|
||||||
"home_team": "Kaiserslautern",
|
|
||||||
"away_team": "F. Düsseldorf",
|
|
||||||
"league": "2. Bundesliga",
|
|
||||||
"match_date_ms": "1775300400000",
|
|
||||||
"referee": "D. Schlager",
|
|
||||||
"odds": {
|
|
||||||
"MS_1": 2.13,
|
|
||||||
"MS_X": 3.23,
|
|
||||||
"MS_2": 2.34,
|
|
||||||
"Alt_2.5": 2.09,
|
|
||||||
"Ust_2.5": 1.38,
|
|
||||||
"KG_Var": 1.32,
|
|
||||||
"KG_Yok": 2.25
|
|
||||||
},
|
|
||||||
"lineups_available": True,
|
|
||||||
"sidelined_count": 0
|
|
||||||
}
|
|
||||||
|
|
||||||
print("✅ Maç verileri hazırlandı:")
|
|
||||||
print(json.dumps(match_data, indent=2, ensure_ascii=False))
|
|
||||||
print()
|
|
||||||
print("⚠️ Tahmin almak için AI Engine'in çalışması gerekiyor.")
|
|
||||||
print()
|
|
||||||
return
|
|
||||||
|
|
||||||
# If engine is running, call the analysis endpoint
|
|
||||||
print("✅ AI Engine çalışıyor!")
|
|
||||||
print()
|
|
||||||
print("🎯 Tahmin yapılıyor...")
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = requests.post(
|
|
||||||
f"{AI_ENGINE_URL}/v20plus/analyze/{MATCH_ID}",
|
|
||||||
json={},
|
|
||||||
timeout=60
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code == 200:
|
|
||||||
result = response.json()
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
print("📊 TAHMİN SONUÇLARI")
|
|
||||||
print("=" * 80)
|
|
||||||
|
|
||||||
# Main Pick
|
|
||||||
if 'main_pick' in result:
|
|
||||||
main = result['main_pick']
|
|
||||||
print(f"\n🎯 ANA TAHMİN:")
|
|
||||||
print(f" Market: {main.get('market', 'N/A')}")
|
|
||||||
print(f" Tahmin: {main.get('pick', 'N/A')}")
|
|
||||||
print(f" Oran: {main.get('odds', 'N/A')}")
|
|
||||||
print(f" Güven: {main.get('confidence', 0):.1f}%")
|
|
||||||
print(f" Olasılık: {main.get('probability', 0):.1f}%")
|
|
||||||
print(f" Bahis Derecesi: {main.get('bet_grade', 'N/A')}")
|
|
||||||
|
|
||||||
# Value Pick
|
|
||||||
if 'value_pick' in result:
|
|
||||||
value = result['value_pick']
|
|
||||||
print(f"\n💎 DEĞER TAHMİNİ:")
|
|
||||||
print(f" Market: {value.get('market', 'N/A')}")
|
|
||||||
print(f" Tahmin: {value.get('pick', 'N/A')}")
|
|
||||||
print(f" Oran: {value.get('odds', 'N/A')}")
|
|
||||||
print(f" Güven: {value.get('confidence', 0):.1f}%")
|
|
||||||
print(f" Edge: {value.get('edge', 0):.2f}")
|
|
||||||
|
|
||||||
# Score Prediction
|
|
||||||
if 'score_prediction' in result:
|
|
||||||
score = result['score_prediction']
|
|
||||||
print(f"\n⚽ SKOR TAHMİNİ:")
|
|
||||||
print(f" İlk Yarı: {score.get('ht', 'N/A')}")
|
|
||||||
print(f" Maç Sonu: {score.get('ft', 'N/A')}")
|
|
||||||
print(f" xG (Ev): {score.get('xg_home', 0):.2f}")
|
|
||||||
print(f" xG (Dep): {score.get('xg_away', 0):.2f}")
|
|
||||||
print(f" Toplam xG: {score.get('xg_total', 0):.2f}")
|
|
||||||
|
|
||||||
# Bet Summary
|
|
||||||
if 'bet_summary' in result:
|
|
||||||
print(f"\n📋 TÜM TAHMİNLER:")
|
|
||||||
for bet in result['bet_summary']:
|
|
||||||
print(f" • {bet.get('market', 'N/A')}: {bet.get('pick', 'N/A')} "
|
|
||||||
f"(Güven: {bet.get('calibrated_confidence', 0):.1f}%, "
|
|
||||||
f"Derece: {bet.get('bet_grade', 'N/A')})")
|
|
||||||
|
|
||||||
# AI Commentary
|
|
||||||
if 'ai_commentary' in result:
|
|
||||||
print(f"\n💬 AI YORUMU:")
|
|
||||||
print(f" {result['ai_commentary']}")
|
|
||||||
|
|
||||||
# Risk Assessment
|
|
||||||
if 'risk' in result:
|
|
||||||
risk = result['risk']
|
|
||||||
print(f"\n⚠️ RİSK DEĞERLENDİRMESİ:")
|
|
||||||
print(f" Seviye: {risk.get('level', 'N/A')}")
|
|
||||||
print(f" Skor: {risk.get('score', 0):.1f}")
|
|
||||||
if risk.get('warnings'):
|
|
||||||
print(f" Uyarılar: {', '.join(risk['warnings'][:3])}")
|
|
||||||
|
|
||||||
# Data Quality
|
|
||||||
if 'data_quality' in result:
|
|
||||||
quality = result['data_quality']
|
|
||||||
print(f"\n📊 VERİ KALİTESİ:")
|
|
||||||
print(f" Seviye: {quality.get('label', 'N/A')}")
|
|
||||||
print(f" Skor: {quality.get('score', 0):.1f}")
|
|
||||||
|
|
||||||
print("\n" + "=" * 80)
|
|
||||||
|
|
||||||
else:
|
|
||||||
print(f"❌ Hata: HTTP {response.status_code}")
|
|
||||||
print(f" {response.text}")
|
|
||||||
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
print("❌ Zaman aşımı! AI Engine yanıt vermiyor.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Hata: {str(e)}")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_prediction()
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { PrismaClient } from '@prisma/client';
|
|
||||||
import * as dotenv from 'dotenv';
|
|
||||||
import axios from 'axios';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
// BigInt serialization fix
|
|
||||||
(BigInt.prototype as any).toJSON = function () {
|
|
||||||
return this.toString();
|
|
||||||
};
|
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
|
||||||
|
|
||||||
const matchIds = [
|
|
||||||
'7cnm7h7qbsq2bbaxngusojh90', // Club Brugge vs Anderlecht - TESTED ✅
|
|
||||||
'7lmrfu2k1e2uxprxfxgaevcb8', // Castellon vs Granada
|
|
||||||
'3ko3otchy41d28rzxfpvl3d3o' // SV Ried vs Altach
|
|
||||||
];
|
|
||||||
|
|
||||||
async function getPrediction(matchId: string) {
|
|
||||||
try {
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log(`🔮 PREDICTION REQUEST: ${matchId}`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
|
|
||||||
// Fetch match from database
|
|
||||||
const match = await prisma.liveMatch.findUnique({
|
|
||||||
where: { id: matchId },
|
|
||||||
include: {
|
|
||||||
homeTeam: true,
|
|
||||||
awayTeam: true,
|
|
||||||
league: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
console.log(`❌ Match not found: ${matchId}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📊 ${match.homeTeam?.name} vs ${match.awayTeam?.name}`);
|
|
||||||
console.log(`🏆 League: ${match.league?.name}`);
|
|
||||||
console.log(`📅 Match Time: ${new Date(Number(match.mstUtc)).toISOString()}`);
|
|
||||||
|
|
||||||
// Send prediction request to AI Engine
|
|
||||||
const aiEngineUrl = 'http://localhost:8007';
|
|
||||||
const predictionUrl = `${aiEngineUrl}/v20plus/analyze/${matchId}`;
|
|
||||||
|
|
||||||
console.log(`\n🤖 Sending to AI Engine: ${predictionUrl}`);
|
|
||||||
|
|
||||||
const startTime = Date.now();
|
|
||||||
const response = await axios.post(predictionUrl, {}, {
|
|
||||||
timeout: 120000, // 2 minutes timeout
|
|
||||||
});
|
|
||||||
|
|
||||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
||||||
|
|
||||||
console.log(`✅ Prediction received in ${elapsed}s`);
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log(`📊 FULL PREDICTION JSON:`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log(JSON.stringify(response.data, null, 2));
|
|
||||||
|
|
||||||
// Summary
|
|
||||||
const pkg = response.data;
|
|
||||||
if (pkg.main_pick) {
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log(`🎯 SUMMARY:`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
console.log(`Main Pick: ${pkg.main_pick.market} → ${pkg.main_pick.pick}`);
|
|
||||||
console.log(`Confidence: ${pkg.main_pick.confidence}%`);
|
|
||||||
console.log(`Odds: ${pkg.main_pick.odds}`);
|
|
||||||
console.log(`Bet Grade: ${pkg.main_pick.bet_grade}`);
|
|
||||||
console.log(`Edge: ${pkg.main_pick.edge || 'N/A'}`);
|
|
||||||
|
|
||||||
if (pkg.value_pick) {
|
|
||||||
console.log(`\nValue Pick: ${pkg.value_pick.market} → ${pkg.value_pick.pick}`);
|
|
||||||
console.log(`Confidence: ${pkg.value_pick.confidence}%`);
|
|
||||||
console.log(`Odds: ${pkg.value_pick.odds}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkg.bet_advice) {
|
|
||||||
console.log(`\n💡 Bet Advice:`);
|
|
||||||
console.log(` Playable: ${pkg.bet_advice.playable}`);
|
|
||||||
console.log(` Stake: ${pkg.bet_advice.suggested_stake_units} units`);
|
|
||||||
console.log(` Reason: ${pkg.bet_advice.reason}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkg.score_prediction) {
|
|
||||||
console.log(`\n⚽ Score Prediction:`);
|
|
||||||
console.log(` FT: ${pkg.score_prediction.ft}`);
|
|
||||||
console.log(` HT: ${pkg.score_prediction.ht}`);
|
|
||||||
console.log(` xG: ${pkg.score_prediction.xg_home} - ${pkg.score_prediction.xg_away}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkg.risk) {
|
|
||||||
console.log(`\n⚠️ Risk Level: ${pkg.risk.level} (${pkg.risk.score})`);
|
|
||||||
if (pkg.risk.warnings?.length > 0) {
|
|
||||||
console.log(` Warnings: ${pkg.risk.warnings.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pkg.ai_commentary) {
|
|
||||||
console.log(`\n💬 AI Commentary:`);
|
|
||||||
console.log(` ${pkg.ai_commentary}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(`❌ Error for match ${matchId}:`);
|
|
||||||
if (error.response) {
|
|
||||||
console.error(` Status: ${error.response.status}`);
|
|
||||||
console.error(` Data: ${JSON.stringify(error.response.data, null, 2)}`);
|
|
||||||
} else {
|
|
||||||
console.error(` Message: ${error.message}`);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
console.log('🚀 VQWEN v3 Prediction Engine - Batch Analysis');
|
|
||||||
console.log(`📡 AI Engine: ${process.env.AI_ENGINE_URL || 'http://localhost:8007'}`);
|
|
||||||
console.log(`🎯 Matches: ${matchIds.length}`);
|
|
||||||
|
|
||||||
const results: { matchId: string; success: boolean }[] = [];
|
|
||||||
|
|
||||||
for (const matchId of matchIds) {
|
|
||||||
const result = await getPrediction(matchId);
|
|
||||||
if (result) {
|
|
||||||
results.push({ matchId, success: true });
|
|
||||||
} else {
|
|
||||||
results.push({ matchId, success: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Small delay between requests
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`\n${'='.repeat(80)}`);
|
|
||||||
console.log(`📊 BATCH SUMMARY:`);
|
|
||||||
console.log('='.repeat(80));
|
|
||||||
results.forEach((r, i) => {
|
|
||||||
console.log(`${r.success ? '✅' : '❌'} ${i + 1}. ${r.matchId}`);
|
|
||||||
});
|
|
||||||
console.log(`\nTotal: ${results.filter(r => r.success).length}/${results.length} successful`);
|
|
||||||
|
|
||||||
await prisma.$disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(console.error);
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Warms the R2 image bucket by requesting every known team / competition /
|
||||||
|
# country image once through the image-proxy Worker, which mirrors each one
|
||||||
|
# into R2 (see workers/image-proxy).
|
||||||
|
#
|
||||||
|
# Run on the production server (needs docker access to iddaai-postgres):
|
||||||
|
# ./warm-image-cache.sh https://files.example.com
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
BASE_URL="${1:?Usage: $0 <image-base-url>}"
|
||||||
|
BASE_URL="${BASE_URL%/}"
|
||||||
|
PSQL=(docker exec iddaai-postgres psql -U iddaai_user -d iddaai_db -At -c)
|
||||||
|
|
||||||
|
{
|
||||||
|
"${PSQL[@]}" "SELECT 'teams/' || id FROM teams"
|
||||||
|
"${PSQL[@]}" "SELECT 'competitions/' || id FROM leagues"
|
||||||
|
"${PSQL[@]}" "SELECT 'areas/' || id FROM countries"
|
||||||
|
} | xargs -P 8 -I{} curl -sS -o /dev/null -w "%{http_code}\n" "$BASE_URL/{}" \
|
||||||
|
| sort | uniq -c \
|
||||||
|
| awk '{printf "HTTP %s: %s istek\n", $2, $1}'
|
||||||
@@ -46,6 +46,7 @@ import { SocialPosterModule } from "./modules/social-poster/social-poster.module
|
|||||||
// Sports Domain Modules
|
// Sports Domain Modules
|
||||||
import { MatchesModule } from "./modules/matches/matches.module";
|
import { MatchesModule } from "./modules/matches/matches.module";
|
||||||
import { PredictionsModule } from "./modules/predictions/predictions.module";
|
import { PredictionsModule } from "./modules/predictions/predictions.module";
|
||||||
|
import { ValueBoardModule } from "./modules/value-board/value-board.module";
|
||||||
import { LeaguesModule } from "./modules/leagues/leagues.module";
|
import { LeaguesModule } from "./modules/leagues/leagues.module";
|
||||||
import { AnalysisModule } from "./modules/analysis/analysis.module";
|
import { AnalysisModule } from "./modules/analysis/analysis.module";
|
||||||
import { CouponsModule } from "./modules/coupons/coupons.module";
|
import { CouponsModule } from "./modules/coupons/coupons.module";
|
||||||
@@ -200,6 +201,7 @@ const historicalFeederMode = process.env.FEEDER_MODE === "historical";
|
|||||||
// Sports Domain Modules
|
// Sports Domain Modules
|
||||||
MatchesModule,
|
MatchesModule,
|
||||||
PredictionsModule,
|
PredictionsModule,
|
||||||
|
ValueBoardModule,
|
||||||
LeaguesModule,
|
LeaguesModule,
|
||||||
AnalysisModule,
|
AnalysisModule,
|
||||||
CouponsModule,
|
CouponsModule,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Central builder for team / competition / country image URLs.
|
||||||
|
*
|
||||||
|
* Images are served from a Cloudflare R2 bucket fronted by the
|
||||||
|
* `workers/image-proxy` Worker, which lazily mirrors each image from the
|
||||||
|
* upstream provider (file.mackolikfeeds.com) into R2 on first request.
|
||||||
|
*
|
||||||
|
* Set IMAGE_BASE_URL (no trailing slash, e.g. https://files.example.com)
|
||||||
|
* to serve from the Worker. When unset, URLs point directly at the
|
||||||
|
* upstream provider so behaviour is unchanged until the bucket is live.
|
||||||
|
*/
|
||||||
|
const DEFAULT_IMAGE_BASE_URL = "https://file.mackolikfeeds.com";
|
||||||
|
|
||||||
|
function imageBaseUrl(): string {
|
||||||
|
return (
|
||||||
|
process.env.IMAGE_BASE_URL?.replace(/\/+$/, "") || DEFAULT_IMAGE_BASE_URL
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function teamLogoUrl(teamId?: string | null): string | undefined {
|
||||||
|
if (!teamId) return undefined;
|
||||||
|
return `${imageBaseUrl()}/teams/${teamId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function competitionLogoUrl(
|
||||||
|
competitionId?: string | null,
|
||||||
|
): string | undefined {
|
||||||
|
if (!competitionId) return undefined;
|
||||||
|
return `${imageBaseUrl()}/competitions/${competitionId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countryFlagUrl(countryId?: string | null): string | undefined {
|
||||||
|
if (!countryId) return undefined;
|
||||||
|
return `${imageBaseUrl()}/areas/${countryId}`;
|
||||||
|
}
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { existsSync, createWriteStream, mkdirSync } from "fs";
|
|
||||||
import { dirname } from "path";
|
|
||||||
import axios from "axios";
|
|
||||||
import { Logger } from "@nestjs/common";
|
|
||||||
|
|
||||||
export class ImageUtils {
|
|
||||||
private static readonly logger = new Logger("ImageUtils");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads an image from a URL and saves it to a local path.
|
|
||||||
* Skips download if file already exists.
|
|
||||||
*/
|
|
||||||
static async downloadImage(url: string, localPath: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
// Check if file exists
|
|
||||||
if (existsSync(localPath)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
const dir = dirname(localPath);
|
|
||||||
if (!existsSync(dir)) {
|
|
||||||
mkdirSync(dir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download
|
|
||||||
const response = await axios({
|
|
||||||
url,
|
|
||||||
method: "GET",
|
|
||||||
responseType: "stream",
|
|
||||||
timeout: 5000,
|
|
||||||
validateStatus: (status) => status === 200, // Only save if 200 OK
|
|
||||||
});
|
|
||||||
|
|
||||||
const writer = createWriteStream(localPath);
|
|
||||||
|
|
||||||
response.data.pipe(writer);
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
writer.on("finish", () => resolve(true));
|
|
||||||
writer.on("error", (err) => {
|
|
||||||
this.logger.warn(
|
|
||||||
`Failed to write image to ${localPath}: ${err.message}`,
|
|
||||||
);
|
|
||||||
reject(new Error(`Failed to write image to ${localPath}`));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error: any) {
|
|
||||||
// Log warning but don't break the application
|
|
||||||
// 404s are common for missing logos
|
|
||||||
if (error.response?.status !== 404) {
|
|
||||||
this.logger.warn(
|
|
||||||
`Failed to download image from ${url}: ${error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -394,4 +394,202 @@ export class AdminController {
|
|||||||
predictions: totalPredictions,
|
predictions: totalPredictions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================== Model Performance (Forward-Test) ==================
|
||||||
|
|
||||||
|
@Get("model-performance")
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
"Per-market calibration (model% vs actual%), ROI and decision rationale " +
|
||||||
|
"from settled prediction_runs. Powers the admin Model Performance page.",
|
||||||
|
})
|
||||||
|
@SwaggerResponse({ status: 200, schema: { type: "object" } })
|
||||||
|
async getModelPerformance(
|
||||||
|
@Query("days") daysRaw?: string,
|
||||||
|
): Promise<ApiResponse<ModelPerformanceResult>> {
|
||||||
|
const days = Math.min(Math.max(Number(daysRaw) || 90, 1), 1000);
|
||||||
|
const sinceMs = Date.now() - days * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// Pull settled rows in window. markets_settled holds one entry per market.
|
||||||
|
const rows = await this.prisma.$queryRawUnsafe<
|
||||||
|
Array<{ payload_summary: unknown; generated_at: Date }>
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
SELECT pr.payload_summary, pr.generated_at
|
||||||
|
FROM prediction_runs pr
|
||||||
|
WHERE pr.eventual_outcome IS NOT NULL
|
||||||
|
AND pr.generated_at >= $1
|
||||||
|
AND pr.payload_summary -> 'settlement' -> 'markets_settled' IS NOT NULL
|
||||||
|
ORDER BY pr.generated_at DESC
|
||||||
|
LIMIT 50000
|
||||||
|
`,
|
||||||
|
new Date(sinceMs),
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Aggregate per market ──────────────────────────────────────────
|
||||||
|
type Acc = {
|
||||||
|
market: string;
|
||||||
|
n: number;
|
||||||
|
wins: number;
|
||||||
|
sumShown: number; // Σ shown_confidence (0–100)
|
||||||
|
shownCount: number;
|
||||||
|
// 10-bin reliability for ECE
|
||||||
|
bins: Array<{ sumP: number; sumY: number; n: number }>;
|
||||||
|
// betting (only playable BET rows with odds)
|
||||||
|
betN: number;
|
||||||
|
betWins: number;
|
||||||
|
betProfit: number;
|
||||||
|
betStake: number;
|
||||||
|
// rationale tally
|
||||||
|
actions: Record<string, number>;
|
||||||
|
tiers: Record<string, number>;
|
||||||
|
};
|
||||||
|
const acc = new Map<string, Acc>();
|
||||||
|
const ensure = (mk: string): Acc => {
|
||||||
|
let a = acc.get(mk);
|
||||||
|
if (!a) {
|
||||||
|
a = {
|
||||||
|
market: mk,
|
||||||
|
n: 0,
|
||||||
|
wins: 0,
|
||||||
|
sumShown: 0,
|
||||||
|
shownCount: 0,
|
||||||
|
bins: Array.from({ length: 10 }, () => ({ sumP: 0, sumY: 0, n: 0 })),
|
||||||
|
betN: 0,
|
||||||
|
betWins: 0,
|
||||||
|
betProfit: 0,
|
||||||
|
betStake: 0,
|
||||||
|
actions: {},
|
||||||
|
tiers: {},
|
||||||
|
};
|
||||||
|
acc.set(mk, a);
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
|
||||||
|
let totalSettledMarkets = 0;
|
||||||
|
for (const row of rows) {
|
||||||
|
const summary =
|
||||||
|
row.payload_summary && typeof row.payload_summary === "object"
|
||||||
|
? (row.payload_summary as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
const settlement =
|
||||||
|
summary.settlement && typeof summary.settlement === "object"
|
||||||
|
? (summary.settlement as Record<string, unknown>)
|
||||||
|
: {};
|
||||||
|
const markets = Array.isArray(settlement.markets_settled)
|
||||||
|
? (settlement.markets_settled as Array<Record<string, unknown>>)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
for (const m of markets) {
|
||||||
|
const market = typeof m.market === "string" ? m.market : "";
|
||||||
|
if (!market) continue;
|
||||||
|
const won = m.won === true;
|
||||||
|
const shown =
|
||||||
|
m.shown_confidence != null ? Number(m.shown_confidence) : null;
|
||||||
|
const a = ensure(market);
|
||||||
|
a.n += 1;
|
||||||
|
if (won) a.wins += 1;
|
||||||
|
totalSettledMarkets += 1;
|
||||||
|
|
||||||
|
if (shown != null && Number.isFinite(shown)) {
|
||||||
|
a.sumShown += shown;
|
||||||
|
a.shownCount += 1;
|
||||||
|
const p = Math.min(Math.max(shown / 100, 0), 0.999999);
|
||||||
|
const bi = Math.min(9, Math.floor(p * 10));
|
||||||
|
a.bins[bi].sumP += p;
|
||||||
|
a.bins[bi].sumY += won ? 1 : 0;
|
||||||
|
a.bins[bi].n += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const odds = m.odds != null ? Number(m.odds) : null;
|
||||||
|
const isBet = m.playable === true && m.action === "BET";
|
||||||
|
if (isBet && odds != null && Number.isFinite(odds) && odds > 1.01) {
|
||||||
|
a.betN += 1;
|
||||||
|
if (won) {
|
||||||
|
a.betWins += 1;
|
||||||
|
a.betProfit += odds - 1;
|
||||||
|
} else {
|
||||||
|
a.betProfit -= 1;
|
||||||
|
}
|
||||||
|
a.betStake += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const action = typeof m.action === "string" ? m.action : "—";
|
||||||
|
a.actions[action] = (a.actions[action] ?? 0) + 1;
|
||||||
|
const tier = typeof m.value_tier === "string" ? m.value_tier : "—";
|
||||||
|
a.tiers[tier] = (a.tiers[tier] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const markets = Array.from(acc.values())
|
||||||
|
.map((a) => {
|
||||||
|
const actualPct = a.n > 0 ? (a.wins / a.n) * 100 : 0;
|
||||||
|
const shownPct = a.shownCount > 0 ? a.sumShown / a.shownCount : 0;
|
||||||
|
// ECE: Σ |acc_bin - conf_bin| * (n_bin / N)
|
||||||
|
let ece = 0;
|
||||||
|
for (const b of a.bins) {
|
||||||
|
if (b.n === 0) continue;
|
||||||
|
const conf = b.sumP / b.n;
|
||||||
|
const acc2 = b.sumY / b.n;
|
||||||
|
ece += Math.abs(acc2 - conf) * (b.n / a.shownCount || 0);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
market: a.market,
|
||||||
|
samples: a.n,
|
||||||
|
shown_pct: Number(shownPct.toFixed(1)),
|
||||||
|
actual_pct: Number(actualPct.toFixed(1)),
|
||||||
|
gap: Number((shownPct - actualPct).toFixed(1)),
|
||||||
|
ece: Number((ece * 100).toFixed(1)),
|
||||||
|
calibration: (Math.abs(shownPct - actualPct) <= 4
|
||||||
|
? "good"
|
||||||
|
: shownPct > actualPct
|
||||||
|
? "overconfident"
|
||||||
|
: "underconfident") as
|
||||||
|
| "good"
|
||||||
|
| "overconfident"
|
||||||
|
| "underconfident",
|
||||||
|
bet_count: a.betN,
|
||||||
|
bet_hit_pct:
|
||||||
|
a.betN > 0 ? Number(((a.betWins / a.betN) * 100).toFixed(1)) : 0,
|
||||||
|
bet_roi_pct:
|
||||||
|
a.betStake > 0
|
||||||
|
? Number(((a.betProfit / a.betStake) * 100).toFixed(1))
|
||||||
|
: 0,
|
||||||
|
actions: a.actions,
|
||||||
|
tiers: a.tiers,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.sort((x, y) => y.samples - x.samples);
|
||||||
|
|
||||||
|
const result: ModelPerformanceResult = {
|
||||||
|
window_days: days,
|
||||||
|
settled_runs: Number(rows.length),
|
||||||
|
settled_markets: totalSettledMarkets,
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
markets,
|
||||||
|
};
|
||||||
|
return createSuccessResponse(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelPerformanceResult {
|
||||||
|
window_days: number;
|
||||||
|
settled_runs: number;
|
||||||
|
settled_markets: number;
|
||||||
|
generated_at: string;
|
||||||
|
markets: Array<{
|
||||||
|
market: string;
|
||||||
|
samples: number;
|
||||||
|
shown_pct: number;
|
||||||
|
actual_pct: number;
|
||||||
|
gap: number;
|
||||||
|
ece: number;
|
||||||
|
calibration: "good" | "overconfident" | "underconfident";
|
||||||
|
bet_count: number;
|
||||||
|
bet_hit_pct: number;
|
||||||
|
bet_roi_pct: number;
|
||||||
|
actions: Record<string, number>;
|
||||||
|
tiers: Record<string, number>;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import {
|
|||||||
DbMarketPayload,
|
DbMarketPayload,
|
||||||
BasketballTeamStats,
|
BasketballTeamStats,
|
||||||
} from "./feeder.types";
|
} from "./feeder.types";
|
||||||
import { ImageUtils } from "../../common/utils/image.util";
|
|
||||||
import { deriveStoredMatchStatus } from "../../common/utils/match-status.util";
|
import { deriveStoredMatchStatus } from "../../common/utils/match-status.util";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -164,24 +163,6 @@ export class FeederPersistenceService {
|
|||||||
oddsArray: DbMarketPayload[],
|
oddsArray: DbMarketPayload[],
|
||||||
officialsData: MatchOfficial[],
|
officialsData: MatchOfficial[],
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
// START IMAGE DOWNLOADS (NON-BLOCKING)
|
|
||||||
const imageDownloads: Promise<void>[] = [];
|
|
||||||
|
|
||||||
const leagueId = this.safeString(league.id);
|
|
||||||
if (leagueId) {
|
|
||||||
const logoUrl = `https://file.mackolikfeeds.com/competitions/${leagueId}`;
|
|
||||||
const localPath = `public/uploads/competitions/${leagueId}.png`;
|
|
||||||
imageDownloads.push(
|
|
||||||
ImageUtils.downloadImage(logoUrl, localPath)
|
|
||||||
.then(() => void 0)
|
|
||||||
.catch((err) => {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to download league logo ${leagueId}: ${err}`,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const teamsToUpsert = [
|
const teamsToUpsert = [
|
||||||
{
|
{
|
||||||
id: homeTeamId,
|
id: homeTeamId,
|
||||||
@@ -197,20 +178,6 @@ export class FeederPersistenceService {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const team of teamsToUpsert) {
|
|
||||||
const teamLogoUrl = `https://file.mackolikfeeds.com/teams/${team.id}`;
|
|
||||||
const teamLocalPath = `public/uploads/teams/${team.id}.png`;
|
|
||||||
imageDownloads.push(
|
|
||||||
ImageUtils.downloadImage(teamLogoUrl, teamLocalPath)
|
|
||||||
.then(() => void 0)
|
|
||||||
.catch((err) => {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to download team logo ${team.id}: ${err}`,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// DATABASE TRANSACTION
|
// DATABASE TRANSACTION
|
||||||
try {
|
try {
|
||||||
await this.prisma.$transaction(
|
await this.prisma.$transaction(
|
||||||
@@ -264,7 +231,6 @@ export class FeederPersistenceService {
|
|||||||
countryId: countryId,
|
countryId: countryId,
|
||||||
sport: sport,
|
sport: sport,
|
||||||
competitionSlug: league.competitionSlug,
|
competitionSlug: league.competitionSlug,
|
||||||
logoUrl: `/uploads/competitions/${finalLeagueId}.png`,
|
|
||||||
} as any,
|
} as any,
|
||||||
});
|
});
|
||||||
if (league.sortOrder !== undefined) {
|
if (league.sortOrder !== undefined) {
|
||||||
@@ -291,10 +257,7 @@ export class FeederPersistenceService {
|
|||||||
|
|
||||||
if (teamsToCreate.length > 0) {
|
if (teamsToCreate.length > 0) {
|
||||||
await tx.team.createMany({
|
await tx.team.createMany({
|
||||||
data: teamsToCreate.map((t) => ({
|
data: teamsToCreate,
|
||||||
...t,
|
|
||||||
logoUrl: `/uploads/teams/${t.id}.png`,
|
|
||||||
})),
|
|
||||||
skipDuplicates: true,
|
skipDuplicates: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -304,7 +267,6 @@ export class FeederPersistenceService {
|
|||||||
where: { id: team.id },
|
where: { id: team.id },
|
||||||
data: {
|
data: {
|
||||||
name: team.name,
|
name: team.name,
|
||||||
logoUrl: `/uploads/teams/${team.id}.png`,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -614,9 +576,6 @@ export class FeederPersistenceService {
|
|||||||
{ maxWait: 40000, timeout: 40000 },
|
{ maxWait: 40000, timeout: 40000 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// WAIT FOR IMAGES AFTER TRANSACTION
|
|
||||||
await Promise.allSettled(imageDownloads);
|
|
||||||
|
|
||||||
this.logger.log(`✅ SAVED: [${matchId}] ${matchSummary.matchName}`);
|
this.logger.log(`✅ SAVED: [${matchId}] ${matchSummary.matchName}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
import { PrismaService } from "../../database/prisma.service";
|
import { PrismaService } from "../../database/prisma.service";
|
||||||
import { Sport } from "@prisma/client";
|
import { Sport } from "@prisma/client";
|
||||||
|
import {
|
||||||
|
countryFlagUrl,
|
||||||
|
teamLogoUrl,
|
||||||
|
} from "../../common/utils/image-url.util";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LeaguesService {
|
export class LeaguesService {
|
||||||
@@ -12,19 +16,21 @@ export class LeaguesService {
|
|||||||
* Get all countries
|
* Get all countries
|
||||||
*/
|
*/
|
||||||
async findAllCountries() {
|
async findAllCountries() {
|
||||||
return this.prisma.country.findMany({
|
const countries = await this.prisma.country.findMany({
|
||||||
orderBy: { name: "asc" },
|
orderBy: { name: "asc" },
|
||||||
});
|
});
|
||||||
|
return countries.map((c) => ({ ...c, flag: countryFlagUrl(c.id) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get country by ID
|
* Get country by ID
|
||||||
*/
|
*/
|
||||||
async findCountryById(id: string) {
|
async findCountryById(id: string) {
|
||||||
return this.prisma.country.findUnique({
|
const country = await this.prisma.country.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
include: { leagues: true },
|
include: { leagues: true },
|
||||||
});
|
});
|
||||||
|
return country ? { ...country, flag: countryFlagUrl(country.id) } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +72,7 @@ export class LeaguesService {
|
|||||||
* Get all teams
|
* Get all teams
|
||||||
*/
|
*/
|
||||||
async findAllTeams(sport?: Sport, search?: string) {
|
async findAllTeams(sport?: Sport, search?: string) {
|
||||||
return this.prisma.team.findMany({
|
const teams = await this.prisma.team.findMany({
|
||||||
where: {
|
where: {
|
||||||
...(sport ? { sport } : {}),
|
...(sport ? { sport } : {}),
|
||||||
...(search ? { name: { contains: search, mode: "insensitive" } } : {}),
|
...(search ? { name: { contains: search, mode: "insensitive" } } : {}),
|
||||||
@@ -74,28 +80,31 @@ export class LeaguesService {
|
|||||||
orderBy: { name: "asc" },
|
orderBy: { name: "asc" },
|
||||||
take: 100,
|
take: 100,
|
||||||
});
|
});
|
||||||
|
return teams.map((t) => ({ ...t, logo: teamLogoUrl(t.id) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get team by ID
|
* Get team by ID
|
||||||
*/
|
*/
|
||||||
async findTeamById(id: string) {
|
async findTeamById(id: string) {
|
||||||
return this.prisma.team.findUnique({
|
const team = await this.prisma.team.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
});
|
});
|
||||||
|
return team ? { ...team, logo: teamLogoUrl(team.id) } : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search teams by name
|
* Search teams by name
|
||||||
*/
|
*/
|
||||||
async searchTeams(name: string, sport?: Sport) {
|
async searchTeams(name: string, sport?: Sport) {
|
||||||
return this.prisma.team.findMany({
|
const teams = await this.prisma.team.findMany({
|
||||||
where: {
|
where: {
|
||||||
name: { contains: name, mode: "insensitive" },
|
name: { contains: name, mode: "insensitive" },
|
||||||
...(sport ? { sport } : {}),
|
...(sport ? { sport } : {}),
|
||||||
},
|
},
|
||||||
take: 20,
|
take: 20,
|
||||||
});
|
});
|
||||||
|
return teams.map((t) => ({ ...t, logo: teamLogoUrl(t.id) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,13 +170,9 @@ export class LeaguesService {
|
|||||||
status: m.status,
|
status: m.status,
|
||||||
state: m.state,
|
state: m.state,
|
||||||
homeTeamName: m.homeTeam?.name,
|
homeTeamName: m.homeTeam?.name,
|
||||||
homeTeamLogo: m.homeTeamId
|
homeTeamLogo: teamLogoUrl(m.homeTeamId) ?? null,
|
||||||
? `https://file.mackolikfeeds.com/teams/${m.homeTeamId}`
|
|
||||||
: null,
|
|
||||||
awayTeamName: m.awayTeam?.name,
|
awayTeamName: m.awayTeam?.name,
|
||||||
awayTeamLogo: m.awayTeamId
|
awayTeamLogo: teamLogoUrl(m.awayTeamId) ?? null,
|
||||||
? `https://file.mackolikfeeds.com/teams/${m.awayTeamId}`
|
|
||||||
: null,
|
|
||||||
leagueName: m.league?.name,
|
leagueName: m.league?.name,
|
||||||
countryName: m.league?.country?.name,
|
countryName: m.league?.country?.name,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -111,6 +111,22 @@ export class MatchesController {
|
|||||||
return this.matchesService.getActiveLeagues(sport || Sport.FOOTBALL);
|
return this.matchesService.getActiveLeagues(sport || Sport.FOOTBALL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /matches/:id/odds-movement
|
||||||
|
* Opening→closing odds movement per market/selection (from live_odds_history)
|
||||||
|
*/
|
||||||
|
@Public()
|
||||||
|
@Get(":id/odds-movement")
|
||||||
|
@ApiOperation({ summary: "Opening→closing odds movement for a match" })
|
||||||
|
@ApiParam({ name: "id", description: "Match ID" })
|
||||||
|
@ApiResponse({ status: 200, description: "{ market: { selection: { open, close } } }" })
|
||||||
|
async getOddsMovement(@Param("id") id: string) {
|
||||||
|
if (!id) {
|
||||||
|
throw new BadRequestException("Match ID is required");
|
||||||
|
}
|
||||||
|
return this.matchesService.getOddsMovement(id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /matches/:id
|
* GET /matches/:id
|
||||||
* Get full match details
|
* Get full match details
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
LIVE_STATUS_VALUES_FOR_DB,
|
LIVE_STATUS_VALUES_FOR_DB,
|
||||||
getDisplayMatchStatus,
|
getDisplayMatchStatus,
|
||||||
} from "../../common/utils/match-status.util";
|
} from "../../common/utils/match-status.util";
|
||||||
|
import {
|
||||||
|
countryFlagUrl,
|
||||||
|
teamLogoUrl,
|
||||||
|
} from "../../common/utils/image-url.util";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MatchesService {
|
export class MatchesService {
|
||||||
@@ -28,6 +32,51 @@ export class MatchesService {
|
|||||||
this.loadTopLeagues();
|
this.loadTopLeagues();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-match odds movement (opening→closing) from live_odds_history.
|
||||||
|
* Returns { [market]: { [selection]: { open, close } } } with the same
|
||||||
|
* Turkish market/selection labels used in match.odds, so the UI can line
|
||||||
|
* them up directly. Returns {} if there is no data or the table is absent.
|
||||||
|
*/
|
||||||
|
async getOddsMovement(
|
||||||
|
matchId: string,
|
||||||
|
): Promise<Record<string, Record<string, { open: number; close: number }>>> {
|
||||||
|
try {
|
||||||
|
const rows = await this.prisma.$queryRawUnsafe<
|
||||||
|
Array<{
|
||||||
|
market: string;
|
||||||
|
selection: string;
|
||||||
|
open: number | null;
|
||||||
|
close: number | null;
|
||||||
|
}>
|
||||||
|
>(
|
||||||
|
`SELECT market, selection,
|
||||||
|
(array_agg(new_value ORDER BY change_time ASC))[1] AS open,
|
||||||
|
(array_agg(new_value ORDER BY change_time DESC))[1] AS close
|
||||||
|
FROM live_odds_history
|
||||||
|
WHERE match_id = $1
|
||||||
|
GROUP BY market, selection`,
|
||||||
|
matchId,
|
||||||
|
);
|
||||||
|
const out: Record<
|
||||||
|
string,
|
||||||
|
Record<string, { open: number; close: number }>
|
||||||
|
> = {};
|
||||||
|
for (const r of rows) {
|
||||||
|
if (r.open == null || r.close == null) continue;
|
||||||
|
(out[r.market] ??= {})[r.selection] = {
|
||||||
|
open: Number(r.open),
|
||||||
|
close: Number(r.close),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
this.logger.warn(`getOddsMovement failed for ${matchId}: ${msg}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private loadTopLeagues() {
|
private loadTopLeagues() {
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(process.cwd(), "top_leagues.json");
|
const filePath = path.join(process.cwd(), "top_leagues.json");
|
||||||
@@ -53,20 +102,12 @@ export class MatchesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate URL for the country flag served from Mackolik
|
|
||||||
*/
|
|
||||||
private getCountryFlagUrl(countryId?: string | null): string | undefined {
|
private getCountryFlagUrl(countryId?: string | null): string | undefined {
|
||||||
if (!countryId) return undefined;
|
return countryFlagUrl(countryId);
|
||||||
return `https://file.mackolikfeeds.com/areas/${countryId}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate URL for the team logo served from local uploads
|
|
||||||
*/
|
|
||||||
private getTeamLogoUrl(teamId?: string | null): string | undefined {
|
private getTeamLogoUrl(teamId?: string | null): string | undefined {
|
||||||
if (!teamId) return undefined;
|
return teamLogoUrl(teamId);
|
||||||
return `https://file.mackolikfeeds.com/teams/${teamId}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getLiveFilter(): Prisma.LiveMatchWhereInput {
|
private getLiveFilter(): Prisma.LiveMatchWhereInput {
|
||||||
|
|||||||
@@ -27,6 +27,20 @@ export class MatchInfoDto {
|
|||||||
@ApiProperty({ required: false, default: false })
|
@ApiProperty({ required: false, default: false })
|
||||||
is_top_league?: boolean;
|
is_top_league?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
required: false,
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
"Backtest-derived per-league confidence (ROI + sample size). " +
|
||||||
|
"null when the league has too little data to judge.",
|
||||||
|
})
|
||||||
|
league_confidence?: {
|
||||||
|
label: "high" | "medium" | "low";
|
||||||
|
bet_roi: number;
|
||||||
|
bet_n: number;
|
||||||
|
hit: number;
|
||||||
|
} | null;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
required: false,
|
required: false,
|
||||||
enum: ["football", "basketball"],
|
enum: ["football", "basketball"],
|
||||||
|
|||||||
@@ -1525,6 +1525,37 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
payload: MatchPredictionDto,
|
payload: MatchPredictionDto,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
// Finished-match re-analyses (manual validation runs) must not pollute
|
||||||
|
// the forward track record: they would bias settlement ROI, the
|
||||||
|
// per-league karne and engine-version comparisons. Tag them into their
|
||||||
|
// own engine_version bucket so every GROUP BY engine_version isolates
|
||||||
|
// them automatically — the data is kept, the live karne stays clean.
|
||||||
|
const auditMatch = await this.prisma.match.findUnique({
|
||||||
|
where: { id: matchId },
|
||||||
|
select: {
|
||||||
|
state: true,
|
||||||
|
status: true,
|
||||||
|
scoreHome: true,
|
||||||
|
scoreAway: true,
|
||||||
|
mstUtc: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const kickoffMs =
|
||||||
|
auditMatch?.mstUtc != null ? Number(auditMatch.mstUtc) : null;
|
||||||
|
const kickoffLongPast =
|
||||||
|
kickoffMs !== null && Date.now() - kickoffMs > 3 * 60 * 60 * 1000;
|
||||||
|
const isCompletedRun =
|
||||||
|
isMatchCompleted({
|
||||||
|
state: auditMatch?.state ?? null,
|
||||||
|
status: auditMatch?.status ?? null,
|
||||||
|
scoreHome: auditMatch?.scoreHome,
|
||||||
|
scoreAway: auditMatch?.scoreAway,
|
||||||
|
}) || kickoffLongPast;
|
||||||
|
const baseVersion = String(payload.model_version || "unknown");
|
||||||
|
const engineVersion = isCompletedRun
|
||||||
|
? `${baseVersion}.sim-finished`
|
||||||
|
: baseVersion;
|
||||||
|
|
||||||
const oddsSnapshot = await this.getPredictionOddsSnapshot(matchId);
|
const oddsSnapshot = await this.getPredictionOddsSnapshot(matchId);
|
||||||
const payloadSummary = this.buildPredictionPayloadSummary(payload);
|
const payloadSummary = this.buildPredictionPayloadSummary(payload);
|
||||||
await this.prisma.$executeRawUnsafe(
|
await this.prisma.$executeRawUnsafe(
|
||||||
@@ -1539,7 +1570,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb)
|
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb)
|
||||||
`,
|
`,
|
||||||
matchId,
|
matchId,
|
||||||
String(payload.model_version || "unknown"),
|
engineVersion,
|
||||||
typeof payload.decision_trace_id === "string"
|
typeof payload.decision_trace_id === "string"
|
||||||
? payload.decision_trace_id
|
? payload.decision_trace_id
|
||||||
: null,
|
: null,
|
||||||
@@ -1611,7 +1642,69 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
// ── Forward-test capture (V31e) ────────────────────────────────────
|
||||||
|
// Persist EVERY market's probability + the rationale behind each pick so
|
||||||
|
// the settlement job can later score each market against reality and the
|
||||||
|
// admin "Model Performance" page can show per-market calibration
|
||||||
|
// (model% → actual%) and decision reasons. Compact projection only.
|
||||||
|
// Some runtime fields (betting_brain, is_underdog_reference) are present
|
||||||
|
// in the AI payload but not declared on the DTO — read them via a cast.
|
||||||
|
const marketsFull = Array.isArray(payload.bet_summary)
|
||||||
|
? payload.bet_summary.map((item) => {
|
||||||
|
const loose = item as unknown as Record<string, unknown>;
|
||||||
|
const bb = (loose.betting_brain ?? {}) as Record<string, unknown>;
|
||||||
|
return {
|
||||||
|
market: item.market,
|
||||||
|
pick: item.pick,
|
||||||
|
odds: item.odds ?? null,
|
||||||
|
model_probability: item.model_probability ?? null,
|
||||||
|
calibrated_confidence: item.calibrated_confidence ?? null,
|
||||||
|
raw_confidence: item.raw_confidence ?? null,
|
||||||
|
calibrated_probability: item.calibrated_probability ?? null,
|
||||||
|
implied_prob: item.implied_prob ?? null,
|
||||||
|
ev_edge: item.ev_edge ?? 0,
|
||||||
|
playable: item.playable ?? false,
|
||||||
|
bet_grade: item.bet_grade ?? "PASS",
|
||||||
|
signal_tier: item.signal_tier ?? null,
|
||||||
|
stake_units: item.stake_units ?? 0,
|
||||||
|
is_underdog_reference: Boolean(loose.is_underdog_reference),
|
||||||
|
action: (bb.action as string | undefined) ?? null,
|
||||||
|
value_tier: (bb.value_tier as string | undefined) ?? null,
|
||||||
|
model_market_gap: (bb.model_market_gap as number | undefined) ?? null,
|
||||||
|
trap_market_flag: Boolean(bb.trap_market_flag),
|
||||||
|
vetoes: Array.isArray(bb.vetoes)
|
||||||
|
? (bb.vetoes as unknown[]).slice(0, 6)
|
||||||
|
: [],
|
||||||
|
positives: Array.isArray(bb.positives)
|
||||||
|
? (bb.positives as unknown[]).slice(0, 6)
|
||||||
|
: [],
|
||||||
|
reasons: Array.isArray(item.reasons) ? item.reasons.slice(0, 6) : [],
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Per-outcome probability distribution for each market (graph bars).
|
||||||
|
const marketBoardProbs =
|
||||||
|
payload.market_board && typeof payload.market_board === "object"
|
||||||
|
? Object.fromEntries(
|
||||||
|
Object.entries(
|
||||||
|
payload.market_board as Record<string, { probs?: unknown }>,
|
||||||
|
).map(([mkt, entry]) => [
|
||||||
|
mkt,
|
||||||
|
entry && typeof entry === "object" ? (entry.probs ?? null) : null,
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
: {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
markets_full: marketsFull,
|
||||||
|
market_board_probs: marketBoardProbs,
|
||||||
|
betting_brain_version:
|
||||||
|
(
|
||||||
|
(payload as unknown as Record<string, unknown>).betting_brain as
|
||||||
|
| { version?: string }
|
||||||
|
| undefined
|
||||||
|
)?.version ?? null,
|
||||||
model_version: payload.model_version,
|
model_version: payload.model_version,
|
||||||
calibration_version: payload.calibration_version ?? null,
|
calibration_version: payload.calibration_version ?? null,
|
||||||
shadow_engine_version: payload.shadow_engine_version ?? null,
|
shadow_engine_version: payload.shadow_engine_version ?? null,
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><circle fill="#F4900C" cx="18" cy="18" r="18"/><path fill="#231F20" d="M36 17h-8.981c.188-5.506 1.943-9.295 4.784-10.546-.445-.531-.926-1.027-1.428-1.504-2.83 1.578-5.145 5.273-5.354 12.049H19V0h-2v17h-6.021c-.208-6.776-2.523-10.471-5.353-12.049-.502.476-.984.972-1.428 1.503C7.039 7.705 8.793 11.494 8.981 17H0v2h8.981c-.188 5.506-1.942 9.295-4.783 10.546.445.531.926 1.027 1.428 1.504 2.831-1.578 5.145-5.273 5.353-12.05H17v17h2V19h6.021c.209 6.776 2.523 10.471 5.354 12.05.502-.476.984-.973 1.428-1.504-2.841-1.251-4.595-5.04-4.784-10.546H36v-2z"/></svg>
|
||||||
|
After Width: | Height: | Size: 617 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFCD05" d="M0 27c0 2.209 1.791 4 4 4h28c2.209 0 4-1.791 4-4v-4H0v4z"/><path fill="#ED1F24" d="M0 14h36v9H0z"/><path fill="#141414" d="M32 5H4C1.791 5 0 6.791 0 9v5h36V9c0-2.209-1.791-4-4-4z"/></svg>
|
||||||
|
After Width: | Height: | Size: 271 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#C60A1D" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V9c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v18z"/><path fill="#FFC400" d="M0 12h36v12H0z"/><path fill="#EA596E" d="M9 17v3c0 1.657 1.343 3 3 3s3-1.343 3-3v-3H9z"/><path fill="#F4A2B2" d="M12 16h3v3h-3z"/><path fill="#DD2E44" d="M9 16h3v3H9z"/><ellipse fill="#EA596E" cx="12" cy="14.5" rx="3" ry="1.5"/><ellipse fill="#FFAC33" cx="12" cy="13.75" rx="3" ry=".75"/><path fill="#99AAB5" d="M7 16h1v7H7zm9 0h1v7h-1z"/><path fill="#66757F" d="M6 22h3v1H6zm9 0h3v1h-3zm-8-7h1v1H7zm9 0h1v1h-1z"/></svg>
|
||||||
|
After Width: | Height: | Size: 629 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#039" d="M32 5H4C1.791 5 0 6.791 0 9v18c0 2.209 1.791 4 4 4h28c2.209 0 4-1.791 4-4V9c0-2.209-1.791-4-4-4z"/><path d="M18.539 9.705l.849-.617h-1.049l-.325-.998-.324.998h-1.049l.849.617-.325.998.849-.617.849.617zm0 17.333l.849-.617h-1.049l-.325-.998-.324.998h-1.049l.849.617-.325.998.849-.617.849.617zm-8.666-8.667l.849-.617h-1.05l-.324-.998-.325.998H7.974l.849.617-.324.998.849-.617.849.617zm1.107-4.285l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.617.849.617zm0 8.619l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.617.849.617zm3.226-11.839l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.617.849.617zm0 15.067l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.616.849.616zm11.921-7.562l-.849-.617h1.05l.324-.998.325.998h1.049l-.849.617.324.998-.849-.617-.849.617zm-1.107-4.285l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.617-.849.617zm0 8.619l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.617-.849.617zm-3.226-11.839l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.617-.849.617zm0 15.067l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.616-.849.616z" fill="#FC0"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#ED2939" d="M36 27c0 2.209-1.791 4-4 4h-8V5h8c2.209 0 4 1.791 4 4v18z"/><path fill="#002495" d="M4 5C1.791 5 0 6.791 0 9v18c0 2.209 1.791 4 4 4h8V5H4z"/><path fill="#EEE" d="M12 5h12v26H12z"/></svg>
|
||||||
|
After Width: | Height: | Size: 270 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#00247D" d="M0 9.059V13h5.628zM4.664 31H13v-5.837zM23 25.164V31h8.335zM0 23v3.941L5.63 23zM31.337 5H23v5.837zM36 26.942V23h-5.631zM36 13V9.059L30.371 13zM13 5H4.664L13 10.837z"/><path fill="#CF1B2B" d="M25.14 23l9.712 6.801c.471-.479.808-1.082.99-1.749L28.627 23H25.14zM13 23h-2.141l-9.711 6.8c.521.53 1.189.909 1.938 1.085L13 23.943V23zm10-10h2.141l9.711-6.8c-.521-.53-1.188-.909-1.937-1.085L23 12.057V13zm-12.141 0L1.148 6.2C.677 6.68.34 7.282.157 7.949L7.372 13h3.487z"/><path fill="#EEE" d="M36 21H21v10h2v-5.836L31.335 31H32c1.117 0 2.126-.461 2.852-1.199L25.14 23h3.487l7.215 5.052c.093-.337.158-.686.158-1.052v-.058L30.369 23H36v-2zM0 21v2h5.63L0 26.941V27c0 1.091.439 2.078 1.148 2.8l9.711-6.8H13v.943l-9.914 6.941c.294.07.598.116.914.116h.664L13 25.163V31h2V21H0zM36 9c0-1.091-.439-2.078-1.148-2.8L25.141 13H23v-.943l9.915-6.942C32.62 5.046 32.316 5 32 5h-.663L23 10.837V5h-2v10h15v-2h-5.629L36 9.059V9zM13 5v5.837L4.664 5H4c-1.118 0-2.126.461-2.852 1.2l9.711 6.8H7.372L.157 7.949C.065 8.286 0 8.634 0 9v.059L5.628 13H0v2h15V5h-2z"/><path fill="#CF1B2B" d="M21 15V5h-6v10H0v6h15v10h6V21h15v-6z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CE2B37" d="M36 27c0 2.209-1.791 4-4 4h-8V5h8c2.209 0 4 1.791 4 4v18z"/><path fill="#009246" d="M4 5C1.791 5 0 6.791 0 9v18c0 2.209 1.791 4 4 4h8V5H4z"/><path fill="#EEE" d="M12 5h12v26H12z"/></svg>
|
||||||
|
After Width: | Height: | Size: 270 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#E30917" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V9c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v18z"/><path fill="#EEE" d="M16 24c-3.314 0-6-2.685-6-6 0-3.314 2.686-6 6-6 1.31 0 2.52.425 3.507 1.138-1.348-1.524-3.312-2.491-5.507-2.491-4.061 0-7.353 3.292-7.353 7.353 0 4.062 3.292 7.354 7.353 7.354 2.195 0 4.16-.967 5.507-2.492C18.521 23.575 17.312 24 16 24zm3.913-5.77l2.44.562.22 2.493 1.288-2.146 2.44.561-1.644-1.888 1.287-2.147-2.303.98-1.644-1.889.22 2.494z"/></svg>
|
||||||
|
After Width: | Height: | Size: 556 B |