Compare commits
19 Commits
659110c806
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d40b55fb9 | |||
| 6269ede4ad | |||
| 4c137fbab6 | |||
| bb911176df | |||
| 950add373f | |||
| 9a8f9941b6 | |||
| e0fbde2fde | |||
| b62a4f2161 | |||
| c3e44ee697 | |||
| 42b6c7ce43 | |||
| 7b17aa1fee | |||
| c338aba1c0 | |||
| 1c03fa5e1c | |||
| 9e41407cb5 | |||
| b9700f9fda | |||
| 033a29c79c | |||
| 4e563e996e | |||
| 671979b07d | |||
| b5cb412236 |
@@ -46,6 +46,7 @@ jobs:
|
||||
-e AI_ENGINE_URL='http://iddaai-ai-engine:8000' \
|
||||
-e JWT_SECRET='${{ secrets.JWT_SECRET }}' \
|
||||
-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"
|
||||
|
||||
- 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]}
|
||||
]}
|
||||
Binary file not shown.
@@ -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:
|
||||
_calibrator_instance = Calibrator()
|
||||
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",
|
||||
}
|
||||
Binary file not shown.
@@ -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,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,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,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:
|
||||
MIN_ODDS = 1.30
|
||||
MIN_BET_SCORE = 72.0
|
||||
MIN_WATCH_SCORE = 62.0
|
||||
MIN_BET_SCORE = 62.0
|
||||
MIN_WATCH_SCORE = 52.0
|
||||
MIN_BAND_SAMPLE = 8
|
||||
HARD_DIVERGENCE = 0.22
|
||||
SOFT_DIVERGENCE = 0.14
|
||||
EXTREME_MODEL_PROB = 0.85
|
||||
EXTREME_GAP = 0.30
|
||||
SNIPER_BYPASSABLE_VETOES = {"play_score_too_low"}
|
||||
# V31d: value-tier underdogs are bet on the odds-premium edge, NOT on
|
||||
# high win-probability. These two vetoes encode a favorite-picking rule
|
||||
# (demand >45% confidence) that structurally excludes every profitable
|
||||
# underdog, so we waive them when a row matches an MS value tier.
|
||||
# Genuine safety vetoes (extreme_neg_ev, ev_too_high_trap, htft_reversal
|
||||
# _risk_high, v25_v27_hard_disagreement, low_reliability_hard_block) are
|
||||
# NOT in this set and still reject.
|
||||
VALUE_TIER_BYPASSABLE_VETOES = {"calibrated_confidence_too_low", "play_score_too_low"}
|
||||
VALUE_TIER_NAMES = {"premium", "strong", "standard"}
|
||||
# V31d: value-regime score floors (replaces favorite-confidence scoring
|
||||
# for value-tier matches). premium clears MIN_BET_SCORE(62) → BET;
|
||||
# strong/standard are capped below it → WATCH (visible, not staked)
|
||||
# because the 60-day data shows those bands break even.
|
||||
VALUE_TIER_BASE_SCORE = {"premium": 70.0, "strong": 56.0, "standard": 54.0}
|
||||
# V31d: flat, small stake for value-tier underdogs. Hit rate ~20% with
|
||||
# long losing streaks (60d: up to 35 in a row) — the edge is in FREQUENCY,
|
||||
# not per-bet size. Keep stake small to survive variance. Tunable: raise
|
||||
# only if the bettor's bankroll/risk appetite allows deeper drawdowns.
|
||||
VALUE_TIER_STAKE_UNITS = 0.5
|
||||
TRAP_MARKET_GAP = 0.10
|
||||
|
||||
# ── 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 = {
|
||||
"MS": 45.0,
|
||||
"DC": 55.0,
|
||||
@@ -39,31 +75,181 @@ class BettingBrain:
|
||||
|
||||
SNIPER_BLOCKED_MARKETS = {"HT", "HTFT", "OE", "CARDS", "HT_OU05", "HT_OU15"}
|
||||
|
||||
# Markets that lose money under every filter combination per the
|
||||
# diagnostic backtest (1000 matches). Until calibration is rebuilt for
|
||||
# these specifically, force NO_BET. Re-evaluate after each backtest run.
|
||||
MUTED_MARKETS = {"BTTS"}
|
||||
# V30: NO markets muted — backtest tüm marketlerin gerçek ROI'sini görmeli.
|
||||
# Tier sistemi zaten filtreleme yapıyor; mute etmek veri kaybına yol açar.
|
||||
MUTED_MARKETS = set()
|
||||
|
||||
# Per-market optimal filter envelopes derived from the diagnostic
|
||||
# backtest grid search (reports/filter_optimization_patch.json). Any
|
||||
# pick falling OUTSIDE this envelope is vetoed. Tightens the playable
|
||||
# band to the ROI-positive zone identified empirically.
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# V31d: KANITA DAYALI KADEMELİ DEĞER SİSTEMİ (Evidence-Based Tiers)
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
# User directive: show 3 quality levels so the bettor picks by risk
|
||||
# appetite ("hangi bahis hangi oranda tutuyor"). Each MS underdog tier
|
||||
# carries a `value_tier` label that propagates to the UI. Only the
|
||||
# PREMIUM band is auto-staked (BET); strong/standard surface as WATCH
|
||||
# (full analysis shown, not staked) because the data says they break
|
||||
# even — see new_gate_sim.py.
|
||||
#
|
||||
# Each entry: {min_conf, min_edge, max_edge, min_odds, max_odds,
|
||||
# min_reliability, require_v27_agree}
|
||||
MARKET_OPTIMAL_FILTERS = {
|
||||
"MS": {
|
||||
"min_edge": -0.05, "max_edge": 0.15,
|
||||
"min_odds": 1.20, "max_odds": 10.0,
|
||||
"min_reliability": 0.0, "require_v27_agree": True,
|
||||
},
|
||||
"OU25": {
|
||||
"min_edge": -1.0, "max_edge": 0.15,
|
||||
"min_odds": 1.80, "max_odds": 10.0,
|
||||
"min_reliability": 0.0, "require_v27_agree": False,
|
||||
},
|
||||
# Validated on 60-day, 72,582-settled-row multi-pick backtest
|
||||
# (ms_envelope.py + new_gate_sim.py, span 2026-04-17..05-28):
|
||||
# PREMIUM (6.0-7.5, gap≥0, protective vetoes kept):
|
||||
# 602 bets, +32.7% ROI, +39.4u, 20.6% hit, avgOdd 6.50
|
||||
# ALL 6 weeks positive (+13.7%..+52.9%); OOS(>05-24) +47.4%;
|
||||
# survives dropping top-5 wins (+24%). 14.3 bets/day.
|
||||
# STRONG (5.0-6.0, gap≥0): ~breakeven (-1%) → WATCH, not staked.
|
||||
# STANDARD (3.0-5.0, gap≥0): +0.5% breakeven → WATCH, volume zone.
|
||||
#
|
||||
# WHY 6.0-7.5 (not 6.0-50.0 as in V31c): the edge is concentrated.
|
||||
# odds 6.0-7.0 +35% | 7.0-8.0 ~breakeven | 8.0+ NEGATIVE (longshot
|
||||
# graveyard, -10..-26% ROI). The old wide premium tier let losing
|
||||
# longshots in. Above 7.5 the model's edge evaporates.
|
||||
#
|
||||
# ROOT-CAUSE FIX (the volume crisis): underdogs were structurally
|
||||
# un-bettable. Two vetoes (calibrated_confidence_too_low,
|
||||
# play_score_too_low) auto-REJECTED every dog because they demand
|
||||
# >45% model confidence — a FAVORITE-picking rule. A 6.5 dog wins
|
||||
# ~20% of the time; that IS the edge (odds premium), not a defect.
|
||||
# For value-tier matches we bypass those two vetoes and score from
|
||||
# the validated tier quality instead of win-probability. Genuine
|
||||
# protections stay: extreme_neg_ev, ev_too_high_trap, htft_reversal
|
||||
# _risk_high, v25_v27_hard_disagreement. Result: 28 → 602 staked
|
||||
# bets (22x volume), -1.6u → +39.4u profit. ALL rich analysis data
|
||||
# (market_board, v25/v27, triple_value, probs) is untouched — only
|
||||
# the `playable` flag changes.
|
||||
#
|
||||
# MULTI-LEG VERDICT (definitive): parlays DESTROY edge.
|
||||
# 1-leg +3.4% → 2-leg -32% → 3-leg -67% → 4-leg -83%.
|
||||
# System must bet SINGLES only. No combo recommendations.
|
||||
#
|
||||
# Non-MS markets: ultrastrict tiers (rarely pass BET) → info-only.
|
||||
# All non-MS configurations showed negative ROI in backtest.
|
||||
# ═══════════════════════════════════════════════════════════════════
|
||||
MARKET_ODDS_TIERS = {
|
||||
# ── MS (Match Score / 1X2) — the ONLY profitable market ────────
|
||||
"MS": [
|
||||
# PREMIUM — the validated edge. 6.0-7.5 odds, model >= market.
|
||||
# 60d: 602 bets, +32.7% ROI, all weeks positive. AUTO-STAKED.
|
||||
# Low hit (~20%) → high variance; stake stays small (see _brain_stake).
|
||||
{"min_odds": 6.00, "max_odds": 7.50, "min_edge": -0.20,
|
||||
"max_edge": 0.25, "min_reliability": 0.30,
|
||||
"min_model_gap": 0.0,
|
||||
"require_v27_agree": False, "require_no_trap": False,
|
||||
"value_tier": "premium",
|
||||
"label": "ms_underdog_premium"},
|
||||
|
||||
# STRONG — 5.0-6.0. Breakeven (-1%) → WATCH (visible, not staked).
|
||||
{"min_odds": 5.00, "max_odds": 6.00, "min_edge": -0.20,
|
||||
"max_edge": 0.25, "min_reliability": 0.30,
|
||||
"min_model_gap": 0.0,
|
||||
"require_v27_agree": False, "require_no_trap": False,
|
||||
"value_tier": "strong",
|
||||
"label": "ms_underdog_strong"},
|
||||
|
||||
# STANDARD — 3.0-5.0 volume zone. +0.5% breakeven → WATCH.
|
||||
{"min_odds": 3.00, "max_odds": 5.00, "min_edge": -0.18,
|
||||
"max_edge": 0.25, "min_reliability": 0.30,
|
||||
"min_model_gap": 0.0,
|
||||
"require_v27_agree": False, "require_no_trap": False,
|
||||
"value_tier": "standard",
|
||||
"label": "ms_underdog_standard"},
|
||||
],
|
||||
|
||||
# ── Non-MS markets: visible but NOT playable ───────────────────
|
||||
# All non-MS markets showed negative ROI in 50K-row backtest.
|
||||
# Tiers exist so the model's read is surfaced (bet_summary),
|
||||
# but criteria are strict enough that almost nothing passes BET.
|
||||
# The user sees info; the system doesn't lose money on them.
|
||||
|
||||
"DC": [
|
||||
{"min_odds": 1.15, "max_odds": 1.60, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "dc_ultrastrict"},
|
||||
],
|
||||
|
||||
"OU25": [
|
||||
{"min_odds": 1.60, "max_odds": 2.20, "min_edge": 0.02,
|
||||
"max_edge": 0.10, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.03,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ou25_ultrastrict"},
|
||||
],
|
||||
|
||||
"OU35": [
|
||||
{"min_odds": 1.50, "max_odds": 2.50, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ou35_ultrastrict"},
|
||||
],
|
||||
|
||||
"BTTS": [
|
||||
{"min_odds": 1.60, "max_odds": 2.10, "min_edge": 0.02,
|
||||
"max_edge": 0.10, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.03,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "btts_ultrastrict"},
|
||||
],
|
||||
|
||||
"HT": [
|
||||
{"min_odds": 2.00, "max_odds": 3.50, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ht_ultrastrict"},
|
||||
],
|
||||
|
||||
"OU15": [
|
||||
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ou15_ultrastrict"},
|
||||
],
|
||||
|
||||
"HTFT": [
|
||||
{"min_odds": 4.00, "max_odds": 15.00, "min_edge": 0.03,
|
||||
"max_edge": 0.15, "min_reliability": 0.45,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "htft_ultrastrict"},
|
||||
],
|
||||
|
||||
"OE": [
|
||||
{"min_odds": 1.80, "max_odds": 2.10, "min_edge": 0.02,
|
||||
"max_edge": 0.08, "min_reliability": 0.55,
|
||||
"max_model_gap": -0.03,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "oe_ultrastrict"},
|
||||
],
|
||||
|
||||
"HT_OU05": [
|
||||
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ht_ou05_ultrastrict"},
|
||||
],
|
||||
|
||||
"HT_OU15": [
|
||||
{"min_odds": 1.60, "max_odds": 3.00, "min_edge": 0.02,
|
||||
"max_edge": 0.12, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "ht_ou15_ultrastrict"},
|
||||
],
|
||||
|
||||
"CARDS": [
|
||||
{"min_odds": 1.60, "max_odds": 2.50, "min_edge": 0.02,
|
||||
"max_edge": 0.10, "min_reliability": 0.50,
|
||||
"max_model_gap": -0.02,
|
||||
"require_v27_agree": False, "require_no_trap": True,
|
||||
"label": "cards_ultrastrict"},
|
||||
],
|
||||
}
|
||||
|
||||
# Legacy flat envelope (backward compat for markets not in tiered system)
|
||||
MARKET_OPTIMAL_FILTERS = {}
|
||||
|
||||
MARKET_PRIORS = {
|
||||
"DC": 4.0,
|
||||
"OU15": 3.0,
|
||||
@@ -77,7 +263,14 @@ class BettingBrain:
|
||||
"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")
|
||||
if not isinstance(v27_engine, dict):
|
||||
return package
|
||||
@@ -197,7 +390,7 @@ class BettingBrain:
|
||||
|
||||
rejected = [d for d in decisions if d.get("action") == "REJECT"]
|
||||
guarded["betting_brain"] = {
|
||||
"version": "judge-v2-score-coherent",
|
||||
"version": "judge-v31f-national-regime",
|
||||
"decision": decision,
|
||||
"reason": decision_reason,
|
||||
"main_pick_key": main_key or None,
|
||||
@@ -227,6 +420,10 @@ class BettingBrain:
|
||||
pick = str(row.get("pick") or "")
|
||||
model_prob = self._market_probability(row, package)
|
||||
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
|
||||
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
|
||||
@@ -240,6 +437,21 @@ class BettingBrain:
|
||||
triple_is_value = bool((triple or {}).get("is_value"))
|
||||
consensus = str((package.get("v27_engine") or {}).get("consensus") or "").upper()
|
||||
|
||||
# V29c: Compute trap_market_flag early (needed by tier require_no_trap)
|
||||
trap_market_flag = False
|
||||
trap_market_gap = None
|
||||
if isinstance(triple, dict):
|
||||
_band_rate = self._safe_float(triple.get("band_rate"))
|
||||
_implied = self._safe_float(triple.get("implied_prob"))
|
||||
if (
|
||||
_band_rate is not None
|
||||
and _implied is not None
|
||||
and band_sample >= self.MIN_BAND_SAMPLE
|
||||
and (_implied - _band_rate) > self.TRAP_MARKET_GAP
|
||||
):
|
||||
trap_market_flag = True
|
||||
trap_market_gap = round(_implied - _band_rate, 4)
|
||||
|
||||
positives: List[str] = []
|
||||
issues: List[str] = []
|
||||
vetoes: List[str] = []
|
||||
@@ -256,7 +468,7 @@ class BettingBrain:
|
||||
if market in self.SNIPER_BLOCKED_MARKETS:
|
||||
is_value_sniper = False
|
||||
if is_value_sniper:
|
||||
score += 20.0
|
||||
score += 8.0 # V29b: reduced from 20, tiers do the real filtering
|
||||
positives.append("value_sniper_override")
|
||||
|
||||
score += max(0.0, min(20.0, calibrated_conf * 0.22))
|
||||
@@ -276,7 +488,7 @@ class BettingBrain:
|
||||
if odds_rel < 0.30:
|
||||
score -= 22.0
|
||||
issues.append("very_low_reliability_league")
|
||||
if market in {"MS", "DC", "OU25", "BTTS"} and not is_value_sniper:
|
||||
if market in {"MS", "DC", "OU25", "BTTS"}: # V29: hard veto, no sniper bypass
|
||||
vetoes.append("low_reliability_league_hard_block")
|
||||
elif odds_rel < 0.45:
|
||||
score -= 12.0
|
||||
@@ -305,38 +517,79 @@ class BettingBrain:
|
||||
# ev_edge < 0 = "model market'in altında olasılık veriyor" = vig'i
|
||||
# yiyemeyeceğimiz negative-EV bahis. Hard veto: oynama.
|
||||
# Sniper override hâlâ geçer (yüksek convicted alternatif pick'ler).
|
||||
if ev_edge < 0.0 and not is_value_sniper:
|
||||
vetoes.append("negative_ev_edge")
|
||||
issues.append(f"ev_edge={ev_edge:.3f}_below_zero")
|
||||
# V29b: negative_ev_edge hard veto REMOVED — tier system handles
|
||||
# edge bounds per-market via min_edge. MS underdog tier allows
|
||||
# ev >= -0.15, so a universal ev<0 veto would kill profitable bets.
|
||||
if ev_edge < -0.20: # Only veto truly extreme negative edge
|
||||
vetoes.append("extreme_negative_ev_edge")
|
||||
issues.append(f"ev_edge={ev_edge:.3f}_extreme_negative")
|
||||
# Trap edge: bizim diagnostic backtest'te ev_edge >= 0.20 olan tüm
|
||||
# bahisler kaybediyordu (n=10, hepsi -%25+ ROI). Model market'i bu
|
||||
# kadar yanlış buluyorsa muhtemelen modelin kendisinin yanlış olduğu
|
||||
# bir senaryo (eksik info, tuhaf maç, vs.) — oynama.
|
||||
if ev_edge >= 0.20 and not is_value_sniper:
|
||||
if ev_edge >= 0.30: # V29b: raised from 0.20, tiers cap at 0.25
|
||||
vetoes.append("ev_edge_too_high_trap")
|
||||
issues.append(f"ev_edge={ev_edge:.3f}_trap_range")
|
||||
|
||||
# ── MUTED MARKETS (grid search showed no profitable config) ──
|
||||
if market in self.MUTED_MARKETS and not is_value_sniper:
|
||||
if market in self.MUTED_MARKETS: # V29: hard veto, no sniper bypass
|
||||
vetoes.append("market_muted_by_backtest")
|
||||
issues.append(f"market_{market}_muted")
|
||||
|
||||
# ── PER-MARKET OPTIMAL ENVELOPE (from grid search) ──
|
||||
envelope = self.MARKET_OPTIMAL_FILTERS.get(market)
|
||||
if envelope and not is_value_sniper:
|
||||
if ev_edge < envelope["min_edge"]:
|
||||
# ── V30: ODDS-TIERED ENVELOPE (from 7K backtest grid search) ──
|
||||
# Each market has multiple odds zones with different filters.
|
||||
# If a bet doesn't fit ANY tier, it gets vetoed.
|
||||
# V30: added model_gap filtering — data shows model>market is
|
||||
# inversely correlated with winning for BTTS/OU25.
|
||||
tiers = self.MARKET_ODDS_TIERS.get(market, [])
|
||||
# Also check legacy flat envelope for backward compat
|
||||
legacy_env = self.MARKET_OPTIMAL_FILTERS.get(market)
|
||||
tier_matched = False
|
||||
tier_label = None
|
||||
tier_value = None # V31c: quality tier (premium/strong/standard)
|
||||
if tiers:
|
||||
for tier in tiers:
|
||||
if not (tier["min_odds"] <= odds <= tier["max_odds"]):
|
||||
continue
|
||||
if ev_edge < tier["min_edge"] or ev_edge > tier["max_edge"]:
|
||||
continue
|
||||
if odds_rel < tier["min_reliability"]:
|
||||
continue
|
||||
if tier.get("require_v27_agree") and consensus != "AGREE":
|
||||
continue
|
||||
if tier.get("require_no_trap") and trap_market_flag:
|
||||
continue
|
||||
# V30: model-market gap filter
|
||||
if model_gap is not None:
|
||||
if "min_model_gap" in tier and model_gap < tier["min_model_gap"]:
|
||||
continue
|
||||
if "max_model_gap" in tier and model_gap > tier["max_model_gap"]:
|
||||
continue
|
||||
tier_matched = True
|
||||
tier_label = tier.get("label")
|
||||
tier_value = tier.get("value_tier") # V31c
|
||||
break
|
||||
if not tier_matched:
|
||||
vetoes.append("outside_all_odds_tiers")
|
||||
issues.append(f"no_profitable_tier_for_{market}_at_odds_{odds:.2f}")
|
||||
elif legacy_env:
|
||||
if ev_edge < legacy_env["min_edge"]:
|
||||
vetoes.append("outside_envelope_edge_low")
|
||||
if ev_edge > envelope["max_edge"]:
|
||||
if ev_edge > legacy_env["max_edge"]:
|
||||
vetoes.append("outside_envelope_edge_high")
|
||||
if odds and odds < envelope["min_odds"]:
|
||||
if odds and odds < legacy_env["min_odds"]:
|
||||
vetoes.append("outside_envelope_odds_low")
|
||||
if odds and odds > envelope["max_odds"]:
|
||||
if odds and odds > legacy_env["max_odds"]:
|
||||
vetoes.append("outside_envelope_odds_high")
|
||||
if odds_rel < envelope["min_reliability"]:
|
||||
if odds_rel < legacy_env["min_reliability"]:
|
||||
vetoes.append("outside_envelope_reliability_low")
|
||||
if envelope["require_v27_agree"] and consensus != "AGREE":
|
||||
if legacy_env.get("require_v27_agree") and consensus != "AGREE":
|
||||
vetoes.append("outside_envelope_v27_must_agree")
|
||||
|
||||
# V31d: a matched value tier is the validated profitable signal.
|
||||
# It unlocks the value-betting regime (veto bypass + score floor).
|
||||
is_value_tier = tier_value in self.VALUE_TIER_NAMES
|
||||
|
||||
if divergence is not None:
|
||||
if divergence >= self.HARD_DIVERGENCE and not is_value_sniper:
|
||||
score -= 42.0
|
||||
@@ -348,22 +601,10 @@ class BettingBrain:
|
||||
score += 11.0
|
||||
positives.append("v25_v27_aligned")
|
||||
|
||||
# Trap market detection: market overpriced vs historical band hit rate
|
||||
trap_market_flag = False
|
||||
trap_market_gap = None
|
||||
if isinstance(triple, dict):
|
||||
band_rate_val = self._safe_float(triple.get("band_rate"))
|
||||
implied_val = self._safe_float(triple.get("implied_prob"))
|
||||
if (
|
||||
band_rate_val is not None
|
||||
and implied_val is not None
|
||||
and band_sample >= self.MIN_BAND_SAMPLE
|
||||
and (implied_val - band_rate_val) > self.TRAP_MARKET_GAP
|
||||
):
|
||||
trap_market_flag = True
|
||||
trap_market_gap = round(implied_val - band_rate_val, 4)
|
||||
score -= 14.0
|
||||
issues.append("trap_market_market_overpriced")
|
||||
# Trap market score penalty (flag computed above, before tier check)
|
||||
if trap_market_flag:
|
||||
score -= 14.0
|
||||
issues.append("trap_market_market_overpriced")
|
||||
|
||||
if isinstance(triple, dict):
|
||||
if triple_is_value:
|
||||
@@ -465,6 +706,120 @@ class BettingBrain:
|
||||
if sniper_bypassed:
|
||||
positives.append("sniper_bypassed_soft_vetoes")
|
||||
|
||||
# ── V31d: VALUE-TIER REGIME ──────────────────────────────────────
|
||||
# A matched MS value tier is the validated profitable signal (60d:
|
||||
# premium 6.0-7.5 → +32.7% ROI). Underdogs are bet on the odds
|
||||
# premium, not on win-probability, so:
|
||||
# (1) waive the two favorite-confidence vetoes (genuine safety
|
||||
# vetoes — extreme_neg_ev, ev_too_high_trap, htft_reversal
|
||||
# _risk_high, v25_v27_hard_disagreement, low_reliability_hard
|
||||
# — are NOT waived and still reject);
|
||||
# (2) replace the favorite-confidence SCORE with a value floor so
|
||||
# premium can clear MIN_BET_SCORE while strong/standard stay
|
||||
# WATCH-level. All rich analysis output is untouched.
|
||||
value_tier_bypassed: List[str] = []
|
||||
if is_value_tier:
|
||||
if vetoes:
|
||||
remaining = []
|
||||
for v in vetoes:
|
||||
if v in self.VALUE_TIER_BYPASSABLE_VETOES:
|
||||
value_tier_bypassed.append(v)
|
||||
else:
|
||||
remaining.append(v)
|
||||
vetoes = remaining
|
||||
if value_tier_bypassed:
|
||||
positives.append("value_tier_bypassed_favorite_vetoes")
|
||||
# Value-regime score: floor by tier quality + small +EV nudge.
|
||||
value_score = self.VALUE_TIER_BASE_SCORE.get(tier_value, 50.0)
|
||||
value_score += max(-5.0, min(10.0, ev_edge * 35.0))
|
||||
if odds_rel >= 0.45:
|
||||
value_score += 3.0
|
||||
# Only premium is auto-staked; cap the rest below MIN_BET_SCORE
|
||||
# so they surface as WATCH (visible analysis, not a staked bet).
|
||||
if tier_value != "premium":
|
||||
value_score = min(value_score, 60.0)
|
||||
score = value_score
|
||||
positives.append(f"value_tier_{tier_value}")
|
||||
|
||||
# ── 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))
|
||||
action = "BET"
|
||||
if vetoes:
|
||||
@@ -487,8 +842,12 @@ class BettingBrain:
|
||||
"issues": issues[:6],
|
||||
"vetoes": vetoes[:6],
|
||||
"sniper_bypassed": sniper_bypassed,
|
||||
"value_tier_bypassed": value_tier_bypassed, # V31d
|
||||
"is_value_tier": is_value_tier, # V31d
|
||||
"trap_market_flag": trap_market_flag,
|
||||
"trap_market_gap": trap_market_gap,
|
||||
"tier_label": tier_label,
|
||||
"value_tier": tier_value, # V31c: premium/strong/standard
|
||||
"model_prob": round(model_prob, 4) if model_prob is not None else None,
|
||||
"implied_prob": round(implied, 4),
|
||||
"model_market_gap": round(model_gap, 4) if model_gap is not None else None,
|
||||
@@ -501,10 +860,15 @@ class BettingBrain:
|
||||
if action != "BET":
|
||||
self._force_no_bet(row, f"betting_brain_{action.lower()}")
|
||||
else:
|
||||
row["is_guaranteed"] = bool(score >= 82.0)
|
||||
# V31d: value-tier underdogs are high-variance (~20% hit) — never
|
||||
# label them "guaranteed" no matter how high the value score is.
|
||||
row["is_guaranteed"] = bool(score >= 82.0) and not is_value_tier
|
||||
row["pick_reason"] = "betting_brain_approved"
|
||||
row["stake_units"] = self._brain_stake(row, score)
|
||||
row["bet_grade"] = "A" if score >= 82.0 else "B"
|
||||
# V31c: bet_grade now reflects value_tier so the UI can show
|
||||
# the bettor which quality band a pick belongs to.
|
||||
row["bet_grade"] = self._grade_from_tier(tier_value, score)
|
||||
row["value_tier"] = tier_value
|
||||
row["playable"] = True
|
||||
|
||||
self._append_reason(row, f"betting_brain_{action.lower()}_{round(score)}")
|
||||
@@ -559,9 +923,13 @@ class BettingBrain:
|
||||
prob = self._safe_float(probs.get(pick), 0.0)
|
||||
if prob is None or prob <= 0.0:
|
||||
continue
|
||||
implied_odd = round(1.0 / prob, 2) if prob > 0.01 else 0.0
|
||||
ref_odd = existing_odds_by_pick.get(pick) or implied_odd
|
||||
rows[key] = {
|
||||
# V35c: only REAL bookmaker odds may be displayed. The old fallback
|
||||
# showed synthetic fair-odds (1/prob) as "Oran" — users could read
|
||||
# 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",
|
||||
"pick": pick,
|
||||
"probability": round(prob, 4),
|
||||
@@ -575,6 +943,12 @@ class BettingBrain:
|
||||
"bet_grade": "PASS",
|
||||
"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
|
||||
def _merge_row(existing: Optional[Dict[str, Any]], incoming: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@@ -600,12 +974,14 @@ class BettingBrain:
|
||||
|
||||
def _summary_item(self, row: Dict[str, Any]) -> Dict[str, Any]:
|
||||
reasons = list(row.get("decision_reasons") or row.get("reasons") or [])
|
||||
brain = row.get("betting_brain") or {}
|
||||
return {
|
||||
"market": row.get("market"),
|
||||
"pick": row.get("pick"),
|
||||
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
|
||||
"calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
|
||||
"bet_grade": row.get("bet_grade", "PASS"),
|
||||
"value_tier": row.get("value_tier") or brain.get("value_tier"), # V31c
|
||||
"playable": bool(row.get("playable")),
|
||||
"stake_units": float(row.get("stake_units", 0.0) or 0.0),
|
||||
"play_score": row.get("play_score", 0.0),
|
||||
@@ -615,9 +991,22 @@ class BettingBrain:
|
||||
"odds": row.get("odds", 0.0),
|
||||
"reasons": reasons[:6],
|
||||
"is_underdog_reference": bool(row.get("is_underdog_reference")),
|
||||
"betting_brain": row.get("betting_brain"),
|
||||
"betting_brain": brain,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _grade_from_tier(value_tier: Optional[str], score: float) -> str:
|
||||
"""V31c: Map value_tier → bet grade so the UI surfaces the
|
||||
quality band. Falls back to score-based grade for untiered picks.
|
||||
premium → A (deep underdog, highest ROI, high variance)
|
||||
strong → B (strong underdog)
|
||||
standard → C (volume zone, thin edge)
|
||||
"""
|
||||
mapping = {"premium": "A", "strong": "B", "standard": "C"}
|
||||
if value_tier in mapping:
|
||||
return mapping[value_tier]
|
||||
return "A" if score >= 82.0 else "B"
|
||||
|
||||
@staticmethod
|
||||
def _candidate_sort_key(row: Dict[str, Any]) -> Tuple[float, float, float]:
|
||||
brain = row.get("betting_brain") or {}
|
||||
@@ -657,6 +1046,12 @@ class BettingBrain:
|
||||
odds = self._safe_float(row.get("odds"), 0.0) or 0.0
|
||||
if odds <= 1.0:
|
||||
return 0.0
|
||||
# V31d: value-tier underdogs use a small FLAT stake (high variance),
|
||||
# not the score-scaled favorite stake. score is high (70+) by design
|
||||
# but that reflects validated tier EV, not win-probability.
|
||||
brain = row.get("betting_brain") or {}
|
||||
if brain.get("is_value_tier") or brain.get("value_tier") in self.VALUE_TIER_NAMES:
|
||||
return self.VALUE_TIER_STAKE_UNITS
|
||||
cap = 2.0 if score >= 82.0 else 1.2
|
||||
if score < 78.0:
|
||||
cap = 0.8
|
||||
|
||||
@@ -56,10 +56,81 @@ from services.match_commentary import generate_match_commentary
|
||||
from utils.top_leagues import load_top_league_ids
|
||||
from utils.league_reliability import load_league_reliability
|
||||
from config.config_loader import build_threshold_dict, get_threshold_default
|
||||
from models.calibration import get_calibrator
|
||||
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:
|
||||
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(
|
||||
self,
|
||||
data: MatchData,
|
||||
@@ -280,6 +351,34 @@ class MarketBoardMixin:
|
||||
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
|
||||
_resp_status = str(data.status or "").upper()
|
||||
_resp_state = str(data.state or "").upper()
|
||||
@@ -294,6 +393,13 @@ class MarketBoardMixin:
|
||||
"home_team": data.home_team_name,
|
||||
"away_team": data.away_team_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,
|
||||
"sport": data.sport,
|
||||
# Live snapshot — match_commentary uses this to detect upset-in-progress
|
||||
@@ -332,15 +438,24 @@ class MarketBoardMixin:
|
||||
"bet_summary": bet_summary,
|
||||
"supporting_picks": supporting,
|
||||
"aggressive_pick": aggressive_pick,
|
||||
"scenario_top5": prediction.ft_scores_top5,
|
||||
"score_prediction": {
|
||||
"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),
|
||||
},
|
||||
"scenario_top5": (
|
||||
cal_score["scenario_top5"] if cal_score else prediction.ft_scores_top5
|
||||
),
|
||||
"score_prediction": (
|
||||
cal_score["score_prediction"]
|
||||
if cal_score
|
||||
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,
|
||||
# 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": {
|
||||
"handicap": prediction.handicap_pick,
|
||||
"cards": {
|
||||
@@ -942,6 +1057,301 @@ class MarketBoardMixin:
|
||||
}
|
||||
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(
|
||||
self,
|
||||
data: MatchData,
|
||||
@@ -1114,10 +1524,31 @@ class MarketBoardMixin:
|
||||
if cal_key and cal_key in calibrator.calibrators:
|
||||
cal_input = max(0.001, min(0.999, raw_conf / 100.0))
|
||||
cal_prob = calibrator.calibrate(cal_key, cal_input, odds_val=odd if odd > 1.0 else None)
|
||||
# V30: Trust-based blending — some calibrators inflate probabilities.
|
||||
# Blend isotonic output with raw model based on calibrator accuracy.
|
||||
trust = POST_CAL_TRUST.get(cal_key, 0.5)
|
||||
cal_prob = trust * cal_prob + (1.0 - trust) * cal_input
|
||||
calibrated_conf = max(1.0, min(99.0, cal_prob * 100.0))
|
||||
else:
|
||||
multiplier = self.market_calibration.get(market, 0.85)
|
||||
calibrated_conf = max(1.0, min(99.0, raw_conf * multiplier))
|
||||
# V31b: Fallback for markets WITHOUT isotonic calibrator.
|
||||
# Old approach used aggressive multipliers (0.58-0.85) causing
|
||||
# massive deflation: HT_OU15 -40.5%, HT_OU05 -25.2%, OE -18.3%.
|
||||
# New approach: mild damping (0.92) acknowledges slight model
|
||||
# overconfidence without destroying probability signal.
|
||||
# The tier system (V31b) is the real profitability gatekeeper.
|
||||
calibrated_conf = max(1.0, min(99.0, raw_conf * 0.92))
|
||||
|
||||
# ── 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)
|
||||
|
||||
implied_prob = (1.0 / odd) if odd > 1.0 else 0.0
|
||||
@@ -1178,9 +1609,11 @@ class MarketBoardMixin:
|
||||
reasons: List[str] = []
|
||||
playable = True
|
||||
|
||||
# V34: Broadened value_sniper bypass — odds-aware model rarely shows 3% EV edge
|
||||
# Allow high-confidence predictions OR modest positive EV to bypass secondary gates
|
||||
is_value_sniper = ev_edge >= 0.008 or calibrated_conf >= 55.0
|
||||
# V29b: Permissive upstream — let betting_brain's tiered system do the real filtering.
|
||||
# Old threshold (ev>=0.008 OR conf>=55) let everything through AND bypassed brain vetoes.
|
||||
# New approach: let most picks through market_board, but brain's MARKET_ODDS_TIERS
|
||||
# + hard vetoes (neg EV, muted, low reliability) handle the intelligent filtering.
|
||||
is_value_sniper = calibrated_conf >= 45.0
|
||||
|
||||
if calibrated_conf < min_conf:
|
||||
if not is_value_sniper:
|
||||
@@ -1283,11 +1716,50 @@ class MarketBoardMixin:
|
||||
stake_units = 0.25 # minimum stake (conservative)
|
||||
reasons.append("no_ev_edge_minimum_stake")
|
||||
|
||||
# ── V30: Birleşik Güven Skoru (BGS) ────────────────────────────
|
||||
# A single, honest metric for users: quality-adjusted win probability.
|
||||
# Combines calibrated probability with data quality signals.
|
||||
# Correlation analysis: model_gap r=-0.12, trap negative, reliability weak positive.
|
||||
bgs = calibrated_conf # POST_CAL_TRUST corrected base
|
||||
model_gap = prob - implied_prob if implied_prob > 0 else 0.0
|
||||
# Penalty when model overestimates vs market (r=-0.12 correlation)
|
||||
if model_gap > 0.05:
|
||||
bgs -= 8.0
|
||||
elif model_gap > 0.0:
|
||||
bgs -= 3.0
|
||||
# Trap market detection: implied prob significantly above historical band rate
|
||||
is_trap_signal = False
|
||||
if band_available and band_prob > 0 and implied_prob > 0:
|
||||
is_trap_signal = (implied_prob - band_prob) > 0.10
|
||||
if is_trap_signal:
|
||||
bgs -= 7.0
|
||||
# League reliability adjustment (±2)
|
||||
bgs += (odds_rel - 0.50) * 4.0
|
||||
# Band alignment
|
||||
if band_available:
|
||||
if bool(band_verdict.get("aligned")):
|
||||
bgs += 2.0
|
||||
else:
|
||||
bgs -= 3.0
|
||||
# BGS label for frontend
|
||||
bgs = max(1.0, min(99.0, bgs))
|
||||
if bgs >= 70:
|
||||
bgs_label = "very_reliable"
|
||||
elif bgs >= 55:
|
||||
bgs_label = "reliable"
|
||||
elif bgs >= 40:
|
||||
bgs_label = "moderate"
|
||||
else:
|
||||
bgs_label = "low"
|
||||
|
||||
out = dict(row)
|
||||
out.update(
|
||||
{
|
||||
"raw_confidence": round(raw_conf, 1),
|
||||
"calibrated_confidence": round(calibrated_conf, 1),
|
||||
"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_play_score": round(min_play_score, 1),
|
||||
"min_required_edge": round(min_edge, 4),
|
||||
@@ -1347,6 +1819,8 @@ class MarketBoardMixin:
|
||||
"pick": row.get("pick"),
|
||||
"raw_confidence": row.get("raw_confidence", row.get("confidence")),
|
||||
"calibrated_confidence": row.get("calibrated_confidence", row.get("confidence")),
|
||||
"unified_score": row.get("unified_score", row.get("calibrated_confidence", 0.0)),
|
||||
"unified_score_label": row.get("unified_score_label", "moderate"),
|
||||
"bet_grade": row.get("bet_grade", "PASS"),
|
||||
"playable": bool(row.get("playable")),
|
||||
"stake_units": float(row.get("stake_units", 0.0)),
|
||||
|
||||
@@ -60,8 +60,23 @@ from models.calibration import get_calibrator
|
||||
|
||||
|
||||
class UpperBrainMixin:
|
||||
def _apply_upper_brain_guards(self, package: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return BettingBrain().judge(package)
|
||||
def _apply_upper_brain_guards(
|
||||
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")
|
||||
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 utils.top_leagues import load_top_league_ids
|
||||
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 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.top_league_ids = load_top_league_ids()
|
||||
self.league_reliability = load_league_reliability()
|
||||
self.league_confidence = load_league_confidence()
|
||||
self.national_leagues = load_national_leagues()
|
||||
self.enrichment = FeatureEnrichmentService()
|
||||
self.odds_band_analyzer = OddsBandAnalyzer()
|
||||
# ── Market Thresholds (loaded from config/market_thresholds.json) ──
|
||||
@@ -664,7 +668,28 @@ class SingleMatchOrchestrator(
|
||||
base_package.setdefault("analysis_details", {})
|
||||
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 ──────────────
|
||||
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,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ı).
|
||||
@@ -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);
|
||||
+23
-1
@@ -46,5 +46,27 @@
|
||||
"722fdbecxzcq9788l6jqclzlw",
|
||||
"8ey0ww2zsosdmwr8ehsorh6t7",
|
||||
"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);
|
||||
Executable
+20
@@ -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
|
||||
import { MatchesModule } from "./modules/matches/matches.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 { AnalysisModule } from "./modules/analysis/analysis.module";
|
||||
import { CouponsModule } from "./modules/coupons/coupons.module";
|
||||
@@ -200,6 +201,7 @@ const historicalFeederMode = process.env.FEEDER_MODE === "historical";
|
||||
// Sports Domain Modules
|
||||
MatchesModule,
|
||||
PredictionsModule,
|
||||
ValueBoardModule,
|
||||
LeaguesModule,
|
||||
AnalysisModule,
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
// ================== 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;
|
||||
raw_confidence: number;
|
||||
calibrated_confidence: number;
|
||||
unified_score: number;
|
||||
unified_score_label: 'very_reliable' | 'reliable' | 'moderate' | 'low';
|
||||
min_required_confidence: number;
|
||||
edge: number;
|
||||
play_score: number;
|
||||
@@ -35,6 +37,8 @@ export interface PredictionBetSummaryRow {
|
||||
pick: string;
|
||||
raw_confidence: number;
|
||||
calibrated_confidence: number;
|
||||
unified_score: number;
|
||||
unified_score_label: 'very_reliable' | 'reliable' | 'moderate' | 'low';
|
||||
bet_grade: BetGrade;
|
||||
playable: boolean;
|
||||
stake_units: number;
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
DbMarketPayload,
|
||||
BasketballTeamStats,
|
||||
} from "./feeder.types";
|
||||
import { ImageUtils } from "../../common/utils/image.util";
|
||||
import { deriveStoredMatchStatus } from "../../common/utils/match-status.util";
|
||||
|
||||
@Injectable()
|
||||
@@ -164,24 +163,6 @@ export class FeederPersistenceService {
|
||||
oddsArray: DbMarketPayload[],
|
||||
officialsData: MatchOfficial[],
|
||||
): 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 = [
|
||||
{
|
||||
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
|
||||
try {
|
||||
await this.prisma.$transaction(
|
||||
@@ -264,7 +231,6 @@ export class FeederPersistenceService {
|
||||
countryId: countryId,
|
||||
sport: sport,
|
||||
competitionSlug: league.competitionSlug,
|
||||
logoUrl: `/uploads/competitions/${finalLeagueId}.png`,
|
||||
} as any,
|
||||
});
|
||||
if (league.sortOrder !== undefined) {
|
||||
@@ -291,10 +257,7 @@ export class FeederPersistenceService {
|
||||
|
||||
if (teamsToCreate.length > 0) {
|
||||
await tx.team.createMany({
|
||||
data: teamsToCreate.map((t) => ({
|
||||
...t,
|
||||
logoUrl: `/uploads/teams/${t.id}.png`,
|
||||
})),
|
||||
data: teamsToCreate,
|
||||
skipDuplicates: true,
|
||||
});
|
||||
}
|
||||
@@ -304,7 +267,6 @@ export class FeederPersistenceService {
|
||||
where: { id: team.id },
|
||||
data: {
|
||||
name: team.name,
|
||||
logoUrl: `/uploads/teams/${team.id}.png`,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -614,9 +576,6 @@ export class FeederPersistenceService {
|
||||
{ maxWait: 40000, timeout: 40000 },
|
||||
);
|
||||
|
||||
// WAIT FOR IMAGES AFTER TRANSACTION
|
||||
await Promise.allSettled(imageDownloads);
|
||||
|
||||
this.logger.log(`✅ SAVED: [${matchId}] ${matchSummary.matchName}`);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import { Sport } from "@prisma/client";
|
||||
import {
|
||||
countryFlagUrl,
|
||||
teamLogoUrl,
|
||||
} from "../../common/utils/image-url.util";
|
||||
|
||||
@Injectable()
|
||||
export class LeaguesService {
|
||||
@@ -12,19 +16,21 @@ export class LeaguesService {
|
||||
* Get all countries
|
||||
*/
|
||||
async findAllCountries() {
|
||||
return this.prisma.country.findMany({
|
||||
const countries = await this.prisma.country.findMany({
|
||||
orderBy: { name: "asc" },
|
||||
});
|
||||
return countries.map((c) => ({ ...c, flag: countryFlagUrl(c.id) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country by ID
|
||||
*/
|
||||
async findCountryById(id: string) {
|
||||
return this.prisma.country.findUnique({
|
||||
const country = await this.prisma.country.findUnique({
|
||||
where: { id },
|
||||
include: { leagues: true },
|
||||
});
|
||||
return country ? { ...country, flag: countryFlagUrl(country.id) } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,7 +72,7 @@ export class LeaguesService {
|
||||
* Get all teams
|
||||
*/
|
||||
async findAllTeams(sport?: Sport, search?: string) {
|
||||
return this.prisma.team.findMany({
|
||||
const teams = await this.prisma.team.findMany({
|
||||
where: {
|
||||
...(sport ? { sport } : {}),
|
||||
...(search ? { name: { contains: search, mode: "insensitive" } } : {}),
|
||||
@@ -74,28 +80,31 @@ export class LeaguesService {
|
||||
orderBy: { name: "asc" },
|
||||
take: 100,
|
||||
});
|
||||
return teams.map((t) => ({ ...t, logo: teamLogoUrl(t.id) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get team by ID
|
||||
*/
|
||||
async findTeamById(id: string) {
|
||||
return this.prisma.team.findUnique({
|
||||
const team = await this.prisma.team.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
return team ? { ...team, logo: teamLogoUrl(team.id) } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search teams by name
|
||||
*/
|
||||
async searchTeams(name: string, sport?: Sport) {
|
||||
return this.prisma.team.findMany({
|
||||
const teams = await this.prisma.team.findMany({
|
||||
where: {
|
||||
name: { contains: name, mode: "insensitive" },
|
||||
...(sport ? { sport } : {}),
|
||||
},
|
||||
take: 20,
|
||||
});
|
||||
return teams.map((t) => ({ ...t, logo: teamLogoUrl(t.id) }));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,13 +170,9 @@ export class LeaguesService {
|
||||
status: m.status,
|
||||
state: m.state,
|
||||
homeTeamName: m.homeTeam?.name,
|
||||
homeTeamLogo: m.homeTeamId
|
||||
? `https://file.mackolikfeeds.com/teams/${m.homeTeamId}`
|
||||
: null,
|
||||
homeTeamLogo: teamLogoUrl(m.homeTeamId) ?? null,
|
||||
awayTeamName: m.awayTeam?.name,
|
||||
awayTeamLogo: m.awayTeamId
|
||||
? `https://file.mackolikfeeds.com/teams/${m.awayTeamId}`
|
||||
: null,
|
||||
awayTeamLogo: teamLogoUrl(m.awayTeamId) ?? null,
|
||||
leagueName: m.league?.name,
|
||||
countryName: m.league?.country?.name,
|
||||
})),
|
||||
|
||||
@@ -111,6 +111,22 @@ export class MatchesController {
|
||||
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 full match details
|
||||
|
||||
@@ -16,6 +16,10 @@ import {
|
||||
LIVE_STATUS_VALUES_FOR_DB,
|
||||
getDisplayMatchStatus,
|
||||
} from "../../common/utils/match-status.util";
|
||||
import {
|
||||
countryFlagUrl,
|
||||
teamLogoUrl,
|
||||
} from "../../common/utils/image-url.util";
|
||||
|
||||
@Injectable()
|
||||
export class MatchesService {
|
||||
@@ -28,6 +32,51 @@ export class MatchesService {
|
||||
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() {
|
||||
try {
|
||||
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 {
|
||||
if (!countryId) return undefined;
|
||||
return `https://file.mackolikfeeds.com/areas/${countryId}`;
|
||||
return countryFlagUrl(countryId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate URL for the team logo served from local uploads
|
||||
*/
|
||||
private getTeamLogoUrl(teamId?: string | null): string | undefined {
|
||||
if (!teamId) return undefined;
|
||||
return `https://file.mackolikfeeds.com/teams/${teamId}`;
|
||||
return teamLogoUrl(teamId);
|
||||
}
|
||||
|
||||
private getLiveFilter(): Prisma.LiveMatchWhereInput {
|
||||
|
||||
@@ -27,6 +27,20 @@ export class MatchInfoDto {
|
||||
@ApiProperty({ required: false, default: false })
|
||||
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({
|
||||
required: false,
|
||||
enum: ["football", "basketball"],
|
||||
|
||||
@@ -1525,6 +1525,37 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
payload: MatchPredictionDto,
|
||||
): Promise<void> {
|
||||
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 payloadSummary = this.buildPredictionPayloadSummary(payload);
|
||||
await this.prisma.$executeRawUnsafe(
|
||||
@@ -1539,7 +1570,7 @@ export class PredictionsService implements OnModuleInit, OnModuleDestroy {
|
||||
VALUES ($1, $2, $3, $4::jsonb, $5::jsonb)
|
||||
`,
|
||||
matchId,
|
||||
String(payload.model_version || "unknown"),
|
||||
engineVersion,
|
||||
typeof payload.decision_trace_id === "string"
|
||||
? payload.decision_trace_id
|
||||
: 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 {
|
||||
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,
|
||||
calibration_version: payload.calibration_version ?? null,
|
||||
shadow_engine_version: payload.shadow_engine_version ?? null,
|
||||
|
||||
@@ -7,6 +7,11 @@ import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
import { ImageRendererService } from "./image-renderer.service";
|
||||
import {
|
||||
competitionLogoUrl,
|
||||
countryFlagUrl,
|
||||
teamLogoUrl,
|
||||
} from "../../common/utils/image-url.util";
|
||||
import { CaptionGeneratorService } from "./caption-generator.service";
|
||||
import { TwitterService } from "./twitter.service";
|
||||
import { MetaService } from "./meta.service";
|
||||
@@ -387,15 +392,15 @@ export class SocialPosterService {
|
||||
match.homeTeam?.name || prediction.match_info?.home_team || "Home",
|
||||
awayTeam:
|
||||
match.awayTeam?.name || prediction.match_info?.away_team || "Away",
|
||||
homeLogo: this.resolveLogoUrl(match.homeTeam?.logoUrl || ""),
|
||||
awayLogo: this.resolveLogoUrl(match.awayTeam?.logoUrl || ""),
|
||||
homeLogo: teamLogoUrl(match.homeTeamId) ?? "",
|
||||
awayLogo: teamLogoUrl(match.awayTeamId) ?? "",
|
||||
leagueName: match.league?.name || prediction.match_info?.league || "",
|
||||
leagueLogo: this.resolveLogoUrl(match.league?.logoUrl || ""),
|
||||
leagueLogo: competitionLogoUrl(match.leagueId) ?? "",
|
||||
countryName:
|
||||
match.league?.country?.name ||
|
||||
prediction.match_info?.country ||
|
||||
this.inferCountryName(match.league?.name || ""),
|
||||
countryFlag: this.resolveLogoUrl(match.league?.country?.flagUrl || ""),
|
||||
countryFlag: countryFlagUrl(match.league?.country?.id) ?? "",
|
||||
matchDate,
|
||||
htScore,
|
||||
ftScore,
|
||||
@@ -488,22 +493,6 @@ export class SocialPosterService {
|
||||
: "football";
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relative logo paths to full HTTP URLs.
|
||||
* On the deployed server, logos exist at public/uploads/teams/...
|
||||
* Locally during dev, we fetch them from the deployed server via APP_BASE_URL.
|
||||
*/
|
||||
private resolveLogoUrl(logoUrl: string): string {
|
||||
if (!logoUrl) return "";
|
||||
// Already a full URL
|
||||
if (logoUrl.startsWith("http")) return logoUrl;
|
||||
// Relative path → check local first, otherwise make full URL
|
||||
const localPath = path.join(process.cwd(), "public", logoUrl);
|
||||
if (fs.existsSync(localPath)) return logoUrl; // Keep relative, renderer reads local
|
||||
// Not local → prepend base URL for remote fetch
|
||||
return `${this.appBaseUrl}${logoUrl}`;
|
||||
}
|
||||
|
||||
private formatMatchDate(mstUtc: number | bigint): string {
|
||||
const d = new Date(Number(mstUtc));
|
||||
const months = [
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Pure de-vig helpers for the Value Board (no I/O, unit-testable).
|
||||
*
|
||||
* The bookmaker's decimal odds encode probability + margin. De-vigging removes
|
||||
* the margin: p_i = (1/odds_i) / Σ(1/odds_j). The result is the market's
|
||||
* "fair" probability — empirically calibrated to <2% ECE (see DATA_FINDINGS.md).
|
||||
* We NEVER fabricate numbers: if a market leg is missing/placeholder, return null.
|
||||
*/
|
||||
|
||||
export type OddsBlob = Record<string, Record<string, number | string>>;
|
||||
|
||||
/** Coerce a raw odds value (number or numeric string) to a finite number > 1.01. */
|
||||
function toOdd(v: number | string | undefined): number | null {
|
||||
if (v === undefined || v === null) return null;
|
||||
const n = typeof v === "number" ? v : parseFloat(String(v));
|
||||
return Number.isFinite(n) && n > 1.01 ? n : null;
|
||||
}
|
||||
|
||||
/** Vig-removed probabilities for a group of selections, in the given key order. */
|
||||
export function devig(
|
||||
market: Record<string, number | string> | undefined,
|
||||
keys: string[],
|
||||
): number[] | null {
|
||||
if (!market) return null;
|
||||
const odds = keys.map((k) => toOdd(market[k]));
|
||||
if (odds.some((o) => o === null)) return null;
|
||||
const inv = (odds as number[]).map((o) => 1 / o);
|
||||
const sum = inv.reduce((a, b) => a + b, 0);
|
||||
if (sum <= 0) return null;
|
||||
return inv.map((x) => x / sum);
|
||||
}
|
||||
|
||||
/** Bookmaker margin (overround) for a market, as a fraction (e.g. 0.19 = 19%). */
|
||||
export function overround(
|
||||
market: Record<string, number | string> | undefined,
|
||||
keys: string[],
|
||||
): number | null {
|
||||
if (!market) return null;
|
||||
const odds = keys.map((k) => toOdd(market[k]));
|
||||
if (odds.some((o) => o === null)) return null;
|
||||
return (odds as number[]).reduce((a, o) => a + 1 / o, 0) - 1;
|
||||
}
|
||||
|
||||
export interface ScoreDistribution {
|
||||
topScores: { score: string; prob: number }[];
|
||||
expectedGoals: number | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* De-vig the "Maç Skoru" (correct-score) market into a calibrated score
|
||||
* distribution. Normalises over the EXPLICIT scorelines (ignoring the "Diğer"
|
||||
* bucket for the displayed picks) and derives expected total goals from them.
|
||||
*/
|
||||
export function scoreDistribution(
|
||||
market: Record<string, number | string> | undefined,
|
||||
topN = 3,
|
||||
): ScoreDistribution {
|
||||
const empty: ScoreDistribution = { topScores: [], expectedGoals: null };
|
||||
if (!market) return empty;
|
||||
const explicit: { score: string; inv: number; goals: number }[] = [];
|
||||
for (const [score, raw] of Object.entries(market)) {
|
||||
const m = /^(\d+)-(\d+)$/.exec(score);
|
||||
const odd = toOdd(raw);
|
||||
if (!m || odd === null) continue; // skip "Diğer" and bad legs
|
||||
explicit.push({
|
||||
score,
|
||||
inv: 1 / odd,
|
||||
goals: parseInt(m[1], 10) + parseInt(m[2], 10),
|
||||
});
|
||||
}
|
||||
if (explicit.length === 0) return empty;
|
||||
const sum = explicit.reduce((a, e) => a + e.inv, 0);
|
||||
if (sum <= 0) return empty;
|
||||
const withProb = explicit
|
||||
.map((e) => ({ score: e.score, prob: e.inv / sum, goals: e.goals }))
|
||||
.sort((a, b) => b.prob - a.prob);
|
||||
const expectedGoals = withProb.reduce((a, e) => a + e.prob * e.goals, 0);
|
||||
return {
|
||||
topScores: withProb
|
||||
.slice(0, topN)
|
||||
.map((e) => ({ score: e.score, prob: e.prob })),
|
||||
expectedGoals: Math.round(expectedGoals * 100) / 100,
|
||||
};
|
||||
}
|
||||
|
||||
export function isCupLeague(name: string | null | undefined): boolean {
|
||||
const n = (name || "").toLowerCase();
|
||||
return ["kupa", "cup", "trophy"].some((w) => n.includes(w));
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Controller, Get, Query } from "@nestjs/common";
|
||||
import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { Public } from "../../common/decorators";
|
||||
import { ValueBoardService } from "./value-board.service";
|
||||
|
||||
@ApiTags("Value Board")
|
||||
@Controller("value-board")
|
||||
export class ValueBoardController {
|
||||
constructor(private readonly valueBoardService: ValueBoardService) {}
|
||||
|
||||
/**
|
||||
* GET /value-board
|
||||
* Upcoming matches with de-vigged (calibrated) probabilities — the honest,
|
||||
* transparent product board. No fabricated value; the bookmaker margin is
|
||||
* disclosed per match.
|
||||
*/
|
||||
@Public()
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: "Upcoming matches with calibrated (de-vigged) probabilities",
|
||||
})
|
||||
@ApiQuery({ name: "sport", required: false, type: String })
|
||||
@ApiResponse({ status: 200, description: "Value board for upcoming matches" })
|
||||
async getBoard(@Query("sport") sport?: string) {
|
||||
return this.valueBoardService.getBoard(sport || "football");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { DatabaseModule } from "../../database/database.module";
|
||||
import { ValueBoardController } from "./value-board.controller";
|
||||
import { ValueBoardService } from "./value-board.service";
|
||||
|
||||
@Module({
|
||||
imports: [DatabaseModule],
|
||||
controllers: [ValueBoardController],
|
||||
providers: [ValueBoardService],
|
||||
exports: [ValueBoardService],
|
||||
})
|
||||
export class ValueBoardModule {}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { PrismaService } from "../../database/prisma.service";
|
||||
import {
|
||||
devig,
|
||||
overround,
|
||||
scoreDistribution,
|
||||
isCupLeague,
|
||||
OddsBlob,
|
||||
} from "./devig.util";
|
||||
import { ValueBoardMatch } from "./value-board.types";
|
||||
|
||||
// Turkish market keys as stored in live_matches.odds
|
||||
const K = {
|
||||
MS: "Maç Sonucu",
|
||||
OU25: "2,5 Alt/Üst",
|
||||
BTTS: "Karşılıklı Gol",
|
||||
HT: "1. Yarı Sonucu",
|
||||
HT15: "1. Yarı 1,5 Alt/Üst",
|
||||
SCORE: "Maç Skoru",
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export class ValueBoardService {
|
||||
private readonly logger = new Logger(ValueBoardService.name);
|
||||
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
/**
|
||||
* Upcoming matches with de-vigged (calibrated) probabilities for the main
|
||||
* markets, the market's correct-score distribution, half-time markets and a
|
||||
* derived expected-goals figure. Honest baseline — no fabricated value.
|
||||
*/
|
||||
async getBoard(
|
||||
sport = "football",
|
||||
limit = 60,
|
||||
): Promise<ValueBoardMatch[]> {
|
||||
const now = BigInt(Date.now());
|
||||
const rows = await this.prisma.liveMatch.findMany({
|
||||
where: {
|
||||
sport,
|
||||
status: "NS",
|
||||
mstUtc: { gt: now },
|
||||
},
|
||||
include: {
|
||||
league: { include: { country: true } },
|
||||
homeTeam: true,
|
||||
awayTeam: true,
|
||||
},
|
||||
orderBy: { mstUtc: "asc" },
|
||||
take: limit,
|
||||
});
|
||||
|
||||
const board: ValueBoardMatch[] = [];
|
||||
for (const m of rows) {
|
||||
if (!m.odds || typeof m.odds !== "object" || Array.isArray(m.odds)) {
|
||||
continue;
|
||||
}
|
||||
const odds = m.odds as unknown as OddsBlob;
|
||||
|
||||
const ms = devig(odds[K.MS], ["1", "X", "2"]);
|
||||
if (!ms) continue; // no real MS price → don't show (never fabricate)
|
||||
|
||||
const ou = devig(odds[K.OU25], ["Alt", "Üst"]);
|
||||
const bt = devig(odds[K.BTTS], ["Var", "Yok"]);
|
||||
const ht = devig(odds[K.HT], ["1", "X", "2"]);
|
||||
const ht15 = devig(odds[K.HT15], ["Alt", "Üst"]);
|
||||
const score = scoreDistribution(odds[K.SCORE]);
|
||||
const vig = overround(odds[K.MS], ["1", "X", "2"]);
|
||||
|
||||
const leanIdx = ms.indexOf(Math.max(...ms));
|
||||
const leanKey = (["1", "X", "2"] as const)[leanIdx];
|
||||
|
||||
board.push({
|
||||
id: m.id,
|
||||
matchName:
|
||||
m.matchName || `${m.homeTeam?.name ?? "?"} vs ${m.awayTeam?.name ?? "?"}`,
|
||||
homeTeam: m.homeTeam?.name ?? "?",
|
||||
awayTeam: m.awayTeam?.name ?? "?",
|
||||
league: m.league?.name ?? "—",
|
||||
country: m.league?.country?.name ?? undefined,
|
||||
kickoff: Number(m.mstUtc ?? 0),
|
||||
isCup: isCupLeague(m.league?.name),
|
||||
vigPct: vig === null ? null : Math.round(vig * 1000) / 10,
|
||||
ms: { home: ms[0], draw: ms[1], away: ms[2] },
|
||||
ou25: ou ? { over: ou[1], under: ou[0] } : null,
|
||||
btts: bt ? { yes: bt[0], no: bt[1] } : null,
|
||||
htResult: ht ? { home: ht[0], draw: ht[1], away: ht[2] } : null,
|
||||
htOu15: ht15 ? { over: ht15[1], under: ht15[0] } : null,
|
||||
topScores: score.topScores,
|
||||
expectedGoals: score.expectedGoals,
|
||||
lean: { key: leanKey, prob: ms[leanIdx] },
|
||||
});
|
||||
}
|
||||
return board;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
export interface ThreeWayProbs {
|
||||
home: number;
|
||||
draw: number;
|
||||
away: number;
|
||||
}
|
||||
|
||||
export interface TwoWayProbs {
|
||||
over: number;
|
||||
under: number;
|
||||
}
|
||||
|
||||
export interface ScoreProb {
|
||||
score: string;
|
||||
prob: number;
|
||||
}
|
||||
|
||||
export interface ValueBoardMatch {
|
||||
id: string;
|
||||
matchName: string;
|
||||
homeTeam: string;
|
||||
awayTeam: string;
|
||||
league: string;
|
||||
country?: string;
|
||||
kickoff: number; // mst_utc (epoch ms)
|
||||
isCup: boolean;
|
||||
vigPct: number | null; // bookmaker margin on the MS market, %
|
||||
ms: ThreeWayProbs | null; // calibrated Maç Sonucu
|
||||
ou25: TwoWayProbs | null; // 2.5 Alt/Üst
|
||||
btts: { yes: number; no: number } | null; // Karşılıklı Gol
|
||||
htResult: ThreeWayProbs | null; // 1. Yarı Sonucu
|
||||
htOu15: TwoWayProbs | null; // 1. Yarı 1,5 Alt/Üst
|
||||
topScores: ScoreProb[]; // calibrated Maç Skoru (top 3)
|
||||
expectedGoals: number | null; // derived from the score distribution
|
||||
lean: { key: "1" | "X" | "2"; prob: number } | null; // most likely MS pick
|
||||
}
|
||||
+185
-21
@@ -23,9 +23,8 @@ import {
|
||||
import { TaskLockService } from "./task-lock.service";
|
||||
import { FeederService } from "../modules/feeder/feeder.service";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Types
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface LiveScoreTeamPayload {
|
||||
id: string;
|
||||
@@ -93,9 +92,9 @@ interface PendingPredictionRunForSettlement {
|
||||
|
||||
type SportType = "football" | "basketball";
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
// Service
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class DataFetcherTask {
|
||||
@@ -108,7 +107,7 @@ export class DataFetcherTask {
|
||||
private readonly scraper: FeederScraperService,
|
||||
private readonly taskLock: TaskLockService,
|
||||
private readonly feeder: FeederService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
// ────────────────────────────────────────────────────────────
|
||||
// CRON 1: Main sync — every 15 minutes
|
||||
@@ -200,10 +199,20 @@ export class DataFetcherTask {
|
||||
|
||||
this.logger.log("syncLiveMatches START");
|
||||
|
||||
// 4-day forward window: pull today .. +3 days so upcoming matches enter
|
||||
// live_matches early and their odds are refreshed every cycle. That rolling
|
||||
// refresh is what lets the odds-movement / steam monitor (and forward CLV)
|
||||
// see a real opening→closing range. Finished/live matches are already
|
||||
// excluded from odds re-fetch in fetchOddsForMatches(), so closed-match
|
||||
// odds and their ranges are never re-pulled.
|
||||
const SYNC_DAYS_AHEAD = 4;
|
||||
const today = getDateStringInTimeZone(new Date(), this.timeZone);
|
||||
const tomorrow = getShiftedDateStringInTimeZone(1, this.timeZone);
|
||||
await this.syncMatchList(today);
|
||||
await this.syncMatchList(tomorrow);
|
||||
for (let dayOffset = 1; dayOffset < SYNC_DAYS_AHEAD; dayOffset++) {
|
||||
await this.syncMatchList(
|
||||
getShiftedDateStringInTimeZone(dayOffset, this.timeZone),
|
||||
);
|
||||
}
|
||||
await this.updateLiveScores();
|
||||
await this.archiveNewlyFinishedMatches(today);
|
||||
await this.settlePredictionRuns();
|
||||
@@ -324,13 +333,13 @@ export class DataFetcherTask {
|
||||
const scoreAway = matchData.awayScore ?? null;
|
||||
const htScoreHome = this.asInt(
|
||||
matchData.score?.ht?.home ??
|
||||
matchData.htHomeScore ??
|
||||
matchData.homeHtScore,
|
||||
matchData.htHomeScore ??
|
||||
matchData.homeHtScore,
|
||||
);
|
||||
const htScoreAway = this.asInt(
|
||||
matchData.score?.ht?.away ??
|
||||
matchData.htAwayScore ??
|
||||
matchData.awayHtScore,
|
||||
matchData.htAwayScore ??
|
||||
matchData.awayHtScore,
|
||||
);
|
||||
const storedStatus = deriveStoredMatchStatus({
|
||||
state: matchData.state,
|
||||
@@ -399,6 +408,12 @@ export class DataFetcherTask {
|
||||
const closingOddsSnapshot = await this.getClosingOddsSnapshot(
|
||||
row.matchId,
|
||||
);
|
||||
// ── Per-market settlement (V31e forward-test) ────────────────
|
||||
// Score EVERY captured market (not just main_pick) against reality,
|
||||
// so the admin Model Performance page can compute per-market
|
||||
// calibration (model% → actual%) and ROI. won=null → push (skip).
|
||||
const marketsSettled = this.settleAllMarkets(row);
|
||||
|
||||
const settlementSummary = {
|
||||
settled_at: new Date().toISOString(),
|
||||
model_version: row.engineVersion,
|
||||
@@ -413,6 +428,7 @@ export class DataFetcherTask {
|
||||
away: row.htScoreAway,
|
||||
},
|
||||
closing_odds_snapshot: closingOddsSnapshot,
|
||||
markets_settled: marketsSettled,
|
||||
};
|
||||
|
||||
await this.prisma.$executeRawUnsafe(
|
||||
@@ -538,6 +554,64 @@ export class DataFetcherTask {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* V31e forward-test: settle EVERY captured market (payload_summary.markets_full)
|
||||
* against the final score. Produces one compact record per market with its
|
||||
* shown probability, the real outcome, and flat profit — the raw material for
|
||||
* per-market calibration (model% vs actual%) on the admin dashboard.
|
||||
*/
|
||||
private settleAllMarkets(
|
||||
row: PendingPredictionRunForSettlement,
|
||||
): Array<Record<string, unknown>> {
|
||||
const summary = this.asRecord(row.payloadSummary);
|
||||
const markets = Array.isArray(summary.markets_full)
|
||||
? (summary.markets_full as unknown[])
|
||||
: [];
|
||||
const out: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (const raw of markets) {
|
||||
const m = this.asRecord(raw);
|
||||
const market = typeof m.market === "string" ? m.market : "";
|
||||
const pick = typeof m.pick === "string" ? m.pick : "";
|
||||
if (!market || !pick) continue;
|
||||
|
||||
const won = this.isPredictionPickWon({
|
||||
market,
|
||||
pick,
|
||||
scoreHome: row.scoreHome,
|
||||
scoreAway: row.scoreAway,
|
||||
htScoreHome: row.htScoreHome,
|
||||
htScoreAway: row.htScoreAway,
|
||||
});
|
||||
if (won === null) continue; // push / unresolvable → exclude from stats
|
||||
|
||||
const odds = Number(m.odds || 0);
|
||||
const hasOdds = Number.isFinite(odds) && odds > 1.01;
|
||||
out.push({
|
||||
market,
|
||||
pick,
|
||||
won,
|
||||
// shown probability for calibration (0–100). Prefer calibrated_confidence.
|
||||
shown_confidence:
|
||||
m.calibrated_confidence != null
|
||||
? Number(m.calibrated_confidence)
|
||||
: m.model_probability != null
|
||||
? Number(m.model_probability) * 100
|
||||
: null,
|
||||
model_probability:
|
||||
m.model_probability != null ? Number(m.model_probability) : null,
|
||||
odds: hasOdds ? odds : null,
|
||||
playable: m.playable === true,
|
||||
bet_grade: typeof m.bet_grade === "string" ? m.bet_grade : null,
|
||||
action: typeof m.action === "string" ? m.action : null,
|
||||
value_tier: typeof m.value_tier === "string" ? m.value_tier : null,
|
||||
// flat 1u profit if a real price existed (for per-market ROI)
|
||||
flat_profit: hasOdds ? Number((won ? odds - 1 : -1).toFixed(4)) : null,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private isPredictionPickWon(input: {
|
||||
market: string;
|
||||
pick: string;
|
||||
@@ -828,9 +902,9 @@ export class DataFetcherTask {
|
||||
]);
|
||||
const sidelined = match.matchSlug
|
||||
? await this.scraper.fetchSidelinedPlayers(
|
||||
match.id,
|
||||
match.matchSlug,
|
||||
)
|
||||
match.id,
|
||||
match.matchSlug,
|
||||
)
|
||||
: null;
|
||||
|
||||
// Normalize to same home.xi/away.xi format used by processMatchOdds
|
||||
@@ -904,9 +978,9 @@ export class DataFetcherTask {
|
||||
const targetMatches =
|
||||
topLeagueIds.size > 0
|
||||
? allMatches.filter(
|
||||
(m) =>
|
||||
!!m.competitionId && topLeagueIds.has(String(m.competitionId)),
|
||||
)
|
||||
(m) =>
|
||||
!!m.competitionId && topLeagueIds.has(String(m.competitionId)),
|
||||
)
|
||||
: allMatches;
|
||||
|
||||
if (targetMatches.length === 0) {
|
||||
@@ -1102,7 +1176,7 @@ export class DataFetcherTask {
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
})
|
||||
.catch(() => {});
|
||||
.catch(() => { });
|
||||
this.logger.debug(
|
||||
`[${sport}] Marked as POSTPONED: ${match.matchName}`,
|
||||
);
|
||||
@@ -1175,6 +1249,89 @@ export class DataFetcherTask {
|
||||
// (Preserved from original — no logic changes)
|
||||
// ────────────────────────────────────────────────────────────
|
||||
|
||||
// One-time guard: run CREATE TABLE IF NOT EXISTS only once per process.
|
||||
private static liveOddsTableReady = false;
|
||||
|
||||
/**
|
||||
* Persist live PRE-MATCH odds movement into a dedicated, FK-FREE table
|
||||
* (live_odds_history). Why not the structured odds_history table: that one is
|
||||
* tied to odd_categories.match_id, a FOREIGN KEY to `matches`. Upcoming matches
|
||||
* live ONLY in live_matches (not yet archived to `matches`), so any write to
|
||||
* odd_categories/odds_history for them fails the FK — silently losing all
|
||||
* pre-match movement. This table is keyed by raw match_id (no FK), so it
|
||||
* captures opening→closing movement for upcoming matches across ALL markets.
|
||||
* Change-only inserts; first capture per selection = opening (previous NULL).
|
||||
* (Finished-match closing odds are still captured in odd_selections/odds_history
|
||||
* by the archival / 08:00 job, which DO have the match in `matches`.)
|
||||
*/
|
||||
private async persistOddsHistory(
|
||||
matchId: string,
|
||||
odds: Record<string, Record<string, number>>,
|
||||
): Promise<void> {
|
||||
try {
|
||||
if (!DataFetcherTask.liveOddsTableReady) {
|
||||
await this.prisma.$executeRawUnsafe(
|
||||
`CREATE TABLE IF NOT EXISTS live_odds_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
match_id TEXT NOT NULL,
|
||||
market TEXT NOT NULL,
|
||||
selection TEXT NOT NULL,
|
||||
previous_value DOUBLE PRECISION,
|
||||
new_value DOUBLE PRECISION NOT NULL,
|
||||
change_time TIMESTAMPTZ NOT NULL DEFAULT now())`,
|
||||
);
|
||||
await this.prisma.$executeRawUnsafe(
|
||||
`CREATE INDEX IF NOT EXISTS idx_loh_match_time ON live_odds_history(match_id, change_time)`,
|
||||
);
|
||||
DataFetcherTask.liveOddsTableReady = true;
|
||||
}
|
||||
|
||||
// Last-known value per (market, selection) for this match.
|
||||
const last = await this.prisma.$queryRawUnsafe<
|
||||
Array<{ market: string; selection: string; new_value: number }>
|
||||
>(
|
||||
`SELECT DISTINCT ON (market, selection) market, selection, new_value
|
||||
FROM live_odds_history WHERE match_id = $1
|
||||
ORDER BY market, selection, change_time DESC`,
|
||||
matchId,
|
||||
);
|
||||
const lastMap = new Map<string, number>();
|
||||
for (const r of last) {
|
||||
lastMap.set(`${r.market}|${r.selection}`, Number(r.new_value));
|
||||
}
|
||||
|
||||
for (const [market, sels] of Object.entries(odds)) {
|
||||
for (const [selection, value] of Object.entries(sels)) {
|
||||
if (!(value > 0)) continue;
|
||||
const prev = lastMap.get(`${market}|${selection}`);
|
||||
if (prev === undefined) {
|
||||
// first capture for this selection = opening
|
||||
await this.prisma.$executeRawUnsafe(
|
||||
`INSERT INTO live_odds_history (match_id, market, selection, previous_value, new_value)
|
||||
VALUES ($1, $2, $3, NULL, $4)`,
|
||||
matchId, market, selection, value,
|
||||
);
|
||||
lastMap.set(`${market}|${selection}`, value);
|
||||
} else if (Math.abs(prev - value) > 1e-9) {
|
||||
// odds moved → log the movement
|
||||
await this.prisma.$executeRawUnsafe(
|
||||
`INSERT INTO live_odds_history (match_id, market, selection, previous_value, new_value)
|
||||
VALUES ($1, $2, $3, $4, $5)`,
|
||||
matchId, market, selection, prev, value,
|
||||
);
|
||||
lastMap.set(`${market}|${selection}`, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
// WARN (not debug): silent debug is exactly what hid the earlier FK failure.
|
||||
this.logger.warn(
|
||||
`live_odds_history persist failed for ${matchId}: ${message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async processMatchOdds(match: LiveMatchOddsTarget): Promise<void> {
|
||||
const matchSlug = match.matchSlug || "match";
|
||||
const sport = String(match.sport || "football").toLowerCase();
|
||||
@@ -1343,6 +1500,13 @@ export class DataFetcherTask {
|
||||
},
|
||||
});
|
||||
|
||||
// Log this pre-match odds refresh into live_odds_history (FK-free table) so
|
||||
// opening→closing movement & steam are queryable in the DB for UPCOMING
|
||||
// matches too (the structured odds_history can't — FK to `matches`).
|
||||
if (Object.keys(odds).length > 0) {
|
||||
await this.persistOddsHistory(match.id, odds);
|
||||
}
|
||||
|
||||
if (
|
||||
Object.keys(odds).length > 0 ||
|
||||
refereeName ||
|
||||
@@ -1586,9 +1750,9 @@ export class DataFetcherTask {
|
||||
|
||||
const score = this.isRecord(value.score)
|
||||
? {
|
||||
home: this.asInt(value.score.home),
|
||||
away: this.asInt(value.score.away),
|
||||
}
|
||||
home: this.asInt(value.score.home),
|
||||
away: this.asInt(value.score.away),
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
[
|
||||
"70excpe1synn9kadnbppahdn7",
|
||||
"482ofyysbdbeoxauk19yg7tdt",
|
||||
"2o9svokc5s7diish3ycrzk7jm",
|
||||
"7af85xa75vozt2l4hzi6ryts7",
|
||||
@@ -22,4 +23,4 @@
|
||||
"8yi6ejjd1zudcqtbn07haahg6",
|
||||
"4w7x0s5gfs5abasphlha5de8k",
|
||||
"e0lck99w8meo9qoalfrxgo33o"
|
||||
]
|
||||
]
|
||||
Binary file not shown.
+1
-1
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "dist-new", "**/*spec.ts"]
|
||||
"exclude": ["node_modules", "test", "dist", "dist-new", "workers", "**/*spec.ts"]
|
||||
}
|
||||
|
||||
+1
-1
@@ -22,5 +22,5 @@
|
||||
"strictBindCallApply": false,
|
||||
"noFallthroughCasesInSwitch": false
|
||||
},
|
||||
"exclude": ["node_modules", "dist", "dist-new", "test"]
|
||||
"exclude": ["node_modules", "dist", "dist-new", "test", "workers"]
|
||||
}
|
||||
|
||||
@@ -1,186 +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();
|
||||
|
||||
// Test with Club Brugge match
|
||||
const matchId = '7cnm7h7qbsq2bbaxngusojh90';
|
||||
|
||||
async function verifyDataUsage() {
|
||||
console.log('🔍 VERIFYING DATA USAGE IN AI ENGINE');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// 1. 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');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`\n📊 Match: ${match.homeTeam?.name} vs ${match.awayTeam?.name}`);
|
||||
console.log(`\n1️⃣ ODDS DATA:`);
|
||||
console.log(` Type: ${typeof match.odds}`);
|
||||
console.log(` Is null: ${match.odds === null}`);
|
||||
|
||||
if (match.odds) {
|
||||
const oddsStr = typeof match.odds === 'string' ? match.odds : JSON.stringify(match.odds);
|
||||
console.log(` Length: ${oddsStr.length} characters`);
|
||||
|
||||
// Parse and show summary
|
||||
try {
|
||||
const oddsObj = typeof match.odds === 'string' ? JSON.parse(match.odds) : match.odds;
|
||||
const markets = Object.keys(oddsObj);
|
||||
console.log(` Markets: ${markets.length}`);
|
||||
console.log(` Sample markets: ${markets.slice(0, 5).join(', ')}...`);
|
||||
console.log(` ✅ ODDS DATA: PRESENT`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ ODDS DATA: Invalid JSON`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ❌ ODDS DATA: NULL/MISSING`);
|
||||
}
|
||||
|
||||
console.log(`\n2️⃣ SIDELINED DATA:`);
|
||||
console.log(` Type: ${typeof match.sidelined}`);
|
||||
console.log(` Is null: ${match.sidelined === null}`);
|
||||
|
||||
if (match.sidelined) {
|
||||
const sidelinedStr = typeof match.sidelined === 'string' ? match.sidelined : JSON.stringify(match.sidelined);
|
||||
console.log(` Length: ${sidelinedStr.length} characters`);
|
||||
|
||||
try {
|
||||
const sidelinedObj = typeof match.sidelined === 'string' ? JSON.parse(match.sidelined) : match.sidelined;
|
||||
const homeTeam = sidelinedObj.homeTeam || sidelinedObj.home;
|
||||
const awayTeam = sidelinedObj.awayTeam || sidelinedObj.away;
|
||||
|
||||
console.log(` Home team sidelined: ${homeTeam?.totalSidelined || homeTeam?.players?.length || 0}`);
|
||||
console.log(` Away team sidelined: ${awayTeam?.totalSidelined || awayTeam?.players?.length || 0}`);
|
||||
console.log(` ✅ SIDELINED DATA: PRESENT`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ SIDELINED DATA: Invalid JSON`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ❌ SIDELINED DATA: NULL/MISSING`);
|
||||
}
|
||||
|
||||
console.log(`\n3️⃣ LINEUP DATA:`);
|
||||
console.log(` Type: ${typeof match.lineups}`);
|
||||
console.log(` Is null: ${match.lineups === null}`);
|
||||
|
||||
if (match.lineups) {
|
||||
try {
|
||||
const lineupsObj = typeof match.lineups === 'string' ? JSON.parse(match.lineups) : match.lineups;
|
||||
const homeCount = lineupsObj.stats?.home?.length || 0;
|
||||
const awayCount = lineupsObj.stats?.away?.length || 0;
|
||||
console.log(` Home lineup: ${homeCount}`);
|
||||
console.log(` Away lineup: ${awayCount}`);
|
||||
console.log(` ✅ LINEUP DATA: PRESENT`);
|
||||
} catch (e) {
|
||||
console.log(` ❌ LINEUP DATA: Invalid JSON`);
|
||||
}
|
||||
} else {
|
||||
console.log(` ❌ LINEUP DATA: NULL/MISSING`);
|
||||
}
|
||||
|
||||
// 2. Send prediction request
|
||||
console.log(`\n\n🤖 SENDING TO AI ENGINE...`);
|
||||
const aiEngineUrl = 'http://localhost:8007';
|
||||
const predictionUrl = `${aiEngineUrl}/v20plus/analyze/${matchId}`;
|
||||
|
||||
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;
|
||||
|
||||
// 3. Verify data quality
|
||||
console.log('📊 AI ENGINE DATA QUALITY:');
|
||||
const dq = pkg.data_quality;
|
||||
console.log(` Label: ${dq.label}`);
|
||||
console.log(` Score: ${dq.score}`);
|
||||
console.log(` Home lineup count: ${dq.home_lineup_count}`);
|
||||
console.log(` Away lineup count: ${dq.away_lineup_count}`);
|
||||
console.log(` Lineup source: ${dq.lineup_source}`);
|
||||
console.log(` Flags: ${dq.flags.join(', ') || 'None'}`);
|
||||
|
||||
// 4. Check if odds influenced the prediction
|
||||
console.log('\n📈 ENGINE BREAKDOWN (signal weights):');
|
||||
const eb = pkg.engine_breakdown;
|
||||
if (eb) {
|
||||
console.log(` Team signal: ${eb.team}%`);
|
||||
console.log(` Player signal: ${eb.player}%`);
|
||||
console.log(` Odds signal: ${eb.odds}%`);
|
||||
console.log(` Referee signal: ${eb.referee}%`);
|
||||
|
||||
if (eb.odds > 50) {
|
||||
console.log(` ✅ ODDS DATA: USED SIGNIFICANTLY (${eb.odds}%)`);
|
||||
} else if (eb.odds > 0) {
|
||||
console.log(` ⚠️ ODDS DATA: USED MINIMALLY (${eb.odds}%)`);
|
||||
} else {
|
||||
console.log(` ❌ ODDS DATA: NOT USED`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Check sidelined impact
|
||||
console.log('\n⚠️ SIDELINED IMPACT:');
|
||||
const reasoning = pkg.reasoning_factors || [];
|
||||
const hasSidelinedMention = reasoning.some((f: string) =>
|
||||
f.toLowerCase().includes('sideline') ||
|
||||
f.toLowerCase().includes('injury') ||
|
||||
f.toLowerCase().includes('absence') ||
|
||||
f.toLowerCase().includes('missing')
|
||||
);
|
||||
|
||||
if (hasSidelinedMention) {
|
||||
console.log(` ✅ SIDELINED DATA: MENTIONED IN REASONING`);
|
||||
reasoning.forEach((f: string) => {
|
||||
if (f.toLowerCase().includes('sideline') ||
|
||||
f.toLowerCase().includes('injury') ||
|
||||
f.toLowerCase().includes('absence') ||
|
||||
f.toLowerCase().includes('missing')) {
|
||||
console.log(` - ${f}`);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log(` ⚠️ SIDELINED DATA: No explicit mention (but may still be used internally)`);
|
||||
console.log(` Reasoning factors: ${reasoning.join(', ')}`);
|
||||
}
|
||||
|
||||
// 6. Main pick summary
|
||||
console.log('\n🎯 PREDICTION SUMMARY:');
|
||||
const mp = pkg.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('\n' + '='.repeat(80));
|
||||
console.log('✅ VERIFICATION COMPLETE');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
|
||||
verifyDataUsage().catch(console.error);
|
||||
@@ -0,0 +1 @@
|
||||
.wrangler/
|
||||
@@ -0,0 +1,51 @@
|
||||
# iddaai image proxy (Cloudflare Worker + R2)
|
||||
|
||||
Takım / lig / ülke görsellerini R2'den servis eden lazy-fill proxy.
|
||||
İstek akışı: **edge cache → R2 → upstream (mackolik) → R2'ye yaz**.
|
||||
Bucket kendi kendine dolar; bir görsel R2'ye girdikten sonra upstream
|
||||
kaldırsa bile kalıcıdır.
|
||||
|
||||
## Canlı kurulum (2026-06-10)
|
||||
|
||||
- Worker: `iddaai-image-proxy` (dashboard üzerinden deploy edildi)
|
||||
- Bucket: `iddaai-images` (binding adı: `BUCKET`)
|
||||
- Domain: `https://files.iddaai.com`
|
||||
- BE env: `IMAGE_BASE_URL=https://files.iddaai.com` (.gitea/workflows/deploy.yml)
|
||||
|
||||
Dashboard'daki kod ile `src/index.ts` aynı mantıktır (dashboard'da JS,
|
||||
burada TS). Kod değişikliği gerekirse ikisini senkron tut veya
|
||||
`npm run deploy` ile buradan deploy et.
|
||||
|
||||
## URL şeması (upstream ile birebir aynı)
|
||||
|
||||
```
|
||||
GET /teams/<teamId> → takım logosu
|
||||
GET /competitions/<leagueId> → lig logosu
|
||||
GET /areas/<countryId> → ülke bayrağı
|
||||
```
|
||||
|
||||
## CLI ile geliştirme / deploy
|
||||
|
||||
```bash
|
||||
cd workers/image-proxy
|
||||
npm install
|
||||
npm run dev # lokal test (miniflare, lokal R2 simülasyonu)
|
||||
npm run typecheck
|
||||
npx wrangler login # ilk seferde
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
## Bucket'ı önceden doldurma (opsiyonel)
|
||||
|
||||
```bash
|
||||
../../scripts/warm-image-cache.sh https://files.iddaai.com
|
||||
```
|
||||
|
||||
Prod sunucuda çalışır; DB'deki tüm takım/lig/ülke ID'lerini Worker
|
||||
üzerinden bir kez ister, Worker her birini R2'ye yazar (~20K istek,
|
||||
~200 MB). Çalıştırılmasa da bucket trafikle kendi kendine dolar.
|
||||
|
||||
## Maliyet
|
||||
|
||||
R2 free tier: 10 GB depolama + sınırsız egress. Workers free tier:
|
||||
100K istek/gün. Bu ölçekte aylık maliyet: 0.
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "iddaai-image-proxy",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20260601.0",
|
||||
"typescript": "^5.6.0",
|
||||
"wrangler": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* iddaai image proxy — Cloudflare Worker in front of an R2 bucket.
|
||||
*
|
||||
* Request flow for GET /teams/<id>, /competitions/<id>, /areas/<id>:
|
||||
* 1. Cloudflare edge cache
|
||||
* 2. R2 bucket (permanent mirror)
|
||||
* 3. Upstream (file.mackolikfeeds.com) → stored in R2 on the way out
|
||||
*
|
||||
* The bucket fills itself lazily; once an image lands in R2 it is served
|
||||
* from there forever, even if the upstream removes it.
|
||||
*
|
||||
* NOTE: deployed manually via the Cloudflare dashboard on 2026-06-10
|
||||
* (worker name: iddaai-image-proxy, domain: files.iddaai.com). Keep this
|
||||
* file in sync with the dashboard copy, or deploy from here with
|
||||
* `npm run deploy`.
|
||||
*/
|
||||
export interface Env {
|
||||
BUCKET: R2Bucket;
|
||||
}
|
||||
|
||||
const UPSTREAM_BASE = "https://file.mackolikfeeds.com";
|
||||
const VALID_KEY = /^(teams|competitions|areas)\/[A-Za-z0-9_-]{1,64}$/;
|
||||
|
||||
// Browsers revalidate daily; the edge keeps hits for a week. Misses are
|
||||
// cached briefly so a missing logo doesn't hammer the upstream.
|
||||
const HIT_CACHE_CONTROL = "public, max-age=86400, s-maxage=604800";
|
||||
const MISS_CACHE_CONTROL = "public, max-age=3600";
|
||||
|
||||
function imageResponse(
|
||||
body: BodyInit | null,
|
||||
contentType: string | undefined,
|
||||
etag?: string,
|
||||
): Response {
|
||||
const headers = new Headers({
|
||||
"Content-Type": contentType ?? "image/png",
|
||||
"Cache-Control": HIT_CACHE_CONTROL,
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
});
|
||||
if (etag) headers.set("ETag", etag);
|
||||
return new Response(body, { headers });
|
||||
}
|
||||
|
||||
function notFound(): Response {
|
||||
return new Response("Not found", {
|
||||
status: 404,
|
||||
headers: { "Cache-Control": MISS_CACHE_CONTROL },
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
request: Request,
|
||||
env: Env,
|
||||
ctx: ExecutionContext,
|
||||
): Promise<Response> {
|
||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||
return new Response("Method not allowed", { status: 405 });
|
||||
}
|
||||
|
||||
const key = new URL(request.url).pathname.slice(1);
|
||||
if (!VALID_KEY.test(key)) return notFound();
|
||||
|
||||
// The Cache API only stores GET entries; use a GET key for HEAD too.
|
||||
const cache = caches.default;
|
||||
const cacheKey = new Request(new URL(request.url).toString());
|
||||
const cached = await cache.match(cacheKey);
|
||||
if (cached) {
|
||||
return request.method === "HEAD"
|
||||
? new Response(null, cached)
|
||||
: cached;
|
||||
}
|
||||
|
||||
// 2. Permanent mirror in R2
|
||||
const object = await env.BUCKET.get(key);
|
||||
if (object) {
|
||||
const response = imageResponse(
|
||||
object.body,
|
||||
object.httpMetadata?.contentType,
|
||||
object.httpEtag,
|
||||
);
|
||||
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
||||
return request.method === "HEAD"
|
||||
? new Response(null, response)
|
||||
: response;
|
||||
}
|
||||
|
||||
// 3. Upstream fetch + mirror into R2 (images are small, buffer them)
|
||||
const upstream = await fetch(`${UPSTREAM_BASE}/${key}`, {
|
||||
cf: { cacheTtl: 0 },
|
||||
});
|
||||
if (!upstream.ok) {
|
||||
const response = notFound();
|
||||
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
||||
return response;
|
||||
}
|
||||
|
||||
const contentType =
|
||||
upstream.headers.get("Content-Type") ?? "image/png";
|
||||
const buffer = await upstream.arrayBuffer();
|
||||
|
||||
ctx.waitUntil(
|
||||
env.BUCKET.put(key, buffer, {
|
||||
httpMetadata: { contentType },
|
||||
}),
|
||||
);
|
||||
|
||||
const response = imageResponse(buffer, contentType);
|
||||
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
||||
return request.method === "HEAD"
|
||||
? new Response(null, response)
|
||||
: response;
|
||||
},
|
||||
} satisfies ExportedHandler<Env>;
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"types": ["@cloudflare/workers-types"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
name = "iddaai-image-proxy"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-06-01"
|
||||
|
||||
[[r2_buckets]]
|
||||
binding = "BUCKET"
|
||||
bucket_name = "iddaai-images"
|
||||
|
||||
# Custom domain (configured in the Cloudflare dashboard on 2026-06-10;
|
||||
# kept here so CLI deploys stay in sync).
|
||||
[[routes]]
|
||||
pattern = "files.iddaai.com"
|
||||
custom_domain = true
|
||||
Reference in New Issue
Block a user