21 Commits

Author SHA1 Message Date
fahricansecer 4d40b55fb9 gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 10m48s
2026-06-23 03:06:33 +03:00
fahricansecer 6269ede4ad lig
Deploy Iddaai Backend / build-and-deploy (push) Successful in 5m18s
2026-06-12 10:11:42 +03:00
fahricansecer 4c137fbab6 wow
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m7s
2026-06-11 00:25:45 +03:00
fahricansecer bb911176df gm
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m16s
2026-06-10 22:48:05 +03:00
fahricansecer 950add373f score
Deploy Iddaai Backend / build-and-deploy (push) Successful in 37s
2026-06-10 22:24:50 +03:00
fahricansecer 9a8f9941b6 Serve images from R2 via files.iddaai.com
Deploy Iddaai Backend / build-and-deploy (push) Successful in 54s
Add IMAGE_BASE_URL to the deploy env so the backend builds image URLs
against the Cloudflare image-proxy Worker instead of mackolik, and check
in the Worker source (already live on files.iddaai.com).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 14:24:38 +03:00
fahricansecer e0fbde2fde gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m0s
2026-06-10 14:05:20 +03:00
fahricansecer b62a4f2161 gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m4s
2026-06-10 03:01:33 +03:00
fahricansecer c3e44ee697 gg65
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m5s
2026-06-07 22:50:33 +03:00
fahricansecer 42b6c7ce43 Update data-fetcher.task.ts
Deploy Iddaai Backend / build-and-deploy (push) Successful in 58s
2026-06-07 21:28:52 +03:00
fahricansecer 7b17aa1fee gg2
Deploy Iddaai Backend / build-and-deploy (push) Successful in 33s
2026-06-07 15:59:41 +03:00
fahricansecer c338aba1c0 gg4
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m5s
2026-06-07 15:17:08 +03:00
fahricansecer 1c03fa5e1c gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 33s
2026-06-06 14:08:30 +03:00
fahricansecer 9e41407cb5 gg3
Deploy Iddaai Backend / build-and-deploy (push) Successful in 35s
2026-06-05 00:36:24 +03:00
fahricansecer b9700f9fda national
Deploy Iddaai Backend / build-and-deploy (push) Successful in 58s
2026-06-02 13:20:45 +03:00
fahricansecer 033a29c79c Update qualified_leagues.json
Deploy Iddaai Backend / build-and-deploy (push) Successful in 54s
2026-06-02 12:07:13 +03:00
fahricansecer 4e563e996e vv
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m7s
2026-06-02 03:37:00 +03:00
fahricansecer 671979b07d gg2
Deploy Iddaai Backend / build-and-deploy (push) Successful in 35s
2026-05-29 13:35:17 +03:00
fahricansecer b5cb412236 gg
Deploy Iddaai Backend / build-and-deploy (push) Successful in 1m6s
2026-05-29 11:59:51 +03:00
fahricansecer 659110c806 Update handoff doc + add backtest checkpoint/resume
Deploy Iddaai Backend / build-and-deploy (push) Successful in 4m32s
2026-05-25 22:29:05 +03:00
fahricansecer 988ee2f50d Add backtest pipeline, betting_brain filters, score coherence + social v3
betting_brain.py:
- HARD_MIN_SAMPLES=50 floor for calibrator bypass
- ev_edge < 0 + >= 0.20 hard vetoes
- BTTS muted (grid search found no profitable config)
- Per-market optimal envelopes (MS, OU25)
- Score coherence filter: main_pick must agree with score prediction
- HTFT reversal cross-check for MS picks

feature_builder.py / data_loader.py:
- Real home/away_position from data (was hardcoded 10)
- Cup detection wired into UpsetEngine
- _estimate_league_position with 300-day season filter

New scripts:
- diagnostic_backtest.py: per-bet diagnostic backtest with loss patterns
- optimize_filters.py: grid search per-market optimal thresholds
- analyze_backtest_csv.py: root-cause hypothesis testing on CSV
- compare_backtests.py: side-by-side validation with verdict
- test_score_coherence.py: smoke test for coherence filter (20/20 pass)

Reports:
- diagnostic_backtest_20260525_024437 (50-match smoke)
- diagnostic_backtest_20260525_035649 (1000-match in-sample)
- filter_optimization_patch.json (grid search winners per market)

Social poster v3:
- satori + resvg HTML/CSS rendering pipeline
- Twemoji football/basketball + flag SVGs
- caption SEO: 12 curated hashtags per post
- image SEO: descriptive filenames + .json metadata sidecar
- /health, /preview-png, /run-now endpoints

Docs:
- mds/SESSION_HANDOFF.md: full session state for cross-machine continuity
- mds/SOCIAL_POSTER_SETUP.md: API keys + test commands

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 20:43:28 +03:00
126 changed files with 12920 additions and 4798 deletions
+1
View File
@@ -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
+313
View File
@@ -0,0 +1,313 @@
# IDDAAI — Bahis Motoru Operasyon Workflow'u (V31d)
> Bu doküman, AI bahis tahmin motorunun **nasıl çalıştırılacağı, doğrulanacağı,
> izleneceği ve yeniden ayarlanacağına** dair operasyon kılavuzudur.
> Hedef: **hem hacim hem kâr** — gerçekçi beklenti **premium tier'da +%30 ROI**,
> daha geniş ağda +%515.
>
> Son güncelleme: 2026-05-29 · Judge sürümü: `judge-v31d-evidence-tiers`
>
> **V31d ne değiştirdi (hacim krizi çözümü):** V31c yalnızca **28 oynanabilir
> bahis / 10k maç** üretiyordu çünkü iki veto (`calibrated_confidence_too_low`,
> `play_score_too_low`) HER underdog'u reddediyordu — bunlar ">%45 model güveni
> iste" diyen FAVORİ-seçme kuralı. Ama kârlı bir 6.5 oran underdog'u zaten sadece
> ~%20 tutar; kâr oran priminden gelir. V31d, **MS değer-tier eşleşmelerinde** bu
> iki vetoyu kaldırır ve skoru tier kalitesinden üretir. Sonuç (60g doğrulama):
> **28 → 602 oynanabilir bahis (22x), 1.6u → +39.4u, ROI %28 → +%32.7.**
> Tüm zengin analiz çıktısı (market_board, v25/v27, triple_value, olasılıklar)
> **aynen korunur** — yalnızca `playable` bayrağı değişir.
---
## 0. TL;DR — En Önemli 5 Kural
1. **SADECE TEKLİ BAHİS OYNA. KOMBİNE YOK.** Matematiksel olarak kanıtlandı:
1-leg `+%3.4` → 2-leg `-%32` → 3-leg `-%67` → 4-leg `-%83`. Marjinal +EV bacakları
çarpmak kazancı yok eder.
2. **Asıl kâr MS (1X2) underdog bölgesinde.** Oran ≥ 6.0 + model_gap ≥ 0 = en yüksek ROI.
3. **Hiçbir market mute edilmez.** Tier sistemi filtreler; gerçek ROI'ler görünür kalır
(`MUTED_MARKETS = set()`).
4. **Kalibrasyon ≠ Bahis sinyali.** MS tier'ları ham model olasılığını kullanır
(`model_gap`, `ev_edge`). İzotonik kalibratörler sadece ekrandaki `calibrated_confidence`'i
etkiler (BTTS/OU25'te şişik — dikkat).
5. **Backtest'e körü körüne güvenme.** Model eğitim kesim tarihini bil; in-sample/out-of-sample
ayrımını her zaman yap (bkz. Bölüm 6).
---
## 1. Sistem Mimarisi (Pipeline)
```
Maç verisi (DB: matches, odds, elo, form, h2h…)
[V25 Ensemble] XGBoost + LightGBM + CatBoost → her market için ham olasılık
[V27 Dual-Engine] ikinci görüş / consensus (AGREE / DISAGREE)
[İzotonik Kalibrasyon] ham olasılık → calibrated_confidence (ekran için)
└─ kalibratörü OLMAYAN marketlerde hafif damping (×0.92)
[BettingBrain V31d — Deterministik Hâkim]
├─ ev_edge = calibrated_probability × oran 1 (ham-prob + market blend)
├─ model_gap = ham_model_olasılık implied_prob
├─ trap_market = market geçmiş banttan fazla fiyatlamış mı?
├─ odds_reliability = lig bazında geçmiş Brier skorundan
└─ MARKET_ODDS_TIERS → value_tier (premium/strong/standard) → bet_grade (A/B/C)
[Çıktı] bet_summary[] → playable, value_tier, stake_units, bet_grade
→ BE (smart-coupon) → FE / Mobile
```
**Anahtar dosyalar:**
- `services/betting_brain.py` — deterministik hâkim, tier tanımları (`MARKET_ODDS_TIERS`)
- `services/orchestrator/market_board.py` — ev_edge/model_gap/kalibrasyon hesapları
- `scripts/diagnostic_backtest_multi.py` — çok-pick backtest (maç başına TÜM marketler)
- `models/v25/`, `models/calibration/` — model ve kalibratör dosyaları
---
## 2. V31d — Kanıta Dayalı Kademeli Değer Sistemi (Evidence-Based Tiers)
Kullanıcı risk iştahına göre seçer. Her tier maç başına ayrı sinyal üretir.
**Sadece premium otomatik STAKE'lenir (BET); strong/standard WATCH** olarak görünür
(tam analiz gösterilir, oynanmaz) çünkü 60 günlük veri o bantların ~başabaş olduğunu
söylüyor.
| Tier | Grade | Oran bandı | Filtre | 60g ROI* | Aksiyon | Karakter |
|------|:----:|-----------|--------|:----:|:----:|----------|
| **premium** | A | **6.00 7.50** | model_gap ≥ 0, rel ≥ 0.30 | **+%32.7** | **BET** | Doğrulanmış edge; ~%20 hit, yüksek varyans |
| **strong** | B | 5.00 6.00 | model_gap ≥ 0, rel ≥ 0.30 | ~%1 (başabaş) | WATCH | Görünür, oynanmaz (kanıt yetersiz) |
| **standard** | C | 3.00 5.00 | model_gap ≥ 0, rel ≥ 0.30 | +%0.5 (başabaş) | WATCH | Hacim bölgesi, marj yok |
| info (—) | — | markete özel | ultrastrict (min_edge≥0.02, rel≥0.45-0.55, trap yok) | ~0 | REJECT/info | Bilgi amaçlı, nadiren geçer |
\* 60 günlük doğrulamadan (72.582 settled satır, 7.793 maç, 2026-04-17..05-28;
`ms_envelope.py` + `new_gate_sim.py`). premium: 602 bahis, +%32.7 ROI, +39.4u,
%20.6 hit, **6 haftanın 6'sı da pozitif**, OOS(>05-24) +%47.4.
**NEDEN 6.07.5 (V31c'deki 6.050.0 değil):** edge dar bir banda yoğunlaşmış.
`6.07.0 +%35` · `7.08.0 ~başabaş` · **`8.0+ NEGATİF`** (%10..26, longshot mezarlığı).
Eski geniş premium tier kaybeden longshot'ları içeri alıyordu. 7.5 üstünde modelin
edge'i buharlaşıyor.
**Tasarım mantığı:** premium = ROI **ve** hacim motoru (60g'de ~14 bahis/gün = bol hacim).
Bahisçi:
- **Düşük risk / yüksek kalite** istiyorsa → sadece **premium (A)** oyna (varsayılan).
- **Daha fazla hacim** istiyorsa → premium bandını 6.08.0'e genişlet (ROI +%32.7 → +%19,
hâlâ sağlam, +%44 hacim) — `MARKET_ODDS_TIERS["MS"]` premium `max_odds`'u değiştir.
**Non-MS marketler (DC, OU25, OU35, BTTS, HT, OU15, HTFT, OE, HT_OU05, HT_OU15, CARDS):**
hepsi `ultrastrict` tek-tier ile bilgi amaçlı. Geçmiş veride sistematik olarak kayıp
verdikleri için BET üretmeleri zorlaştırıldı (mute YOK — sadece sıkı eşik).
**Veto mantığı (V31d kritik):** value-tier eşleşmelerinde `calibrated_confidence_too_low`
ve `play_score_too_low` vetoları KALDIRILIR (bunlar favori-seçme kuralı). Ama gerçek
koruma vetoları AKTİF kalır: `extreme_negative_ev` (ev<0.20), `ev_edge_too_high_trap`
(ev≥0.30), `htft_reversal_risk_high`, `v25_v27_hard_disagreement`, `low_reliability_hard`.
60g'de premium tier-eşleşmelerinin ~%71'i oynanabilir oldu; kalan ~%29 bu koruma
vetolarıyla doğru şekilde reddedildi.
---
## 3. EN İYİ BAHİS DEĞERLERİ — Kesin Sıralama (Best Bet Values)
> "Multi bahislerde bütün bahis değerlerinin en iyisi" sorusunun cevabı.
> **Hepsi TEKLİ oynanır.** (Aşağıdaki ROI'ler 0.2u sabit stake simülasyonundan.)
### MS (1X2) underdog — ince oran-bandı haritası (60g, gap ≥ 0)
> "Hangi bahis hangi oranda tutuyor" sorusunun kesin cevabı. `ms_envelope.py`.
> drop-3/5 = en büyük 3/5 kazancı çıkarınca ROI (konsantrasyon/sağlamlık testi).
| Oran bandı | Bahis | Hit% | ROI | drop-3 ROI | Karar |
|-----------|------:|-----:|----:|-----:|:-----:|
| **6.0 6.5** | 469 | %22.0 | **+%37.7** | +%34.4 | ✅ elit |
| **6.0 7.0** | 492 | %21.5 | **+%35.2** | +%29.9 | ✅ elit, sağlam |
| **6.0 7.5** (premium) | 645 | %20.0 | **+%29.3** | +%24.4 | ✅ ÖNERİLEN |
| 6.0 8.0 | 928 | %17.7 | +%19.1 | +%15.5 | ✅ hacim opsiyonu |
| 7.5 8.0 | 283 | %12.4 | %4.0 | — | ❌ |
| 8.0 9.0 | 78 | %9.0 | %25.7 | — | ❌ longshot |
| 9.0+ | ~266 | <%10 | negatif | — | ❌ mezarlık |
| 5.0 6.0 (strong) | ~1000 | %18 | ~%1 | — | ⚠️ başabaş → WATCH |
| 3.0 5.0 (standard) | ~5745 | %27 | +%0.5 | — | ⚠️ başabaş → WATCH |
**Korumalı premium (htft/disagreement vetoları uygulanmış) = staked set:**
602 bahis · %20.6 hit · **+%32.7 ROI** · +39.4u · 6/6 hafta pozitif · OOS +%47.4.
**Okuma:** Edge tamamen **6.07.5** bandında. 8.0 üstü longshot'lar kaybeder
(eski 6.050.0 premium tier'ı bu yüzden sulandırıyordu). 5.0 altı başabaş.
Premium tek başına ~14 bahis/gün = hem hacim hem +%32.7 ROI.
### ❌ İşe YARAMAYAN yapılandırmalar
- **Kombine (parlay):** her ek bacak ROI'yi çökertir (yukarıdaki TL;DR).
- **MS 8.0+ longshot:** %10..26 ROI, model edge'i yok.
- **MS 5.06.0 / 3.05.0:** başabaş; WATCH olarak göster, stake'leme.
- **OU25 her konfigürasyon:** sistematik kayıp (60g'de OU25 %22.8, OU35 %17.2).
- **BTTS:** sadece çok yüksek reliability'de marjinal.
---
## 4. KRİTİK KURAL — Tekli Bahis, Kombine Yok
| Kupon tipi | Hit% | ROI | Sonuç |
|-----------|-----:|----:|:-----:|
| 1-leg (tekli) | ~%24 | **+%3.4** | ✅ |
| 2-leg | düşük | %32.4 | ❌ |
| 3-leg | çok düşük | %66.6 | ❌ |
| 4-leg | minimal | %83.0 | ❌ |
**Neden:** Tekil bacaklar yalnızca marjinal +EV. Kombine, kazanma olasılıklarını
çarparken (her biri <1) kayıp olasılığını üssel büyütür. Düz (flat) tekli stake
matematiksel olarak üstündür. **Ürün, kullanıcıyı kombineye teşvik etmemeli;**
"günün premium tekli değerleri" şeklinde sunmalı.
---
## 5. Önerilen Stake Politikası
- **Flat stake** (sabit birim) — Kelly değil. Marjinal edge'de Kelly varyansı patlatır.
- **premium (A): 0.5u sabit** (`VALUE_TIER_STAKE_UNITS`). ~%20 hit + uzun kayıp serileri
(60g'de en uzun 35 ardışık kayıp) nedeniyle KÜÇÜK tutulur — kâr **frekanstan** gelir,
bahis başı büyüklükten değil. Bankroll/risk iştahı izin veriyorsa artırılabilir.
- strong/standard WATCH = stake YOK (görünür ama oynanmaz).
- Günlük/maç başına 1 sinyal; aynı maça birden çok tier'dan bahis = korelasyon riski,
en yüksek value_tier'ı seç.
- **Drawdown uyarısı:** 0.5u'da en kötü tarihsel düşüş ≈ 34u; 35 ardışık kayıp mümkün.
Bu bir maraton stratejisidir — kısa vadeli sonuçlara göre stake değiştirme.
---
## 6. Backtest Metodolojisi & Leakage Disiplini ⚠️
**En kritik bölüm. Backtest sayıları yanlış yorumlanırsa sistem kârlı sanılıp kaybettirir.**
### 6.1 Komut
```bash
# Konteyner içinde:
python scripts/diagnostic_backtest_multi.py --days 60 --max-matches 10000 \
--progress-interval 100 --checkpoint-every 200
# Çıktı: reports/multi_backtest_YYYYMMDD.{csv,json,txt}
# Checkpoint'li → kesilirse kaldığı yerden devam eder.
```
### 6.2 Lookahead / Sızıntı (leakage) kontrolü — ZORUNLU
- **Feature lookahead:** ✅ temiz — feature'lar match_date ÖNCESİ veriden hesaplanıyor.
- **Model eğitim-seti üyeliği:** Bunu HER ZAMAN kontrol et. Kalibratörler
`models/calibration/*_metrics.json` içindeki `last_trained` tarihinde, son ~5000
maç üzerinde fit edilir. Backtest penceresi bu tarihle çakışırsa **calibrated_confidence
in-sample (şişik)** olur.
- **Pratik test (ucuz):** Backtest sonucunu eğitim kesim tarihine göre ikiye böl;
in-sample vs out-of-sample hit% karşılaştır. Tüm-market hit% **neredeyse aynıysa**
(örn. %49.7 vs %49.4) → temel modellerde anlamlı sızıntı YOK, edge gerçek.
Eski veride hit% **aniden yükseliyorsa** → o dönem eğitim setinde, ROI'yi yok say.
- Hazır script: `/tmp/leakage_split.py <csv>` (eğitim tarihine göre böler).
- **Geriye doğru ne kadar gidilebilir?** Modeller en son holdout penceresini (≈son
10k maç ≈ 60-70 gün) eğitimden hariç tutuyor. Bu yüzden **~60 gün geriye backtest
çoğunlukla temiz holdout'tur.** Daha geriye (90+ gün) gitmek eğitim setine girip
ROI'yi yapay iyi gösterebilir → kaçın.
### 6.3 Doğrulama scriptleri
- `/tmp/v31c_validation.py <csv>` — V31c tier dökümü (premium/strong/standard ROI).
- `/tmp/best_bet_values.py <csv>` — grid-search liderlik tablosu + portföy + kombine testi.
- `/tmp/leakage_split.py <csv>` — in/out-of-sample sızıntı probu.
### 6.4 Doğrulama eşiği (bir tier "kârlı" sayılmadan önce)
- n ≥ 50 bahis (tercihen ≥ 200), out-of-sample.
- ROI > 0 hem in- hem out-of-sample'da, ya da en azından OOS'ta çökmemiş.
- Kümülatif kâr eğrisi yukarı trend (tek bir şanslı güne bağlı değil).
---
## 7. Operasyonel Döngü (Cadence)
### Günlük
- Motor sağlık kontrolü (futbol pipeline çalışıyor mu; basketbol `readiness_summary`
hatası bilinen/zararsız).
- Günün sinyallerini üret; **premium (A) tekli** değerleri öne çıkar.
- Settle olan dünün bahislerini logla (gerçek hit/ROI takibi).
### Haftalık
- Son 7-14 günün gerçek sonuçlarını backtest tahminiyle karşılaştır (calibration drift).
- Tier bazında gerçekleşen ROI'yi izle; standard (C) sürekli negatifse eşik sıkılaştır.
### Aylık
- Modelleri yeniden eğit (Colab: `extract_training_data_v27.py` → eğitim → `fetch_xgb_models.sh`).
- **Yeniden eğitimden sonra MUTLAKA** 60 günlük backtest + leakage_split ile yeniden doğrula.
- Tier eşiklerini güncelle (Bölüm 8).
- `models/calibration/*_metrics.json` `last_trained` tarihini not et (bir sonraki
backtest'in OOS penceresini bilmek için).
---
## 8. Tier / Eşik Güncelleme Protokolü
1. Yeni backtest CSV'sini al → `v31c_validation.py` + `leakage_split.py` çalıştır.
2. Her tier için OOS ROI'ye bak:
- ROI sağlam pozitif + n yeterli → koru.
- ROI marjinal/negatif → oran bandını daralt veya min_reliability/min_model_gap yükselt.
- premium 6.0+ eşiği: OOS'ta hâlâ en iyi ROI mi? Değilse bandı kaydır (örn. 6.5+).
3. `betting_brain.py``MARKET_ODDS_TIERS` düzenle, **versiyon string'ini artır**
(`judge-v31c-…``judge-v31d-…`).
4. Lokal syntax kontrol → sunucuya deploy (Bölüm 9) → yeniden doğrula.
5. Tier'lar netleştikten SONRA `value_tier`'ı UI'a yay (BE smart-coupon → FE badge → mobil).
---
## 9. Deploy Prosedürü (AI Engine)
```bash
# 1. Lokal syntax kontrol
python3 -c "import ast; ast.parse(open('services/betting_brain.py').read())"
# 2. Sunucuya kopyala (SSH: port 2222, kullanıcı haruncan)
scp -P 2222 services/betting_brain.py haruncan@<host>:/tmp/betting_brain.py
# 3. Konteynere koy + import testi
docker cp /tmp/betting_brain.py iddaai-ai-engine:/app/services/betting_brain.py
docker exec iddaai-ai-engine python -c "from services.betting_brain import BettingBrain; print('OK')"
# 4. Yeniden başlat + doğrula
docker restart iddaai-ai-engine
docker exec iddaai-ai-engine python -c "from services.betting_brain import BettingBrain as B; \
print([t['value_tier'] for t in B().MARKET_ODDS_TIERS['MS']])"
```
> Not: Port 8000 host-localhost'a expose DEĞİL; sağlık testini konteyner içinden veya
> Docker network üzerinden yap. Basketbol `readiness_summary` hatası bilinen, bloklamıyor.
---
## 10. Bilinen Sınırlamalar & Uyarılar
- **Kalibrasyon şişmesi:** BTTS / OU25 izotonik kalibratörleri olasılığı %10-15 fazla
gösteriyor (overcalibrated). Bu marketlerde ekrandaki `calibrated_confidence`'e tam
güvenme; bahis kararı zaten ham-prob `model_gap`/`ev_edge` ile veriliyor.
- **Out-of-sample örneklem küçük:** Eğitim kesim tarihinden sonraki temiz pencere dar
olabilir (~200 MS bahsi). İstatistiksel kesinlik için ileriye doğru gerçek sonuç
biriktir (paper-trade) veya 60 günlük holdout backtest kullan.
- **standard (C) tier kırılgan:** in-sample +%0.4, küçük OOS örnekte negatife düşebiliyor.
Hacim için var; ROI garantisi değil.
- **Tek pencere overfit riski:** Tek bir sezon/dönem penceresine göre ayar yapma;
farklı lig/sezon çeşitliliği ara.
- **Basketbol:** `BasketballV25Predictor.readiness_summary` eksik — futbolu etkilemiyor,
ayrı düzeltilecek.
---
## 11. Hızlı Komut Referansı
```bash
# 60 günlük backtest (konteyner içi)
python scripts/diagnostic_backtest_multi.py --days 60 --max-matches 10000
# Doğrulama (CSV lokale çekildikten sonra)
python3 /tmp/v31c_validation.py reports/multi_backtest_YYYYMMDD.csv
python3 /tmp/best_bet_values.py reports/multi_backtest_YYYYMMDD.csv
python3 /tmp/leakage_split.py reports/multi_backtest_YYYYMMDD.csv
# Kalibratör eğitim tarihleri
grep -o '"last_trained":[^,]*' models/calibration/*.json
```
@@ -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.
-871
View File
@@ -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 iter Logloss
2 0 0.692389481
3 1 0.6916338586
4 2 0.6910159214
5 3 0.6903417151
6 4 0.6896961461
7 5 0.6890979366
8 6 0.6884946167
9 7 0.6879503686
10 8 0.6874528094
11 9 0.6869036785
12 10 0.6863761921
13 11 0.6859038678
14 12 0.685410175
15 13 0.6849483392
16 14 0.6845417792
17 15 0.6841038875
18 16 0.6836957422
19 17 0.6832947461
20 18 0.6829014105
21 19 0.6825264546
22 20 0.6822106577
23 21 0.6818649349
24 22 0.6815467855
25 23 0.6812293319
26 24 0.6808837443
27 25 0.6805816494
28 26 0.6803209634
29 27 0.6800350862
30 28 0.6797703947
31 29 0.6794926675
32 30 0.6792251865
33 31 0.6789670166
34 32 0.678722402
35 33 0.678476935
36 34 0.6782297335
37 35 0.6780226701
38 36 0.6778291026
39 37 0.6776045324
40 38 0.6773969079
41 39 0.6771819602
42 40 0.6769816736
43 41 0.6767984027
44 42 0.6766201184
45 43 0.6764394377
46 44 0.6762698797
47 45 0.6760974263
48 46 0.6759245179
49 47 0.6757673909
50 48 0.6756172628
51 49 0.675474531
52 50 0.6753286933
53 51 0.6751900513
54 52 0.6750574835
55 53 0.6749329567
56 54 0.6748033265
57 55 0.6746797823
58 56 0.674535525
59 57 0.6744256514
60 58 0.674310819
61 59 0.6741967947
62 60 0.6740879654
63 61 0.6739772476
64 62 0.67388281
65 63 0.6737789726
66 64 0.6736812332
67 65 0.6735930009
68 66 0.6734947116
69 67 0.6733961481
70 68 0.6732990195
71 69 0.6732133575
72 70 0.673111539
73 71 0.6730080451
74 72 0.6729157861
75 73 0.6728347949
76 74 0.6727640693
77 75 0.6726808811
78 76 0.6726029645
79 77 0.6725356026
80 78 0.6724606887
81 79 0.6723849561
82 80 0.6723050519
83 81 0.6722508802
84 82 0.6721773904
85 83 0.6721007598
86 84 0.6720353564
87 85 0.6719790902
88 86 0.6719140024
89 87 0.6718573633
90 88 0.671795602
91 89 0.6717369134
92 90 0.6716711079
93 91 0.6716070843
94 92 0.6715517232
95 93 0.6714957378
96 94 0.6714364567
97 95 0.6713881758
98 96 0.6713336502
99 97 0.6712700267
100 98 0.6712154424
101 99 0.6711600413
102 100 0.6711060533
103 101 0.6710494943
104 102 0.6709936897
105 103 0.6709472183
106 104 0.6708914508
107 105 0.6708388195
108 106 0.6707885854
109 107 0.6707454167
110 108 0.6706973013
111 109 0.6706577031
112 110 0.67061108
113 111 0.6705625485
114 112 0.6705146484
115 113 0.6704704423
116 114 0.6704155922
117 115 0.6703687117
118 116 0.6703324232
119 117 0.6702884624
120 118 0.670253478
121 119 0.6702140804
122 120 0.6701682529
123 121 0.6701320588
124 122 0.6700939824
125 123 0.6700655902
126 124 0.6700190743
127 125 0.6699792296
128 126 0.6699379404
129 127 0.669895454
130 128 0.6698563938
131 129 0.6698215571
132 130 0.6697857067
133 131 0.6697449303
134 132 0.6697052425
135 133 0.6696695553
136 134 0.6696269265
137 135 0.6695969271
138 136 0.6695489786
139 137 0.6695173859
140 138 0.6694811164
141 139 0.6694477439
142 140 0.6694082161
143 141 0.6693679185
144 142 0.6693341916
145 143 0.6692933159
146 144 0.6692619696
147 145 0.6692229289
148 146 0.6691840164
149 147 0.6691581406
150 148 0.6691177196
151 149 0.6690851126
152 150 0.6690518144
153 151 0.6690149711
154 152 0.668993877
155 153 0.6689596579
156 154 0.6689372651
157 155 0.6689003045
158 156 0.6688680182
159 157 0.6688348164
160 158 0.6687947046
161 159 0.6687605251
162 160 0.668726253
163 161 0.6686862718
164 162 0.668663478
165 163 0.6686399521
166 164 0.6686058279
167 165 0.6685761282
168 166 0.6685469327
169 167 0.6685157003
170 168 0.6684805143
171 169 0.6684485765
172 170 0.6684144429
173 171 0.6683849752
174 172 0.6683568537
175 173 0.6683266628
176 174 0.6682937842
177 175 0.6682657097
178 176 0.6682301443
179 177 0.6681995916
180 178 0.6681658267
181 179 0.6681422687
182 180 0.6681216601
183 181 0.6680899019
184 182 0.6680676394
185 183 0.6680413672
186 184 0.6680088406
187 185 0.6679873982
188 186 0.6679663544
189 187 0.6679417375
190 188 0.6679100197
191 189 0.667881208
192 190 0.6678475427
193 191 0.6678310341
194 192 0.6678060257
195 193 0.6677789336
196 194 0.6677478773
197 195 0.6677212408
198 196 0.667704316
199 197 0.6676819639
200 198 0.6676554448
201 199 0.6676318346
202 200 0.6676074705
203 201 0.6675849784
204 202 0.6675631744
205 203 0.6675397619
206 204 0.6675169086
207 205 0.6674864762
208 206 0.6674670714
209 207 0.6674375599
210 208 0.6674148457
211 209 0.6673974446
212 210 0.6673812139
213 211 0.6673515687
214 212 0.6673197956
215 213 0.6672900754
216 214 0.6672550009
217 215 0.6672271563
218 216 0.667204521
219 217 0.667181968
220 218 0.6671640023
221 219 0.66714351
222 220 0.6671167156
223 221 0.6670915937
224 222 0.6670595279
225 223 0.667033994
226 224 0.6670008246
227 225 0.6669858319
228 226 0.6669553964
229 227 0.6669274683
230 228 0.666896348
231 229 0.6668698686
232 230 0.6668513411
233 231 0.6668309985
234 232 0.6668058585
235 233 0.6667845908
236 234 0.6667582863
237 235 0.6667332943
238 236 0.6667070085
239 237 0.6666907315
240 238 0.6666633028
241 239 0.6666406707
242 240 0.6666134624
243 241 0.6665850522
244 242 0.6665631193
245 243 0.6665412643
246 244 0.6665168385
247 245 0.6664904845
248 246 0.6664678274
249 247 0.6664539777
250 248 0.6664334121
251 249 0.6664121724
252 250 0.666392034
253 251 0.666366899
254 252 0.6663414098
255 253 0.6663157816
256 254 0.6662989799
257 255 0.6662696102
258 256 0.6662479711
259 257 0.6662231874
260 258 0.6661947927
261 259 0.6661669951
262 260 0.6661426137
263 261 0.6661216749
264 262 0.6660983123
265 263 0.6660803402
266 264 0.6660617842
267 265 0.6660443878
268 266 0.6660176079
269 267 0.6659967546
270 268 0.6659751467
271 269 0.6659539329
272 270 0.6659263951
273 271 0.6659038921
274 272 0.6658767418
275 273 0.6658510507
276 274 0.6658210119
277 275 0.6657963011
278 276 0.6657748552
279 277 0.6657490013
280 278 0.665732402
281 279 0.6657118786
282 280 0.665684467
283 281 0.6656584634
284 282 0.6656309991
285 283 0.6656073482
286 284 0.6655890957
287 285 0.6655665563
288 286 0.6655452454
289 287 0.6655255286
290 288 0.6655053548
291 289 0.6654893396
292 290 0.6654648912
293 291 0.6654442759
294 292 0.6654173127
295 293 0.6653914518
296 294 0.6653648946
297 295 0.665344141
298 296 0.6653140817
299 297 0.665295365
300 298 0.6652787488
301 299 0.6652502991
302 300 0.665231168
303 301 0.6652136682
304 302 0.6651903001
305 303 0.6651697153
306 304 0.6651525958
307 305 0.6651322685
308 306 0.6651113828
309 307 0.6650886807
310 308 0.6650622251
311 309 0.6650429987
312 310 0.665015513
313 311 0.6650019022
314 312 0.664979951
315 313 0.6649549638
316 314 0.6649340455
317 315 0.6649162445
318 316 0.6649048119
319 317 0.6648796463
320 318 0.6648605481
321 319 0.6648429084
322 320 0.6648238121
323 321 0.6647969527
324 322 0.6647854723
325 323 0.6647589304
326 324 0.6647429024
327 325 0.6647237508
328 326 0.6647059396
329 327 0.664686288
330 328 0.6646532527
331 329 0.6646306438
332 330 0.6646098516
333 331 0.6645858284
334 332 0.6645707188
335 333 0.6645485788
336 334 0.6645305696
337 335 0.6645108881
338 336 0.6644923286
339 337 0.6644805222
340 338 0.6644572776
341 339 0.6644320741
342 340 0.6644115048
343 341 0.6643949013
344 342 0.6643619789
345 343 0.6643389502
346 344 0.6643088915
347 345 0.664286972
348 346 0.664274149
349 347 0.6642536926
350 348 0.6642357634
351 349 0.664207914
352 350 0.6641853097
353 351 0.6641654917
354 352 0.664143804
355 353 0.6641290647
356 354 0.6641117244
357 355 0.6640880219
358 356 0.6640669415
359 357 0.6640462999
360 358 0.664030296
361 359 0.6640028542
362 360 0.6639813347
363 361 0.6639597941
364 362 0.6639429832
365 363 0.6639222708
366 364 0.6639065546
367 365 0.6638823236
368 366 0.6638648195
369 367 0.6638436235
370 368 0.6638208732
371 369 0.6637956357
372 370 0.6637718453
373 371 0.663756918
374 372 0.6637353525
375 373 0.6637143112
376 374 0.6636956547
377 375 0.663680995
378 376 0.66366728
379 377 0.6636487567
380 378 0.6636266904
381 379 0.6636116064
382 380 0.6635902746
383 381 0.6635654896
384 382 0.6635393029
385 383 0.6635171734
386 384 0.663500789
387 385 0.663477743
388 386 0.6634584806
389 387 0.6634337499
390 388 0.6634135584
391 389 0.6633868455
392 390 0.6633755323
393 391 0.663356103
394 392 0.6633337631
395 393 0.663319422
396 394 0.6632911566
397 395 0.6632687875
398 396 0.6632431997
399 397 0.6632189331
400 398 0.663201035
401 399 0.6631898553
402 400 0.6631712482
403 401 0.663143025
404 402 0.663121538
405 403 0.6631087792
406 404 0.6630859067
407 405 0.663066483
408 406 0.6630443652
409 407 0.6630250376
410 408 0.6630007822
411 409 0.6629768728
412 410 0.6629528093
413 411 0.6629260936
414 412 0.6629102182
415 413 0.6628863488
416 414 0.6628648972
417 415 0.6628454339
418 416 0.6628200274
419 417 0.6627942591
420 418 0.6627744647
421 419 0.662765485
422 420 0.6627503257
423 421 0.6627323029
424 422 0.6627111509
425 423 0.6626785863
426 424 0.6626576561
427 425 0.6626363113
428 426 0.6626181065
429 427 0.66259794
430 428 0.6625765658
431 429 0.6625526572
432 430 0.66253135
433 431 0.6625035695
434 432 0.662480212
435 433 0.6624611632
436 434 0.6624332625
437 435 0.6624120584
438 436 0.6623941719
439 437 0.6623766304
440 438 0.6623623329
441 439 0.6623442925
442 440 0.6623212715
443 441 0.6623025941
444 442 0.6622749791
445 443 0.6622534499
446 444 0.6622305473
447 445 0.6622059333
448 446 0.6621871707
449 447 0.6621638454
450 448 0.6621511296
451 449 0.6621349978
452 450 0.6621120424
453 451 0.6620958271
454 452 0.6620793528
455 453 0.6620572713
456 454 0.6620395025
457 455 0.6620188044
458 456 0.6620017347
459 457 0.6619811454
460 458 0.6619695569
461 459 0.661952377
462 460 0.6619237442
463 461 0.6619089407
464 462 0.6618886168
465 463 0.6618831383
466 464 0.6618690774
467 465 0.661845878
468 466 0.6618290213
469 467 0.6618050064
470 468 0.6617832833
471 469 0.6617652311
472 470 0.6617443144
473 471 0.6617202619
474 472 0.6617005831
475 473 0.6616824419
476 474 0.6616538226
477 475 0.6616314155
478 476 0.6616127861
479 477 0.6616029072
480 478 0.6615843751
481 479 0.661563216
482 480 0.6615432257
483 481 0.6615263324
484 482 0.6615033259
485 483 0.661484293
486 484 0.6614678231
487 485 0.6614463024
488 486 0.6614155436
489 487 0.6613958945
490 488 0.661380611
491 489 0.6613677802
492 490 0.6613530086
493 491 0.6613248211
494 492 0.6613059359
495 493 0.6612729965
496 494 0.6612624948
497 495 0.6612401679
498 496 0.6612191637
499 497 0.6611912219
500 498 0.6611773017
501 499 0.6611638216
502 500 0.6611450533
503 501 0.6611179111
504 502 0.6610959069
505 503 0.6610728788
506 504 0.6610436668
507 505 0.6610188976
508 506 0.6610030555
509 507 0.6609831174
510 508 0.6609586562
511 509 0.660935882
512 510 0.6609202024
513 511 0.6609011137
514 512 0.6608726737
515 513 0.6608608849
516 514 0.6608387256
517 515 0.6608136063
518 516 0.6607946343
519 517 0.6607703935
520 518 0.6607509625
521 519 0.6607238109
522 520 0.6606999858
523 521 0.6606813873
524 522 0.6606610372
525 523 0.660638456
526 524 0.6606156483
527 525 0.6605968623
528 526 0.6605735776
529 527 0.6605517294
530 528 0.6605309239
531 529 0.6605086434
532 530 0.6604803349
533 531 0.6604566326
534 532 0.6604430839
535 533 0.6604273738
536 534 0.6604048016
537 535 0.6603845173
538 536 0.6603669212
539 537 0.6603488983
540 538 0.6603176881
541 539 0.6602953862
542 540 0.6602672025
543 541 0.6602568636
544 542 0.660235705
545 543 0.6602152295
546 544 0.6601897709
547 545 0.6601683731
548 546 0.6601472267
549 547 0.6601262337
550 548 0.6601119991
551 549 0.6600869973
552 550 0.6600667497
553 551 0.6600397508
554 552 0.660016863
555 553 0.6599933158
556 554 0.6599632649
557 555 0.6599446007
558 556 0.6599138126
559 557 0.6598965504
560 558 0.6598785723
561 559 0.659860838
562 560 0.6598408724
563 561 0.6598244857
564 562 0.6598082469
565 563 0.6597851673
566 564 0.6597683521
567 565 0.6597479006
568 566 0.6597310938
569 567 0.6597096581
570 568 0.6596862311
571 569 0.6596574779
572 570 0.6596385418
573 571 0.6596189903
574 572 0.65959275
575 573 0.6595730662
576 574 0.6595566809
577 575 0.6595365076
578 576 0.6595163446
579 577 0.6594816637
580 578 0.6594570142
581 579 0.6594353055
582 580 0.6594162362
583 581 0.659395036
584 582 0.6593798831
585 583 0.6593556719
586 584 0.6593292627
587 585 0.6592976737
588 586 0.6592754841
589 587 0.6592510441
590 588 0.6592290326
591 589 0.6592097404
592 590 0.6591876204
593 591 0.6591705995
594 592 0.6591456195
595 593 0.6591107122
596 594 0.6590819533
597 595 0.6590551327
598 596 0.6590373916
599 597 0.6590177149
600 598 0.6589946095
601 599 0.6589697628
602 600 0.6589442269
603 601 0.6589182437
604 602 0.6588837179
605 603 0.6588674101
606 604 0.6588406916
607 605 0.6588149945
608 606 0.6587866031
609 607 0.6587636648
610 608 0.6587502469
611 609 0.6587292784
612 610 0.6587104112
613 611 0.6586953782
614 612 0.6586641191
615 613 0.6586450136
616 614 0.6586136263
617 615 0.6585862768
618 616 0.6585585235
619 617 0.6585371631
620 618 0.6585092632
621 619 0.6584914317
622 620 0.6584662432
623 621 0.6584454668
624 622 0.6584249408
625 623 0.6583931228
626 624 0.6583660767
627 625 0.658354264
628 626 0.6583253625
629 627 0.6582968632
630 628 0.6582687399
631 629 0.658242535
632 630 0.6582199874
633 631 0.6581918101
634 632 0.6581735218
635 633 0.6581445869
636 634 0.6581202427
637 635 0.6580977862
638 636 0.6580724179
639 637 0.6580426322
640 638 0.6580111256
641 639 0.6579834747
642 640 0.6579541367
643 641 0.6579254503
644 642 0.657898555
645 643 0.6578676875
646 644 0.6578324163
647 645 0.6578062223
648 646 0.6577760631
649 647 0.6577483474
650 648 0.6577249642
651 649 0.6576974966
652 650 0.657675114
653 651 0.6576447891
654 652 0.6576102356
655 653 0.6575793887
656 654 0.6575543309
657 655 0.6575340787
658 656 0.6575061464
659 657 0.657476113
660 658 0.6574447014
661 659 0.6574247361
662 660 0.6574034983
663 661 0.6573783832
664 662 0.657357694
665 663 0.6573411592
666 664 0.6573118559
667 665 0.6572819076
668 666 0.6572430097
669 667 0.6572160391
670 668 0.6571931413
671 669 0.6571737099
672 670 0.6571532872
673 671 0.6571208939
674 672 0.6570887673
675 673 0.6570633692
676 674 0.6570454361
677 675 0.6570231031
678 676 0.6570052089
679 677 0.6569855794
680 678 0.6569579709
681 679 0.6569333354
682 680 0.6569069617
683 681 0.6568931857
684 682 0.6568734532
685 683 0.6568435196
686 684 0.6568108038
687 685 0.6567811374
688 686 0.6567467284
689 687 0.6567172734
690 688 0.6566967606
691 689 0.6566720128
692 690 0.6566441608
693 691 0.6566172287
694 692 0.6565952549
695 693 0.6565702687
696 694 0.6565392213
697 695 0.6565157938
698 696 0.6564902789
699 697 0.6564644734
700 698 0.6564349549
701 699 0.6564046572
702 700 0.6563744107
703 701 0.6563525063
704 702 0.6563189867
705 703 0.6562939062
706 704 0.6562739297
707 705 0.656256438
708 706 0.6562366475
709 707 0.6562073096
710 708 0.6561864222
711 709 0.6561578826
712 710 0.6561208567
713 711 0.6560924703
714 712 0.6560656907
715 713 0.6560362588
716 714 0.6560124527
717 715 0.6559875055
718 716 0.6559547281
719 717 0.6559230866
720 718 0.6558924823
721 719 0.6558676469
722 720 0.6558459277
723 721 0.6558149638
724 722 0.6557812248
725 723 0.6557546502
726 724 0.6557274948
727 725 0.6557044723
728 726 0.6556751811
729 727 0.6556539158
730 728 0.6556182915
731 729 0.6555977079
732 730 0.6555667903
733 731 0.6555394075
734 732 0.6555122742
735 733 0.6554814941
736 734 0.6554517373
737 735 0.655429552
738 736 0.655396579
739 737 0.6553735864
740 738 0.6553472597
741 739 0.6553252832
742 740 0.6552971659
743 741 0.6552763852
744 742 0.6552488203
745 743 0.65521229
746 744 0.6551949744
747 745 0.6551673797
748 746 0.6551421856
749 747 0.6551255516
750 748 0.6551019608
751 749 0.6550758728
752 750 0.655051966
753 751 0.6550351058
754 752 0.6549998756
755 753 0.6549721212
756 754 0.6549401744
757 755 0.6549207325
758 756 0.6548900891
759 757 0.6548682731
760 758 0.6548418938
761 759 0.6548234717
762 760 0.6547996833
763 761 0.6547726174
764 762 0.6547509314
765 763 0.6547168175
766 764 0.6546907846
767 765 0.6546671611
768 766 0.6546475893
769 767 0.6546206223
770 768 0.6545874193
771 769 0.6545620629
772 770 0.6545346297
773 771 0.6545172316
774 772 0.6544943049
775 773 0.6544632323
776 774 0.6544384097
777 775 0.6544084745
778 776 0.6543765257
779 777 0.6543536123
780 778 0.6543303593
781 779 0.6543005831
782 780 0.6542678123
783 781 0.6542439303
784 782 0.6542100401
785 783 0.6541836178
786 784 0.654158129
787 785 0.6541343464
788 786 0.6541092921
789 787 0.6540812254
790 788 0.654060259
791 789 0.6540467253
792 790 0.6540306837
793 791 0.6540103667
794 792 0.6539821302
795 793 0.6539577914
796 794 0.653923724
797 795 0.6539086888
798 796 0.6538798424
799 797 0.6538566996
800 798 0.6538290752
801 799 0.6538051255
802 800 0.6537917354
803 801 0.6537684302
804 802 0.6537402991
805 803 0.6537165427
806 804 0.6536853601
807 805 0.6536681479
808 806 0.6536409101
809 807 0.6536120189
810 808 0.6535912493
811 809 0.6535617421
812 810 0.6535315174
813 811 0.6534972927
814 812 0.6534818476
815 813 0.6534498323
816 814 0.6534305025
817 815 0.6534081059
818 816 0.6533765804
819 817 0.6533441549
820 818 0.6533053405
821 819 0.6532838469
822 820 0.6532604302
823 821 0.6532364412
824 822 0.6532100089
825 823 0.6531782515
826 824 0.6531449701
827 825 0.653115452
828 826 0.6530787602
829 827 0.653052397
830 828 0.6530313579
831 829 0.6530010363
832 830 0.6529752146
833 831 0.652954801
834 832 0.6529330351
835 833 0.6528993709
836 834 0.6528665883
837 835 0.6528413041
838 836 0.6528217161
839 837 0.6527978782
840 838 0.6527789461
841 839 0.6527432001
842 840 0.6527139767
843 841 0.6526857244
844 842 0.652657086
845 843 0.6526355016
846 844 0.6526054936
847 845 0.6525793707
848 846 0.6525584692
849 847 0.6525279747
850 848 0.6525038765
851 849 0.6524849104
852 850 0.6524610603
853 851 0.6524357337
854 852 0.6524082286
855 853 0.65238051
856 854 0.6523557826
857 855 0.6523391233
858 856 0.652325347
859 857 0.6522924958
860 858 0.6522623584
861 859 0.6522343891
862 860 0.6522094424
863 861 0.6521841478
864 862 0.6521657946
865 863 0.6521304278
866 864 0.6521045712
867 865 0.6520753696
868 866 0.6520519528
869 867 0.6520216555
870 868 0.6519926935
871 869 0.6519734186
-871
View File
@@ -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
1 iter Passed Remaining
2 0 46 93548
3 1 83 83419
4 2 132 88415
5 3 162 81250
6 4 196 78573
7 5 230 76747
8 6 269 76701
9 7 319 79674
10 8 364 80653
11 9 411 81918
12 10 456 82497
13 11 491 81432
14 12 522 79809
15 13 555 78774
16 14 595 78777
17 15 630 78123
18 16 662 77290
19 17 700 77124
20 18 730 76120
21 19 764 75651
22 20 804 75774
23 21 835 75128
24 22 886 76169
25 23 920 75764
26 24 960 75853
27 25 989 75130
28 26 1025 74941
29 27 1060 74714
30 28 1104 75079
31 29 1141 74976
32 30 1180 74975
33 31 1213 74640
34 32 1245 74260
35 33 1287 74434
36 34 1327 74528
37 35 1376 75071
38 36 1427 75741
39 37 1468 75804
40 38 1508 75857
41 39 1549 75922
42 40 1586 75781
43 41 1621 75590
44 42 1663 75705
45 43 1701 75621
46 44 1739 75591
47 45 1776 75460
48 46 1819 75616
49 47 1869 76025
50 48 1916 76288
51 49 1953 76191
52 50 1993 76197
53 51 2038 76381
54 52 2080 76420
55 53 2158 77788
56 54 2220 78529
57 55 2286 79390
58 56 2328 79372
59 57 2367 79254
60 58 2409 79257
61 59 2444 79049
62 60 2484 78985
63 61 2521 78820
64 62 2554 78528
65 63 2593 78466
66 64 2623 78111
67 65 2660 77969
68 66 2695 77776
69 67 2725 77446
70 68 2761 77291
71 69 2791 76975
72 70 2824 76739
73 71 2861 76611
74 72 2897 76476
75 73 2935 76408
76 74 3040 78027
77 75 3097 78411
78 76 3152 78741
79 77 3216 79248
80 78 3256 79195
81 79 3305 79336
82 80 3348 79320
83 81 3381 79089
84 82 3416 78911
85 83 3480 79399
86 84 3535 79649
87 85 3581 79716
88 86 3612 79428
89 87 3644 79185
90 88 3678 78975
91 89 3712 78785
92 90 3743 78531
93 91 3775 78297
94 92 3806 78047
95 93 3837 77821
96 94 3871 77629
97 95 3913 77618
98 96 3945 77403
99 97 3989 77433
100 98 4020 77204
101 99 4053 77020
102 100 4084 76789
103 101 4116 76597
104 102 4148 76401
105 103 4176 76141
106 104 4202 75845
107 105 4232 75634
108 106 4261 75390
109 107 4290 75168
110 108 4324 75018
111 109 4351 74766
112 110 4386 74648
113 111 4424 74577
114 112 4458 74455
115 113 4497 74400
116 114 4533 74307
117 115 4564 74136
118 116 4596 73981
119 117 4628 73818
120 118 4668 73786
121 119 4692 73509
122 120 4723 73354
123 121 4756 73220
124 122 4788 73065
125 123 4815 72854
126 124 4843 72647
127 125 4875 72514
128 126 4916 72515
129 127 4952 72436
130 128 4991 72397
131 129 5028 72327
132 130 5059 72180
133 131 5096 72116
134 132 5125 71946
135 133 5156 71804
136 134 5190 71704
137 135 5221 71564
138 136 5251 71407
139 137 5274 71165
140 138 5309 71084
141 139 5344 71008
142 140 5377 70902
143 141 5416 70866
144 142 5452 70803
145 143 5490 70760
146 144 5521 70641
147 145 5553 70522
148 146 5582 70365
149 147 5611 70217
150 148 5636 70026
151 149 5673 69975
152 150 5706 69874
153 151 5738 69764
154 152 5765 69605
155 153 5795 69471
156 154 5817 69246
157 155 5853 69191
158 156 5888 69122
159 157 5924 69070
160 158 5964 69061
161 159 5996 68963
162 160 6022 68789
163 161 6050 68650
164 162 6079 68510
165 163 6108 68385
166 164 6140 68292
167 165 6169 68162
168 166 6202 68074
169 167 6231 67953
170 168 6263 67858
171 169 6295 67764
172 170 6325 67656
173 171 6356 67561
174 172 6395 67545
175 173 6437 67554
176 174 6472 67495
177 175 6503 67395
178 176 6533 67291
179 177 6562 67174
180 178 6590 67049
181 179 6624 66982
182 180 6655 66882
183 181 6687 66804
184 182 6718 66703
185 183 6751 66632
186 184 6784 66559
187 185 6810 66424
188 186 6832 66246
189 187 6867 66187
190 188 6918 66294
191 189 6969 66393
192 190 7018 66470
193 191 7074 66614
194 192 7117 66635
195 193 7191 66943
196 194 7242 67036
197 195 7282 67027
198 196 7317 66967
199 197 7351 66903
200 198 7389 66879
201 199 7432 66896
202 200 7471 66869
203 201 7506 66814
204 202 7540 66752
205 203 7568 66628
206 204 7605 66596
207 205 7638 66519
208 206 7665 66397
209 207 7700 66340
210 208 7734 66276
211 209 7766 66197
212 210 7796 66106
213 211 7831 66053
214 212 7871 66037
215 213 7910 66016
216 214 7951 66014
217 215 7989 65983
218 216 8025 65946
219 217 8058 65872
220 218 8087 65768
221 219 8112 65638
222 220 8148 65594
223 221 8197 65655
224 222 8239 65655
225 223 8268 65556
226 224 8298 65466
227 225 8327 65366
228 226 8357 65278
229 227 8384 65167
230 228 8418 65103
231 229 8453 65058
232 230 8490 65020
233 231 8523 64958
234 232 8550 64848
235 233 8575 64718
236 234 8607 64648
237 235 8635 64545
238 236 8660 64426
239 237 8691 64345
240 238 8719 64250
241 239 8746 64137
242 240 8773 64038
243 241 8803 63951
244 242 8833 63873
245 243 8862 63779
246 244 8892 63698
247 245 8932 63688
248 246 8962 63611
249 247 8991 63521
250 248 9021 63442
251 249 9051 63358
252 250 9085 63306
253 251 9110 63193
254 252 9137 63093
255 253 9174 63066
256 254 9196 62935
257 255 9238 62934
258 256 9267 62855
259 257 9297 62776
260 258 9324 62681
261 259 9357 62625
262 260 9388 62552
263 261 9427 62536
264 262 9461 62491
265 263 9496 62443
266 264 9524 62356
267 265 9553 62278
268 266 9590 62247
269 267 9620 62172
270 268 9645 62071
271 269 9682 62040
272 270 9711 61962
273 271 9739 61872
274 272 9768 61797
275 273 9804 61761
276 274 9848 61777
277 275 9886 61755
278 276 9925 61740
279 277 9965 61728
280 278 9995 61656
281 279 10022 61564
282 280 10055 61516
283 281 10080 61410
284 282 10111 61344
285 283 10147 61311
286 284 10175 61230
287 285 10202 61141
288 286 10234 61084
289 287 10264 61018
290 288 10299 60977
291 289 10323 60874
292 290 10353 60804
293 291 10394 60803
294 292 10431 60773
295 293 10471 60763
296 294 10503 60707
297 295 10534 60645
298 296 10576 60646
299 297 10612 60612
300 298 10639 60525
301 299 10668 60453
302 300 10702 60411
303 301 10729 60326
304 302 10764 60290
305 303 10801 60263
306 304 10829 60182
307 305 10857 60108
308 306 10892 60067
309 307 10930 60047
310 308 10972 60045
311 309 11002 59983
312 310 11030 59902
313 311 11058 59828
314 312 11092 59788
315 313 11117 59696
316 314 11149 59641
317 315 11187 59617
318 316 11211 59525
319 317 11243 59468
320 318 11274 59413
321 319 11304 59346
322 320 11334 59287
323 321 11362 59209
324 322 11394 59158
325 323 11436 59158
326 324 11477 59153
327 325 11513 59122
328 326 11547 59081
329 327 11572 58991
330 328 11607 58956
331 329 11637 58894
332 330 11668 58833
333 331 11700 58785
334 332 11724 58694
335 333 11757 58648
336 334 11780 58550
337 335 11815 58515
338 336 11844 58451
339 337 11869 58364
340 338 11905 58335
341 339 11941 58302
342 340 11986 58315
343 341 12020 58274
344 342 12066 58292
345 343 12122 58358
346 344 12177 58415
347 345 12221 58422
348 346 12264 58423
349 347 12300 58394
350 348 12324 58304
351 349 12354 58243
352 350 12401 58262
353 351 12438 58232
354 352 12479 58228
355 353 12512 58179
356 354 12541 58116
357 355 12569 58044
358 356 12597 57977
359 357 12628 57920
360 358 12653 57839
361 359 12682 57775
362 360 12720 57752
363 361 12744 57666
364 362 12770 57592
365 363 12811 57583
366 364 12841 57522
367 365 12870 57460
368 366 12897 57386
369 367 12938 57378
370 368 12974 57347
371 369 13009 57313
372 370 13038 57249
373 371 13078 57235
374 372 13117 57216
375 373 13147 57159
376 374 13181 57118
377 375 13205 57036
378 376 13235 56979
379 377 13274 56960
380 378 13306 56911
381 379 13333 56841
382 380 13366 56798
383 381 13396 56741
384 382 13421 56666
385 383 13467 56674
386 384 13508 56664
387 385 13540 56616
388 386 13569 56559
389 387 13598 56496
390 388 13627 56438
391 389 13656 56376
392 390 13685 56317
393 391 13717 56271
394 392 13750 56227
395 393 13771 56135
396 394 13804 56090
397 395 13825 55999
398 396 13858 55957
399 397 13888 55904
400 398 13917 55843
401 399 13953 55812
402 400 13994 55802
403 401 14025 55752
404 402 14048 55670
405 403 14076 55607
406 404 14105 55551
407 405 14142 55526
408 406 14182 55511
409 407 14214 55464
410 408 14240 55394
411 409 14267 55328
412 410 14299 55284
413 411 14324 55213
414 412 14351 55146
415 413 14379 55086
416 414 14410 55036
417 415 14451 55025
418 416 14484 54984
419 417 14513 54929
420 418 14536 54851
421 419 14565 54793
422 420 14587 54710
423 421 14615 54650
424 422 14642 54588
425 423 14666 54515
426 424 14690 54441
427 425 14719 54384
428 426 14739 54297
429 427 14772 54257
430 428 14790 54164
431 429 14824 54125
432 430 14844 54039
433 431 14876 53995
434 432 14906 53946
435 433 14938 53902
436 434 14980 53894
437 435 15006 53829
438 436 15033 53770
439 437 15059 53706
440 438 15085 53639
441 439 15110 53574
442 440 15134 53503
443 441 15160 53438
444 442 15184 53369
445 443 15211 53308
446 444 15234 53236
447 445 15266 53193
448 446 15287 53114
449 447 15316 53059
450 448 15336 52978
451 449 15366 52929
452 450 15393 52870
453 451 15429 52843
454 452 15469 52828
455 453 15490 52748
456 454 15523 52712
457 455 15550 52653
458 456 15577 52594
459 457 15604 52536
460 458 15630 52476
461 459 15656 52414
462 460 15682 52353
463 461 15711 52304
464 462 15736 52238
465 463 15765 52188
466 464 15786 52112
467 465 15817 52068
468 466 15839 51996
469 467 15873 51961
470 468 15903 51916
471 469 15935 51873
472 470 15969 51840
473 471 15994 51779
474 472 16022 51726
475 473 16047 51663
476 474 16073 51605
477 475 16099 51546
478 476 16128 51495
479 477 16152 51431
480 478 16176 51367
481 479 16205 51317
482 480 16228 51250
483 481 16255 51194
484 482 16277 51123
485 483 16305 51071
486 484 16328 51005
487 485 16362 50973
488 486 16392 50928
489 487 16426 50894
490 488 16459 50860
491 489 16480 50787
492 490 16510 50743
493 491 16530 50668
494 492 16561 50625
495 493 16585 50562
496 494 16613 50510
497 495 16638 50453
498 496 16663 50393
499 497 16690 50339
500 498 16716 50282
501 499 16740 50222
502 500 16773 50186
503 501 16802 50139
504 502 16836 50107
505 503 16873 50085
506 504 16921 50094
507 505 16989 50163
508 506 17038 50173
509 507 17069 50132
510 508 17110 50121
511 509 17145 50091
512 510 17190 50091
513 511 17219 50044
514 512 17247 49994
515 513 17271 49932
516 514 17298 49878
517 515 17343 49878
518 516 17373 49836
519 517 17417 49831
520 518 17460 49823
521 519 17490 49781
522 520 17518 49731
523 521 17546 49680
524 522 17571 49622
525 523 17600 49577
526 524 17625 49520
527 525 17655 49474
528 526 17679 49414
529 527 17707 49366
530 528 17729 49300
531 529 17758 49254
532 530 17781 49191
533 531 17808 49141
534 532 17829 49071
535 533 17862 49038
536 534 17905 49031
537 535 18028 49241
538 536 18072 49236
539 537 18106 49203
540 538 18135 49157
541 539 18165 49114
542 540 18200 49083
543 541 18223 49022
544 542 18254 48980
545 543 18280 48927
546 544 18307 48876
547 545 18338 48834
548 546 18367 48790
549 547 18411 48783
550 548 18444 48747
551 549 18470 48693
552 550 18503 48660
553 551 18531 48611
554 552 18557 48558
555 553 18584 48508
556 554 18625 48493
557 555 18650 48436
558 556 18677 48388
559 557 18703 48333
560 558 18729 48282
561 559 18756 48231
562 560 18781 48176
563 561 18808 48126
564 562 18834 48074
565 563 18869 48043
566 564 18902 48008
567 565 18930 47960
568 566 18958 47914
569 567 18983 47859
570 568 19016 47824
571 569 19037 47761
572 570 19068 47720
573 571 19090 47660
574 572 19111 47595
575 573 19141 47553
576 574 19164 47494
577 575 19196 47458
578 576 19217 47393
579 577 19249 47358
580 578 19274 47303
581 579 19298 47247
582 580 19324 47195
583 581 19357 47162
584 582 19391 47130
585 583 19427 47103
586 584 19460 47070
587 585 19483 47012
588 586 19511 46967
589 587 19542 46929
590 588 19564 46867
591 589 19597 46833
592 590 19621 46779
593 591 19647 46729
594 592 19670 46672
595 593 19699 46627
596 594 19726 46582
597 595 19753 46532
598 596 19778 46480
599 597 19803 46429
600 598 19830 46381
601 599 19857 46335
602 600 19896 46313
603 601 19925 46271
604 602 19957 46236
605 603 19991 46204
606 604 20019 46159
607 605 20047 46115
608 606 20072 46063
609 607 20098 46015
610 608 20123 45963
611 609 20149 45913
612 610 20176 45867
613 611 20202 45817
614 612 20230 45774
615 613 20253 45719
616 614 20285 45682
617 615 20307 45626
618 616 20338 45589
619 617 20361 45532
620 618 20394 45500
621 619 20423 45459
622 620 20454 45420
623 621 20488 45390
624 622 20510 45333
625 623 20543 45301
626 624 20569 45252
627 625 20594 45201
628 626 20619 45151
629 627 20646 45107
630 628 20675 45066
631 629 20701 45016
632 630 20727 44970
633 631 20752 44919
634 632 20782 44881
635 633 20804 44825
636 634 20837 44791
637 635 20862 44742
638 636 20892 44704
639 637 20931 44683
640 638 20960 44643
641 639 20994 44612
642 640 21022 44570
643 641 21052 44531
644 642 21082 44493
645 643 21107 44443
646 644 21135 44401
647 645 21160 44351
648 646 21185 44302
649 647 21210 44253
650 648 21236 44208
651 649 21262 44161
652 650 21288 44113
653 651 21315 44068
654 652 21343 44027
655 653 21377 43997
656 654 21403 43949
657 655 21440 43926
658 656 21477 43903
659 657 21502 43854
660 658 21533 43819
661 659 21559 43772
662 660 21586 43727
663 661 21611 43680
664 662 21637 43633
665 663 21662 43586
666 664 21688 43539
667 665 21714 43493
668 666 21742 43451
669 667 21771 43413
670 668 21818 43409
671 669 21846 43366
672 670 21888 43352
673 671 21934 43345
674 672 21971 43322
675 673 22019 43320
676 674 22053 43289
677 675 22090 43266
678 676 22141 43269
679 677 22176 43240
680 678 22213 43215
681 679 22239 43171
682 680 22270 43134
683 681 22296 43088
684 682 22321 43041
685 683 22350 43002
686 684 22379 42962
687 685 22419 42944
688 686 22452 42912
689 687 22484 42878
690 688 22511 42834
691 689 22537 42789
692 690 22571 42757
693 691 22598 42714
694 692 22624 42669
695 693 22653 42630
696 694 22680 42586
697 695 22708 42545
698 696 22739 42510
699 697 22761 42457
700 698 22792 42421
701 699 22816 42373
702 700 22845 42333
703 701 22870 42288
704 702 22902 42253
705 703 22942 42234
706 704 22974 42201
707 705 23002 42160
708 706 23033 42124
709 707 23054 42071
710 708 23086 42038
711 709 23115 41999
712 710 23143 41957
713 711 23169 41914
714 712 23195 41868
715 713 23230 41840
716 714 23259 41801
717 715 23287 41760
718 716 23311 41713
719 717 23341 41676
720 718 23372 41641
721 719 23405 41610
722 720 23438 41578
723 721 23483 41566
724 722 23507 41519
725 723 23540 41488
726 724 23566 41444
727 725 23595 41406
728 726 23623 41365
729 727 23648 41320
730 728 23677 41281
731 729 23700 41231
732 730 23728 41192
733 731 23752 41144
734 732 23784 41111
735 733 23807 41063
736 734 23840 41031
737 735 23870 40994
738 736 23908 40972
739 737 23941 40940
740 738 23974 40909
741 739 24006 40875
742 740 24036 40838
743 741 24064 40798
744 742 24092 40759
745 743 24127 40730
746 744 24153 40688
747 745 24179 40644
748 746 24207 40604
749 747 24233 40561
750 748 24261 40522
751 749 24295 40491
752 750 24318 40444
753 751 24349 40410
754 752 24376 40368
755 753 24408 40335
756 754 24442 40306
757 755 24474 40273
758 756 24508 40242
759 757 24548 40222
760 758 24575 40182
761 759 24605 40145
762 760 24632 40104
763 761 24660 40064
764 762 24689 40027
765 763 24714 39982
766 764 24745 39949
767 765 24766 39897
768 766 24797 39863
769 767 24825 39823
770 768 24854 39786
771 769 24880 39744
772 770 24909 39706
773 771 24940 39672
774 772 24970 39635
775 773 25004 39606
776 774 25030 39564
777 775 25056 39522
778 776 25086 39486
779 777 25107 39436
780 778 25139 39403
781 779 25159 39351
782 780 25188 39314
783 781 25214 39272
784 782 25240 39230
785 783 25266 39188
786 784 25288 39141
787 785 25315 39101
788 786 25341 39058
789 787 25367 39016
790 788 25391 38972
791 789 25417 38930
792 790 25448 38895
793 791 25482 38867
794 792 25514 38834
795 793 25542 38795
796 794 25569 38756
797 795 25595 38714
798 796 25618 38669
799 797 25643 38626
800 798 25667 38581
801 799 25695 38543
802 800 25716 38494
803 801 25743 38454
804 802 25770 38415
805 803 25790 38364
806 804 25822 38332
807 805 25843 38284
808 806 25873 38249
809 807 25896 38203
810 808 25925 38167
811 809 25955 38131
812 810 25988 38101
813 811 26028 38080
814 812 26055 38042
815 813 26081 38000
816 814 26108 37961
817 815 26131 37916
818 816 26159 37878
819 817 26188 37841
820 818 26214 37800
821 819 26242 37764
822 820 26272 37728
823 821 26298 37688
824 822 26327 37652
825 823 26359 37619
826 824 26385 37580
827 825 26408 37534
828 826 26444 37507
829 827 26477 37478
830 828 26517 37456
831 829 26539 37411
832 830 26573 37382
833 831 26597 37339
834 832 26623 37298
835 833 26650 37259
836 834 26677 37221
837 835 26704 37182
838 836 26728 37138
839 837 26763 37111
840 838 26791 37073
841 839 26822 37041
842 840 26872 37033
843 841 26924 37029
844 842 26982 37033
845 843 27054 37055
846 844 27097 37038
847 845 27120 36994
848 846 27146 36954
849 847 27180 36925
850 848 27206 36884
851 849 27234 36846
852 850 27260 36807
853 851 27289 36770
854 852 27318 36734
855 853 27347 36698
856 854 27386 36675
857 855 27413 36637
858 856 27439 36596
859 857 27471 36564
860 858 27501 36529
861 859 27535 36500
862 860 27572 36474
863 861 27595 36431
864 862 27627 36398
865 863 27654 36360
866 864 27683 36324
867 865 27711 36287
868 866 27738 36249
869 867 27765 36210
870 868 27794 36175
871 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
}
]
}
}
+134
View File
@@ -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"
}
}
}
+31
View File
@@ -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"
]
}
+102
View File
@@ -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 0100 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
]
}
}
+184
View File
@@ -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",
}
+258
View File
@@ -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
+166
View File
@@ -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,51 @@
match_id,match_date,league_id,score_home,score_away,ht_score_home,ht_score_away,market,pick,odds,stake_units,playable,won,unit_profit,raw_confidence,calibrated_confidence,play_score,ev_edge,bet_grade,is_value_sniper,bb_score,bb_action,bb_vetoes,bb_issues,bb_positives,bb_model_prob,bb_implied_prob,bb_model_market_gap,bb_divergence,bb_trap_market,v27_consensus,data_quality_score,data_quality_flags,risk_level,odds_reliability,htft_reversal_prob,htft_top_pick,league_name,is_cup,model_version,decision_reason
5iam9c9dw3ggz3y1ohr9uh53o,2026-05-24,8nbwkj392b0xzssqpw9jwmzdn,0,0,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
8c90p2ft4zxjdck8wlgq1a61g,2026-05-24,a9vrdkelbgif0gtu3wxsr75xo,2,2,0,0,OU25,Üst,1.41,0.2,True,True,0.082,59.5,65.4,58.4,-0.1557,B,True,26.4,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5955,0.7092,-0.1137,0.0339,True,DISAGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.6618,,,,False,v28-pro-max,betting_brain_approved
9ljz1grea3a8jajif4e9b7bpw,2026-05-24,2wolc27r8z03itcvwp43e38c5,1,1,1,1,BTTS,KG Var,1.62,0.2,True,True,0.124,53.7,69.9,48.4,-0.1476,B,True,1.6,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.6173,-0.0802,,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5592,,,,False,v28-pro-max,betting_brain_approved
1r7iq0nhg2b674jpcm92ragpg,2026-05-24,1zp1du9n4rj36p1ss9zbxtqfb,4,0,1,0,BTTS,KG Var,1.89,0.2,True,False,-0.2,53.2,69.9,52.9,-0.0832,B,True,0.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5317,0.5291,0.0026,,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5353,,,,False,v28-pro-max,betting_brain_approved
70dgok3yq76g076vemaps0178,2026-05-24,6lwpjhktjhl9g7x2w7njmzva6,2,1,1,0,BTTS,KG Var,1.74,0.2,True,True,0.148,51.9,68.4,60.4,-0.1255,B,True,15.7,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5192,0.5747,-0.0555,,True,AGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.6164,,,,False,v28-pro-max,betting_brain_approved
72q9d4uimmby6g6bor26taz9w,2026-05-24,6jgwiu2gq3dllmrwt45pfdn2z,2,0,2,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.555,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,LOW,,,,,False,v28-pro-max,no_bet_conditions_met
67dd2t7043kv0yw1zj1buwdg4,2026-05-24,8n9w0n3i9kk05echhtmstn6o9,1,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.698,missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
ejdwfph35q57phtfz7jr8st1w,2026-05-24,8ey0ww2zsosdmwr8ehsorh6t7,0,2,0,0,BTTS,KG Var,1.82,0.2,True,False,-0.2,53.2,69.9,63.5,-0.1097,B,True,12.1,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5317,0.5495,-0.0178,,True,DISAGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.51,,X/X,,False,v28-pro-max,betting_brain_approved
68y8tlfnilw5trs1oqi4dhfkk,2026-05-24,8n9w0n3i9kk05echhtmstn6o9,2,2,1,1,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,DISAGREE,0.535,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
68fghiwdtwspk1m0ft5mspzx0,2026-05-24,8n9w0n3i9kk05echhtmstn6o9,1,2,1,2,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,DISAGREE,0.602,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
a1kqq0ggywfl6sl4srntxy2hg,2026-05-24,3ww12jab49q8q8mk9avdwjqgk,1,1,1,0,OU25,Üst,1.62,0.2,True,False,-0.2,61.4,65.4,60.6,-0.148,B,True,27.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.6173,-0.0029,0.0854,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5961,,,,False,v28-pro-max,betting_brain_approved
a1vpp4i6t61v7qm3dfy6iuj2s,2026-05-24,3ww12jab49q8q8mk9avdwjqgk,2,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.74,missing_full_ms_odds;live_match_pre_match_features;ai_features_inferred_from_history,LOW,,,,,False,v28-pro-max,no_bet_conditions_met
28rk26zqah60qj7h78qh5beac,2026-05-24,663a54fmymndjeev47qm7d3nf,2,2,2,1,OU25,Üst,1.39,0.2,True,True,0.078,61.1,65.4,57.5,-0.2039,B,True,13.3,BET,,low_reliability_league;inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6107,0.7194,-0.1087,0.0577,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,0.3522,,,,False,v28-pro-max,betting_brain_approved
a26stt54g5ju5ecodpcbcqxw4,2026-05-24,3ww12jab49q8q8mk9avdwjqgk,2,2,1,1,BTTS,KG Var,2.05,0.2,True,True,0.21,53.7,69.9,70.6,0.0354,B,True,33.5,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;usable_historical_sample,0.5371,0.4878,0.0493,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5961,,,,False,v28-pro-max,betting_brain_approved
318rrnl9b1vlenjd9hwahpvro,2026-05-24,9z5643nd06afqu01ea2wt8y4g,1,0,0,0,DC,1X,1.12,0.0,False,True,0.0,73.5,73.5,72.9,-0.0553,PASS,True,41.8,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features,value_sniper_override;v25_v27_aligned;triple_value_confirmed;usable_historical_sample,0.7351,0.8929,-0.1578,0.031,False,AGREE,0.51,lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8734,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
65tlfs3m6sc70261z37i90jys,2026-05-24,89ovpy1rarewwzqvi30bfdr8b,4,3,1,1,OU25,Üst,1.36,0.2,True,True,0.072,60.2,65.4,53.5,-0.2107,B,True,24.8,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6018,0.7353,-0.1335,0.0982,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.7068,,,,False,v28-pro-max,betting_brain_approved
ei6nilo5074tnb17kvv37oy6s,2026-05-24,3j81qr7yc4gdnakfwnxf95ovh,1,1,1,1,MS,2,1.11,0.0,False,False,0.0,75.5,90.9,66.9,-0.0497,PASS,True,0.0,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features;v25_v27_soft_disagreement;triple_value_not_confirmed,value_sniper_override;strong_historical_sample,0.7554,0.9009,-0.1455,0.204,False,AGREE,0.51,lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8771,0.0,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
cx8nb7w3nmell38mta1umh2qc,2026-05-24,54c65mhi143utomzvvv3q2avh,0,2,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.726,missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
53348iqniod61z7xurb4tx250,2026-05-24,477yyajzheg2z8u7uick0e13e,2,0,0,0,OU25,Üst,1.93,0.2,True,False,-0.2,59.5,65.4,71.3,0.0414,B,True,45.8,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5955,0.5181,0.0774,0.1084,False,AGREE,0.74,lineup_probable_not_confirmed;ai_features_inferred_from_history,MEDIUM,0.4706,,,,False,v28-pro-max,betting_brain_approved
2hw717s7fxi2v2w53kdsplhqs,2026-05-24,8najqkluatpaxvqws78b9s17c,1,1,0,0,DC,12,1.24,0.0,False,False,0.0,70.0,70.0,61.2,-0.12,PASS,True,0.0,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,value_sniper_override;v25_v27_aligned;strong_historical_sample,0.7001,0.8065,-0.1064,0.0075,True,AGREE,0.51,lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,HIGH,0.5082,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
1q1s55dy4d4z4gs34qk6vx9n8,2026-05-24,cegl2ivkc25blcatxp4jmk1ec,1,0,1,0,OU25,Üst,2.03,0.2,True,False,-0.2,50.0,57.7,54.3,0.0546,B,True,35.7,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5,0.4926,0.0074,0.0555,False,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5993,,,,False,v28-pro-max,betting_brain_approved
648u8zd49cwcxpspmvinlmexg,2026-05-24,1eruend45vd20g9hbrpiggs5u,1,0,0,0,MS,1,1.32,0.2,True,True,0.064,63.1,65.5,55.7,-0.0828,B,True,39.4,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6313,0.7576,-0.1263,0.0683,False,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8083,0.0,,,False,v28-pro-max,betting_brain_approved
1psnufak57w8dfs9e5cvbmgwk,2026-05-24,cegl2ivkc25blcatxp4jmk1ec,3,0,2,0,BTTS,KG Var,1.94,0.2,True,False,-0.2,53.7,69.9,53.1,-0.0765,B,True,5.3,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.5155,0.0216,,True,AGREE,0.74,lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,HIGH,0.5993,,,,False,v28-pro-max,betting_brain_approved
921kqaviappxt0w1kfmq1ek2c,2026-05-24,byu00jvt1j6csyv4y1lkt2fm2,1,0,1,0,DC,X2,1.18,0.0,False,False,0.0,75.8,75.8,74.9,-0.1267,PASS,True,0.0,WATCH_NO_VALUE,odds_below_minimum,inferred_statistical_features;v25_v27_soft_disagreement;trap_market_market_overpriced;triple_value_not_confirmed;engine_consensus_disagree,base_model_playable;value_sniper_override;strong_historical_sample,0.7584,0.8475,-0.0891,0.1783,True,DISAGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.5359,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
3m11hvh2fzailt3ykd0uhzz84,2026-05-24,54c65mhi143utomzvvv3q2avh,0,0,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.591,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
7kvvf6blnps2xk15100ccdedw,2026-05-24,4zwgbb66rif2spcoeeol2motx,5,0,3,0,BTTS,KG Var,1.33,0.2,True,False,-0.2,57.1,69.9,62.3,-0.1512,B,True,29.1,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5707,0.7519,-0.1812,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.6995,,1/1,,False,v28-pro-max,betting_brain_approved
7liir8zj32o7m2udr7cknb8d0,2026-05-24,4zwgbb66rif2spcoeeol2motx,3,0,2,0,OU25,Üst,1.33,0.2,True,True,0.066,61.4,65.4,58.3,-0.1437,B,True,27.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.7519,-0.1375,0.0279,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.6995,,1/1,,False,v28-pro-max,betting_brain_approved
7l74ilyz7olljclexvn8tbjtg,2026-05-24,4zwgbb66rif2spcoeeol2motx,5,1,4,0,BTTS,KG Var,1.55,0.2,True,True,0.11,57.1,69.9,68.2,-0.0873,B,True,32.7,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5707,0.6452,-0.0745,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.6995,,1/1,,False,v28-pro-max,betting_brain_approved
8f6gex4eh119d2hh9y2zb5clw,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,2,0,2,0,OU25,Üst,1.49,0.2,True,False,-0.2,66.5,65.4,77.9,0.0081,B,True,50.1,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6651,0.6711,-0.006,0.1144,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.5033,,1/1,,False,v28-pro-max,betting_brain_approved
8ee7ipt4u6kyk6baueedsdafo,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,0,2,0,2,BTTS,KG Var,1.69,0.2,True,False,-0.2,54.3,69.9,64.7,-0.0792,B,True,27.6,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5433,0.5917,-0.0484,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,2/2,,False,v28-pro-max,betting_brain_approved
8fydg367drpc25r1bobxqj3f8,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,3,1,2,1,OU25,Üst,1.61,0.2,True,True,0.122,50.1,57.7,50.7,-0.1072,B,True,35.0,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5009,0.6211,-0.1202,0.0553,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,,,False,v28-pro-max,betting_brain_approved
8fkdhce1peguwgnsunwoln3f8,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,0,2,0,2,OU25,Üst,1.24,0.0,False,False,0.0,61.4,65.4,61.1,-0.1548,PASS,True,16.8,WATCH_NO_VALUE,odds_below_minimum,inferred_statistical_features;triple_value_not_confirmed;historical_sample_too_low,base_model_playable;value_sniper_override;v25_v27_aligned,0.6144,0.8065,-0.1921,0.0942,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.5033,,,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
9g5hqtjja6ceqhkpghwmoy6ms,2026-05-24,2y8bntiif3a9y6gtmauv30gt,2,0,1,0,OU25,Üst,1.71,0.2,True,False,-0.2,50.1,57.7,52.6,-0.0794,B,True,36.5,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5009,0.5848,-0.0839,0.0481,False,DISAGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.4782,,,,False,v28-pro-max,betting_brain_approved
8h6429zr5ijqcxc8gjxygjtw4,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,3,0,1,0,MS,1,1.33,0.2,True,True,0.066,66.1,65.5,66.7,-0.1155,B,True,0.0,BET,,inferred_statistical_features;v25_v27_soft_disagreement;trap_market_market_overpriced;triple_value_not_confirmed;htft_reversal_prob_minor=0.11,base_model_playable;value_sniper_override;strong_historical_sample,0.6614,0.7519,-0.0905,0.2583,True,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,0.0814,1/1,,False,v28-pro-max,betting_brain_approved
77knm2ibdtb7akzrbltwz7axg,2026-05-24,bly7ema5au6j40i0grhl0pnub,1,1,1,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.726,missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
8es4680yd87gtmomg2jk3isyc,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,0,1,0,0,OU25,Üst,1.53,0.2,True,False,-0.2,59.5,65.4,67.8,-0.0259,B,True,42.4,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5955,0.6536,-0.0581,0.0713,False,DISAGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,2/2,,False,v28-pro-max,betting_brain_approved
8dmcz3k1u4ze53nvrsoz7eoes,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,1,1,1,0,BTTS,KG Var,1.26,0.0,False,True,0.0,54.9,69.9,57.6,-0.2005,PASS,True,3.3,WATCH_NO_VALUE,odds_below_minimum,inferred_statistical_features;triple_value_not_confirmed;historical_sample_too_low,base_model_playable;value_sniper_override,0.5488,0.7937,-0.2449,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.5033,,1/1,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
8gcbai6m7v7o8piqfram4qe50,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,3,1,2,0,HTFT,1/1,4.59,0.0,False,True,0.0,27.3,27.3,24.6,0.1657,PASS,True,0.0,REJECT,calibrated_confidence_too_low;play_score_too_low;volatile_market_requires_exceptional_evidence,inferred_statistical_features;historical_sample_too_low,base_model_playable,0.2734,0.2179,0.0555,,False,DISAGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,1/1,,False,v28-pro-max,betting_brain_no_safe_pick
3azy3enp78au0zfugc3l1yf4k,2026-05-24,54c65mhi143utomzvvv3q2avh,2,0,1,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.532,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
1d2fb7bt5f8xy5on24w1kj1g4,2026-05-24,54c65mhi143utomzvvv3q2avh,1,0,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.532,missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history,LOW,,,,,False,v28-pro-max,no_bet_conditions_met
pw01xm8v3jlz13fpi3zq0ftg,2026-05-24,3umprqta6ipyann6qjjh07biz,1,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
mjo9k4zr1x884vjlwea2y1hw,2026-05-24,3umprqta6ipyann6qjjh07biz,1,0,1,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
8d8fm7wli7tfx8hm9w5l8nuhg,2026-05-24,3is4bkgf3loxv9qfg3hm8zfqb,1,1,1,0,BTTS,KG Var,1.72,0.2,True,True,0.144,53.7,69.9,65.2,-0.0712,B,True,28.0,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.5814,-0.0443,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.5033,,,,False,v28-pro-max,betting_brain_approved
oqsq3f0kvic8xfed8dp302z8,2026-05-24,3umprqta6ipyann6qjjh07biz,3,2,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
o7tn4si7fxvq9c2mg0xs48wk,2026-05-24,3umprqta6ipyann6qjjh07biz,0,1,0,0,,,,1.0,False,,0.0,,,,,,False,,,,,,,,,,False,AGREE,0.33,missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,,,,,False,v28-pro-max,no_bet_conditions_met
eh9jfegscokidyczxfq691990,2026-05-24,3j81qr7yc4gdnakfwnxf95ovh,2,3,0,1,OU25,Üst,1.44,0.2,True,True,0.088,50.1,57.7,32.9,-0.2537,B,True,17.0,BET,,inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.5009,0.6944,-0.1935,0.0596,True,AGREE,0.51,lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history,MEDIUM,0.8771,,,,False,v28-pro-max,betting_brain_approved
dkhhkbwnxwl47e8hybv89mwb8,2026-05-24,5jd0k2txwnq69frs79eulba8j,1,2,0,1,OU25,Üst,1.23,0.0,False,True,0.0,61.4,65.4,61.2,-0.1185,PASS,True,11.4,WATCH_NO_VALUE,odds_below_minimum,base_model_not_playable;inferred_statistical_features;triple_value_not_confirmed,value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.813,-0.1986,0.0179,False,AGREE,0.74,ai_features_inferred_from_history,LOW,0.9233,,1/1,,False,v28-pro-max,betting_brain_no_value_odds_below_minimum
1lknqdz9vmb3hnqu144zkkefo,2026-05-24,1r097lpxe0xn03ihb7wi98kao,1,0,1,0,BTTS,KG Var,1.78,0.2,True,False,-0.2,50.0,61.7,55.6,-0.088,B,True,29.3,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5,0.5618,-0.0618,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,MEDIUM,0.7391,,1/1,,False,v28-pro-max,betting_brain_approved
3oazp9kfbyyiatn246k4to6xg,2026-05-24,9ynnnx1qmkizq1o3qr3v0nsuk,1,2,0,1,BTTS,KG Var,1.36,0.2,True,True,0.072,53.7,69.9,61.2,-0.1571,B,True,33.7,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;strong_historical_sample,0.5371,0.7353,-0.1982,,False,AGREE,0.74,live_match_pre_match_features;ai_features_inferred_from_history,LOW,0.554,,2/2,,False,v28-pro-max,betting_brain_approved
8cr8t6qh0r6g0mv6ftq0ic1sk,2026-05-24,a9vrdkelbgif0gtu3wxsr75xo,2,1,0,1,OU25,Üst,1.46,0.2,True,True,0.092,61.4,65.4,68.1,-0.0182,B,True,47.8,BET,,inferred_statistical_features;triple_value_not_confirmed,base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample,0.6144,0.6849,-0.0705,0.0535,False,AGREE,0.74,ai_features_inferred_from_history,MEDIUM,0.6618,,,,False,v28-pro-max,betting_brain_approved
1 match_id match_date league_id score_home score_away ht_score_home ht_score_away market pick odds stake_units playable won unit_profit raw_confidence calibrated_confidence play_score ev_edge bet_grade is_value_sniper bb_score bb_action bb_vetoes bb_issues bb_positives bb_model_prob bb_implied_prob bb_model_market_gap bb_divergence bb_trap_market v27_consensus data_quality_score data_quality_flags risk_level odds_reliability htft_reversal_prob htft_top_pick league_name is_cup model_version decision_reason
2 5iam9c9dw3ggz3y1ohr9uh53o 2026-05-24 8nbwkj392b0xzssqpw9jwmzdn 0 0 0 0 1.0 False 0.0 False False AGREE 0.33 missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
3 8c90p2ft4zxjdck8wlgq1a61g 2026-05-24 a9vrdkelbgif0gtu3wxsr75xo 2 2 0 0 OU25 Üst 1.41 0.2 True True 0.082 59.5 65.4 58.4 -0.1557 B True 26.4 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.5955 0.7092 -0.1137 0.0339 True DISAGREE 0.74 ai_features_inferred_from_history MEDIUM 0.6618 False v28-pro-max betting_brain_approved
4 9ljz1grea3a8jajif4e9b7bpw 2026-05-24 2wolc27r8z03itcvwp43e38c5 1 1 1 1 BTTS KG Var 1.62 0.2 True True 0.124 53.7 69.9 48.4 -0.1476 B True 1.6 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5371 0.6173 -0.0802 True AGREE 0.74 lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history HIGH 0.5592 False v28-pro-max betting_brain_approved
5 1r7iq0nhg2b674jpcm92ragpg 2026-05-24 1zp1du9n4rj36p1ss9zbxtqfb 4 0 1 0 BTTS KG Var 1.89 0.2 True False -0.2 53.2 69.9 52.9 -0.0832 B True 0.0 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5317 0.5291 0.0026 True AGREE 0.74 lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history HIGH 0.5353 False v28-pro-max betting_brain_approved
6 70dgok3yq76g076vemaps0178 2026-05-24 6lwpjhktjhl9g7x2w7njmzva6 2 1 1 0 BTTS KG Var 1.74 0.2 True True 0.148 51.9 68.4 60.4 -0.1255 B True 15.7 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5192 0.5747 -0.0555 True AGREE 0.74 ai_features_inferred_from_history MEDIUM 0.6164 False v28-pro-max betting_brain_approved
7 72q9d4uimmby6g6bor26taz9w 2026-05-24 6jgwiu2gq3dllmrwt45pfdn2z 2 0 2 0 1.0 False 0.0 False False AGREE 0.555 missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history LOW False v28-pro-max no_bet_conditions_met
8 67dd2t7043kv0yw1zj1buwdg4 2026-05-24 8n9w0n3i9kk05echhtmstn6o9 1 1 0 0 1.0 False 0.0 False False AGREE 0.698 missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
9 ejdwfph35q57phtfz7jr8st1w 2026-05-24 8ey0ww2zsosdmwr8ehsorh6t7 0 2 0 0 BTTS KG Var 1.82 0.2 True False -0.2 53.2 69.9 63.5 -0.1097 B True 12.1 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5317 0.5495 -0.0178 True DISAGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.51 X/X False v28-pro-max betting_brain_approved
10 68y8tlfnilw5trs1oqi4dhfkk 2026-05-24 8n9w0n3i9kk05echhtmstn6o9 2 2 1 1 1.0 False 0.0 False False DISAGREE 0.535 missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
11 68fghiwdtwspk1m0ft5mspzx0 2026-05-24 8n9w0n3i9kk05echhtmstn6o9 1 2 1 2 1.0 False 0.0 False False DISAGREE 0.602 missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
12 a1kqq0ggywfl6sl4srntxy2hg 2026-05-24 3ww12jab49q8q8mk9avdwjqgk 1 1 1 0 OU25 Üst 1.62 0.2 True False -0.2 61.4 65.4 60.6 -0.148 B True 27.0 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6144 0.6173 -0.0029 0.0854 True AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5961 False v28-pro-max betting_brain_approved
13 a1vpp4i6t61v7qm3dfy6iuj2s 2026-05-24 3ww12jab49q8q8mk9avdwjqgk 2 1 0 0 1.0 False 0.0 False False AGREE 0.74 missing_full_ms_odds;live_match_pre_match_features;ai_features_inferred_from_history LOW False v28-pro-max no_bet_conditions_met
14 28rk26zqah60qj7h78qh5beac 2026-05-24 663a54fmymndjeev47qm7d3nf 2 2 2 1 OU25 Üst 1.39 0.2 True True 0.078 61.1 65.4 57.5 -0.2039 B True 13.3 BET low_reliability_league;inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6107 0.7194 -0.1087 0.0577 True AGREE 0.74 lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history MEDIUM 0.3522 False v28-pro-max betting_brain_approved
15 a26stt54g5ju5ecodpcbcqxw4 2026-05-24 3ww12jab49q8q8mk9avdwjqgk 2 2 1 1 BTTS KG Var 2.05 0.2 True True 0.21 53.7 69.9 70.6 0.0354 B True 33.5 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;usable_historical_sample 0.5371 0.4878 0.0493 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5961 False v28-pro-max betting_brain_approved
16 318rrnl9b1vlenjd9hwahpvro 2026-05-24 9z5643nd06afqu01ea2wt8y4g 1 0 0 0 DC 1X 1.12 0.0 False True 0.0 73.5 73.5 72.9 -0.0553 PASS True 41.8 WATCH_NO_VALUE odds_below_minimum base_model_not_playable;inferred_statistical_features value_sniper_override;v25_v27_aligned;triple_value_confirmed;usable_historical_sample 0.7351 0.8929 -0.1578 0.031 False AGREE 0.51 lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM 0.8734 False v28-pro-max betting_brain_no_value_odds_below_minimum
17 65tlfs3m6sc70261z37i90jys 2026-05-24 89ovpy1rarewwzqvi30bfdr8b 4 3 1 1 OU25 Üst 1.36 0.2 True True 0.072 60.2 65.4 53.5 -0.2107 B True 24.8 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6018 0.7353 -0.1335 0.0982 True AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.7068 False v28-pro-max betting_brain_approved
18 ei6nilo5074tnb17kvv37oy6s 2026-05-24 3j81qr7yc4gdnakfwnxf95ovh 1 1 1 1 MS 2 1.11 0.0 False False 0.0 75.5 90.9 66.9 -0.0497 PASS True 0.0 WATCH_NO_VALUE odds_below_minimum base_model_not_playable;inferred_statistical_features;v25_v27_soft_disagreement;triple_value_not_confirmed value_sniper_override;strong_historical_sample 0.7554 0.9009 -0.1455 0.204 False AGREE 0.51 lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM 0.8771 0.0 False v28-pro-max betting_brain_no_value_odds_below_minimum
19 cx8nb7w3nmell38mta1umh2qc 2026-05-24 54c65mhi143utomzvvv3q2avh 0 2 0 0 1.0 False 0.0 False False AGREE 0.726 missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
20 53348iqniod61z7xurb4tx250 2026-05-24 477yyajzheg2z8u7uick0e13e 2 0 0 0 OU25 Üst 1.93 0.2 True False -0.2 59.5 65.4 71.3 0.0414 B True 45.8 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.5955 0.5181 0.0774 0.1084 False AGREE 0.74 lineup_probable_not_confirmed;ai_features_inferred_from_history MEDIUM 0.4706 False v28-pro-max betting_brain_approved
21 2hw717s7fxi2v2w53kdsplhqs 2026-05-24 8najqkluatpaxvqws78b9s17c 1 1 0 0 DC 12 1.24 0.0 False False 0.0 70.0 70.0 61.2 -0.12 PASS True 0.0 WATCH_NO_VALUE odds_below_minimum base_model_not_playable;inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed value_sniper_override;v25_v27_aligned;strong_historical_sample 0.7001 0.8065 -0.1064 0.0075 True AGREE 0.51 lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history HIGH 0.5082 False v28-pro-max betting_brain_no_value_odds_below_minimum
22 1q1s55dy4d4z4gs34qk6vx9n8 2026-05-24 cegl2ivkc25blcatxp4jmk1ec 1 0 1 0 OU25 Üst 2.03 0.2 True False -0.2 50.0 57.7 54.3 0.0546 B True 35.7 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.5 0.4926 0.0074 0.0555 False AGREE 0.74 lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history HIGH 0.5993 False v28-pro-max betting_brain_approved
23 648u8zd49cwcxpspmvinlmexg 2026-05-24 1eruend45vd20g9hbrpiggs5u 1 0 0 0 MS 1 1.32 0.2 True True 0.064 63.1 65.5 55.7 -0.0828 B True 39.4 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6313 0.7576 -0.1263 0.0683 False AGREE 0.74 lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history MEDIUM 0.8083 0.0 False v28-pro-max betting_brain_approved
24 1psnufak57w8dfs9e5cvbmgwk 2026-05-24 cegl2ivkc25blcatxp4jmk1ec 3 0 2 0 BTTS KG Var 1.94 0.2 True False -0.2 53.7 69.9 53.1 -0.0765 B True 5.3 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5371 0.5155 0.0216 True AGREE 0.74 lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history HIGH 0.5993 False v28-pro-max betting_brain_approved
25 921kqaviappxt0w1kfmq1ek2c 2026-05-24 byu00jvt1j6csyv4y1lkt2fm2 1 0 1 0 DC X2 1.18 0.0 False False 0.0 75.8 75.8 74.9 -0.1267 PASS True 0.0 WATCH_NO_VALUE odds_below_minimum inferred_statistical_features;v25_v27_soft_disagreement;trap_market_market_overpriced;triple_value_not_confirmed;engine_consensus_disagree base_model_playable;value_sniper_override;strong_historical_sample 0.7584 0.8475 -0.0891 0.1783 True DISAGREE 0.74 ai_features_inferred_from_history MEDIUM 0.5359 False v28-pro-max betting_brain_no_value_odds_below_minimum
26 3m11hvh2fzailt3ykd0uhzz84 2026-05-24 54c65mhi143utomzvvv3q2avh 0 0 0 0 1.0 False 0.0 False False AGREE 0.591 missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
27 7kvvf6blnps2xk15100ccdedw 2026-05-24 4zwgbb66rif2spcoeeol2motx 5 0 3 0 BTTS KG Var 1.33 0.2 True False -0.2 57.1 69.9 62.3 -0.1512 B True 29.1 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5707 0.7519 -0.1812 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.6995 1/1 False v28-pro-max betting_brain_approved
28 7liir8zj32o7m2udr7cknb8d0 2026-05-24 4zwgbb66rif2spcoeeol2motx 3 0 2 0 OU25 Üst 1.33 0.2 True True 0.066 61.4 65.4 58.3 -0.1437 B True 27.0 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6144 0.7519 -0.1375 0.0279 True AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.6995 1/1 False v28-pro-max betting_brain_approved
29 7l74ilyz7olljclexvn8tbjtg 2026-05-24 4zwgbb66rif2spcoeeol2motx 5 1 4 0 BTTS KG Var 1.55 0.2 True True 0.11 57.1 69.9 68.2 -0.0873 B True 32.7 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5707 0.6452 -0.0745 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.6995 1/1 False v28-pro-max betting_brain_approved
30 8f6gex4eh119d2hh9y2zb5clw 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 2 0 2 0 OU25 Üst 1.49 0.2 True False -0.2 66.5 65.4 77.9 0.0081 B True 50.1 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6651 0.6711 -0.006 0.1144 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history LOW 0.5033 1/1 False v28-pro-max betting_brain_approved
31 8ee7ipt4u6kyk6baueedsdafo 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 0 2 0 2 BTTS KG Var 1.69 0.2 True False -0.2 54.3 69.9 64.7 -0.0792 B True 27.6 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5433 0.5917 -0.0484 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5033 2/2 False v28-pro-max betting_brain_approved
32 8fydg367drpc25r1bobxqj3f8 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 3 1 2 1 OU25 Üst 1.61 0.2 True True 0.122 50.1 57.7 50.7 -0.1072 B True 35.0 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.5009 0.6211 -0.1202 0.0553 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5033 False v28-pro-max betting_brain_approved
33 8fkdhce1peguwgnsunwoln3f8 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 0 2 0 2 OU25 Üst 1.24 0.0 False False 0.0 61.4 65.4 61.1 -0.1548 PASS True 16.8 WATCH_NO_VALUE odds_below_minimum inferred_statistical_features;triple_value_not_confirmed;historical_sample_too_low base_model_playable;value_sniper_override;v25_v27_aligned 0.6144 0.8065 -0.1921 0.0942 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history LOW 0.5033 False v28-pro-max betting_brain_no_value_odds_below_minimum
34 9g5hqtjja6ceqhkpghwmoy6ms 2026-05-24 2y8bntiif3a9y6gtmauv30gt 2 0 1 0 OU25 Üst 1.71 0.2 True False -0.2 50.1 57.7 52.6 -0.0794 B True 36.5 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.5009 0.5848 -0.0839 0.0481 False DISAGREE 0.74 ai_features_inferred_from_history MEDIUM 0.4782 False v28-pro-max betting_brain_approved
35 8h6429zr5ijqcxc8gjxygjtw4 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 3 0 1 0 MS 1 1.33 0.2 True True 0.066 66.1 65.5 66.7 -0.1155 B True 0.0 BET inferred_statistical_features;v25_v27_soft_disagreement;trap_market_market_overpriced;triple_value_not_confirmed;htft_reversal_prob_minor=0.11 base_model_playable;value_sniper_override;strong_historical_sample 0.6614 0.7519 -0.0905 0.2583 True AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5033 0.0814 1/1 False v28-pro-max betting_brain_approved
36 77knm2ibdtb7akzrbltwz7axg 2026-05-24 bly7ema5au6j40i0grhl0pnub 1 1 1 0 1.0 False 0.0 False False AGREE 0.726 missing_full_ms_odds;lineup_probable_not_confirmed;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
37 8es4680yd87gtmomg2jk3isyc 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 0 1 0 0 OU25 Üst 1.53 0.2 True False -0.2 59.5 65.4 67.8 -0.0259 B True 42.4 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.5955 0.6536 -0.0581 0.0713 False DISAGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5033 2/2 False v28-pro-max betting_brain_approved
38 8dmcz3k1u4ze53nvrsoz7eoes 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 1 1 1 0 BTTS KG Var 1.26 0.0 False True 0.0 54.9 69.9 57.6 -0.2005 PASS True 3.3 WATCH_NO_VALUE odds_below_minimum inferred_statistical_features;triple_value_not_confirmed;historical_sample_too_low base_model_playable;value_sniper_override 0.5488 0.7937 -0.2449 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history LOW 0.5033 1/1 False v28-pro-max betting_brain_no_value_odds_below_minimum
39 8gcbai6m7v7o8piqfram4qe50 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 3 1 2 0 HTFT 1/1 4.59 0.0 False True 0.0 27.3 27.3 24.6 0.1657 PASS True 0.0 REJECT calibrated_confidence_too_low;play_score_too_low;volatile_market_requires_exceptional_evidence inferred_statistical_features;historical_sample_too_low base_model_playable 0.2734 0.2179 0.0555 False DISAGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5033 1/1 False v28-pro-max betting_brain_no_safe_pick
40 3azy3enp78au0zfugc3l1yf4k 2026-05-24 54c65mhi143utomzvvv3q2avh 2 0 1 0 1.0 False 0.0 False False AGREE 0.532 missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
41 1d2fb7bt5f8xy5on24w1kj1g4 2026-05-24 54c65mhi143utomzvvv3q2avh 1 0 0 0 1.0 False 0.0 False False AGREE 0.532 missing_full_ms_odds;lineup_probable_not_confirmed;lineup_projection_low_confidence;lineup_incomplete;missing_referee;ai_features_inferred_from_history LOW False v28-pro-max no_bet_conditions_met
42 pw01xm8v3jlz13fpi3zq0ftg 2026-05-24 3umprqta6ipyann6qjjh07biz 1 1 0 0 1.0 False 0.0 False False AGREE 0.33 missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
43 mjo9k4zr1x884vjlwea2y1hw 2026-05-24 3umprqta6ipyann6qjjh07biz 1 0 1 0 1.0 False 0.0 False False AGREE 0.33 missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
44 8d8fm7wli7tfx8hm9w5l8nuhg 2026-05-24 3is4bkgf3loxv9qfg3hm8zfqb 1 1 1 0 BTTS KG Var 1.72 0.2 True True 0.144 53.7 69.9 65.2 -0.0712 B True 28.0 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5371 0.5814 -0.0443 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.5033 False v28-pro-max betting_brain_approved
45 oqsq3f0kvic8xfed8dp302z8 2026-05-24 3umprqta6ipyann6qjjh07biz 3 2 0 0 1.0 False 0.0 False False AGREE 0.33 missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
46 o7tn4si7fxvq9c2mg0xs48wk 2026-05-24 3umprqta6ipyann6qjjh07biz 0 1 0 0 1.0 False 0.0 False False AGREE 0.33 missing_full_ms_odds;lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM False v28-pro-max no_bet_conditions_met
47 eh9jfegscokidyczxfq691990 2026-05-24 3j81qr7yc4gdnakfwnxf95ovh 2 3 0 1 OU25 Üst 1.44 0.2 True True 0.088 50.1 57.7 32.9 -0.2537 B True 17.0 BET inferred_statistical_features;trap_market_market_overpriced;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.5009 0.6944 -0.1935 0.0596 True AGREE 0.51 lineup_unavailable;lineup_incomplete;missing_referee;ai_features_inferred_from_history MEDIUM 0.8771 False v28-pro-max betting_brain_approved
48 dkhhkbwnxwl47e8hybv89mwb8 2026-05-24 5jd0k2txwnq69frs79eulba8j 1 2 0 1 OU25 Üst 1.23 0.0 False True 0.0 61.4 65.4 61.2 -0.1185 PASS True 11.4 WATCH_NO_VALUE odds_below_minimum base_model_not_playable;inferred_statistical_features;triple_value_not_confirmed value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6144 0.813 -0.1986 0.0179 False AGREE 0.74 ai_features_inferred_from_history LOW 0.9233 1/1 False v28-pro-max betting_brain_no_value_odds_below_minimum
49 1lknqdz9vmb3hnqu144zkkefo 2026-05-24 1r097lpxe0xn03ihb7wi98kao 1 0 1 0 BTTS KG Var 1.78 0.2 True False -0.2 50.0 61.7 55.6 -0.088 B True 29.3 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5 0.5618 -0.0618 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history MEDIUM 0.7391 1/1 False v28-pro-max betting_brain_approved
50 3oazp9kfbyyiatn246k4to6xg 2026-05-24 9ynnnx1qmkizq1o3qr3v0nsuk 1 2 0 1 BTTS KG Var 1.36 0.2 True True 0.072 53.7 69.9 61.2 -0.1571 B True 33.7 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;strong_historical_sample 0.5371 0.7353 -0.1982 False AGREE 0.74 live_match_pre_match_features;ai_features_inferred_from_history LOW 0.554 2/2 False v28-pro-max betting_brain_approved
51 8cr8t6qh0r6g0mv6ftq0ic1sk 2026-05-24 a9vrdkelbgif0gtu3wxsr75xo 2 1 0 1 OU25 Üst 1.46 0.2 True True 0.092 61.4 65.4 68.1 -0.0182 B True 47.8 BET inferred_statistical_features;triple_value_not_confirmed base_model_playable;value_sniper_override;v25_v27_aligned;strong_historical_sample 0.6144 0.6849 -0.0705 0.0535 False AGREE 0.74 ai_features_inferred_from_history MEDIUM 0.6618 False v28-pro-max betting_brain_approved
@@ -0,0 +1,220 @@
{
"args": {
"days": 3,
"max_matches": 50,
"start": null,
"end": null,
"progress_interval": 50
},
"aggregate": {
"overall": {
"n_total": 50,
"n_playable_settled": 27,
"wins": 15,
"losses": 12,
"hit_rate_pct": 55.56,
"unit_profit": -0.862,
"staked": 5.4,
"roi_pct": -15.96
},
"by_market": {
"OU25": {
"n_total": 13,
"n_playable_settled": 13,
"wins": 7,
"losses": 6,
"hit_rate_pct": 53.85,
"unit_profit": -0.6,
"staked": 2.6,
"roi_pct": -23.08
},
"BTTS": {
"n_total": 12,
"n_playable_settled": 12,
"wins": 6,
"losses": 6,
"hit_rate_pct": 50.0,
"unit_profit": -0.392,
"staked": 2.4,
"roi_pct": -16.33
},
"MS": {
"n_total": 2,
"n_playable_settled": 2,
"wins": 2,
"losses": 0,
"hit_rate_pct": 100.0,
"unit_profit": 0.13,
"staked": 0.4,
"roi_pct": 32.5
}
},
"by_confidence": {
"65-70": {
"n_total": 22,
"n_playable_settled": 22,
"wins": 13,
"losses": 9,
"hit_rate_pct": 59.09,
"unit_profit": -0.472,
"staked": 4.4,
"roi_pct": -10.73
},
"55-60": {
"n_total": 4,
"n_playable_settled": 4,
"wins": 2,
"losses": 2,
"hit_rate_pct": 50.0,
"unit_profit": -0.19,
"staked": 0.8,
"roi_pct": -23.75
},
"60-65": {
"n_total": 1,
"n_playable_settled": 1,
"wins": 0,
"losses": 1,
"hit_rate_pct": 0.0,
"unit_profit": -0.2,
"staked": 0.2,
"roi_pct": -100.0
}
},
"by_odds": {
"1.3-1.5": {
"n_total": 11,
"n_playable_settled": 11,
"wins": 9,
"losses": 2,
"hit_rate_pct": 81.82,
"unit_profit": 0.28,
"staked": 2.2,
"roi_pct": 12.73
},
"1.5-1.8": {
"n_total": 10,
"n_playable_settled": 10,
"wins": 5,
"losses": 5,
"hit_rate_pct": 50.0,
"unit_profit": -0.352,
"staked": 2.0,
"roi_pct": -17.6
},
"1.8-2.2": {
"n_total": 6,
"n_playable_settled": 6,
"wins": 1,
"losses": 5,
"hit_rate_pct": 16.67,
"unit_profit": -0.79,
"staked": 1.2,
"roi_pct": -65.83
}
},
"by_grade": {
"B": {
"n_total": 27,
"n_playable_settled": 27,
"wins": 15,
"losses": 12,
"hit_rate_pct": 55.56,
"unit_profit": -0.862,
"staked": 5.4,
"roi_pct": -15.96
}
},
"by_competition": {
"league": {
"n_total": 27,
"n_playable_settled": 27,
"wins": 15,
"losses": 12,
"hit_rate_pct": 55.56,
"unit_profit": -0.862,
"staked": 5.4,
"roi_pct": -15.96
}
}
},
"loss_diagnostics": {
"n_losses": 12,
"total_loss_units": -2.4,
"patterns": {
"high_htft_reversal_prob (>=0.20)": [
0,
0.0
],
"cup_match": [
0,
0.0
],
"low_league_reliability (<0.45)": [
0,
0.0
],
"v27_disagree": [
3,
25.0
],
"trap_market_flagged": [
4,
33.33
],
"low_calibrated_conf (<55)": [
0,
0.0
],
"high_odds_underdog (>=2.5)": [
0,
0.0
],
"low_data_quality (<0.55)": [
0,
0.0
],
"high_risk_level": [
3,
25.0
],
"inferred_features": [
12,
100.0
]
},
"by_market": [
[
"BTTS",
6
],
[
"OU25",
6
]
],
"by_league": [
[
null,
12
]
],
"top_bb_issues_in_losses": [
[
"inferred_statistical_features",
12
],
[
"triple_value_not_confirmed",
12
],
[
"trap_market_market_overpriced",
4
]
],
"top_bb_vetoes_in_losses": []
},
"recommendations": [],
"errors_sample": []
}
@@ -0,0 +1,71 @@
==============================================================================
DIAGNOSTIC BACKTEST REPORT
==============================================================================
Generated: 2026-05-25T02:44:37
Sample window: start=-3d, end=now
Max matches: 50
Excluded days: ['2026-04-29', '2026-05-03']
OVERALL
------------------------------------------------------------------------------
n_total : 50
n_playable_settled : 27
wins : 15
losses : 12
hit_rate_pct : 55.56
unit_profit : -0.862
staked : 5.4
roi_pct : -15.96
PER MARKET
------------------------------------------------------------------------------
market n hit% profit roi%
OU25 13 53.85 -0.6 -23.08
BTTS 12 50.0 -0.392 -16.33
MS 2 100.0 0.13 32.5
PER CALIBRATED CONFIDENCE BAND
------------------------------------------------------------------------------
band n hit% roi%
55-60 4 50.0 -23.75
60-65 1 0.0 -100.0
65-70 22 59.09 -10.73
PER ODDS BAND
------------------------------------------------------------------------------
band n hit% roi%
1.3-1.5 11 81.82 12.73
1.5-1.8 10 50.0 -17.6
1.8-2.2 6 16.67 -65.83
LEAGUE vs CUP
------------------------------------------------------------------------------
league n= 27 hit=55.56% roi=-15.96%
LOSS DIAGNOSTICS
------------------------------------------------------------------------------
total losses: 12
total lost units: -2.4
By market: [('BTTS', 6), ('OU25', 6)]
Loss patterns (count, % of losses):
high_htft_reversal_prob (>=0.20) 0 (0.0%)
cup_match 0 (0.0%)
low_league_reliability (<0.45) 0 (0.0%)
v27_disagree 3 (25.0%)
trap_market_flagged 4 (33.33%)
low_calibrated_conf (<55) 0 (0.0%)
high_odds_underdog (>=2.5) 0 (0.0%)
low_data_quality (<0.55) 0 (0.0%)
high_risk_level 3 (25.0%)
inferred_features 12 (100.0%)
Top betting_brain issues seen in losses:
inferred_statistical_features 12
triple_value_not_confirmed 12
trap_market_market_overpriced 4
Top betting_brain vetoes (in losses — i.e. veto fired but bet still went through value-sniper override):
RECOMMENDATIONS
------------------------------------------------------------------------------
(none surfaced — sample too small or no clear pattern)
==============================================================================
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,335 @@
{
"args": {
"days": 14,
"max_matches": 1000,
"start": null,
"end": null,
"progress_interval": 50
},
"aggregate": {
"overall": {
"n_total": 1000,
"n_playable_settled": 524,
"wins": 287,
"losses": 237,
"hit_rate_pct": 54.77,
"unit_profit": -17.897,
"staked": 107.0,
"roi_pct": -16.73
},
"by_market": {
"OU25": {
"n_total": 236,
"n_playable_settled": 236,
"wins": 134,
"losses": 102,
"hit_rate_pct": 56.78,
"unit_profit": -6.271,
"staked": 48.5,
"roi_pct": -12.93
},
"BTTS": {
"n_total": 205,
"n_playable_settled": 205,
"wins": 105,
"losses": 100,
"hit_rate_pct": 51.22,
"unit_profit": -8.89,
"staked": 41.1,
"roi_pct": -21.63
},
"MS": {
"n_total": 76,
"n_playable_settled": 76,
"wins": 44,
"losses": 32,
"hit_rate_pct": 57.89,
"unit_profit": -2.396,
"staked": 16.0,
"roi_pct": -14.98
},
"OU35": {
"n_total": 3,
"n_playable_settled": 3,
"wins": 0,
"losses": 3,
"hit_rate_pct": 0.0,
"unit_profit": -0.6,
"staked": 0.6,
"roi_pct": -100.0
},
"DC": {
"n_total": 4,
"n_playable_settled": 4,
"wins": 4,
"losses": 0,
"hit_rate_pct": 100.0,
"unit_profit": 0.26,
"staked": 0.8,
"roi_pct": 32.5
}
},
"by_confidence": {
"65-70": {
"n_total": 420,
"n_playable_settled": 420,
"wins": 233,
"losses": 187,
"hit_rate_pct": 55.48,
"unit_profit": -14.057,
"staked": 85.1,
"roi_pct": -16.52
},
"60-65": {
"n_total": 33,
"n_playable_settled": 33,
"wins": 16,
"losses": 17,
"hit_rate_pct": 48.48,
"unit_profit": -1.61,
"staked": 6.6,
"roi_pct": -24.39
},
"55-60": {
"n_total": 52,
"n_playable_settled": 52,
"wins": 28,
"losses": 24,
"hit_rate_pct": 53.85,
"unit_profit": -0.668,
"staked": 10.5,
"roi_pct": -6.36
},
"50-55": {
"n_total": 5,
"n_playable_settled": 5,
"wins": 2,
"losses": 3,
"hit_rate_pct": 40.0,
"unit_profit": -0.64,
"staked": 1.3,
"roi_pct": -49.23
},
"45-50": {
"n_total": 8,
"n_playable_settled": 8,
"wins": 4,
"losses": 4,
"hit_rate_pct": 50.0,
"unit_profit": -0.382,
"staked": 1.9,
"roi_pct": -20.11
},
"70-80": {
"n_total": 6,
"n_playable_settled": 6,
"wins": 4,
"losses": 2,
"hit_rate_pct": 66.67,
"unit_profit": -0.54,
"staked": 1.6,
"roi_pct": -33.75
}
},
"by_odds": {
"1.3-1.5": {
"n_total": 241,
"n_playable_settled": 241,
"wins": 148,
"losses": 93,
"hit_rate_pct": 61.41,
"unit_profit": -7.408,
"staked": 49.0,
"roi_pct": -15.12
},
"1.5-1.8": {
"n_total": 221,
"n_playable_settled": 221,
"wins": 115,
"losses": 106,
"hit_rate_pct": 52.04,
"unit_profit": -6.926,
"staked": 44.3,
"roi_pct": -15.63
},
"1.8-2.2": {
"n_total": 56,
"n_playable_settled": 56,
"wins": 23,
"losses": 33,
"hit_rate_pct": 41.07,
"unit_profit": -2.789,
"staked": 12.2,
"roi_pct": -22.86
},
"2.2-3.0": {
"n_total": 5,
"n_playable_settled": 5,
"wins": 1,
"losses": 4,
"hit_rate_pct": 20.0,
"unit_profit": -0.574,
"staked": 1.3,
"roi_pct": -44.15
},
"3.0-5.0": {
"n_total": 1,
"n_playable_settled": 1,
"wins": 0,
"losses": 1,
"hit_rate_pct": 0.0,
"unit_profit": -0.2,
"staked": 0.2,
"roi_pct": -100.0
}
},
"by_grade": {
"B": {
"n_total": 518,
"n_playable_settled": 518,
"wins": 285,
"losses": 233,
"hit_rate_pct": 55.02,
"unit_profit": -16.931,
"staked": 105.3,
"roi_pct": -16.08
},
"A": {
"n_total": 6,
"n_playable_settled": 6,
"wins": 2,
"losses": 4,
"hit_rate_pct": 33.33,
"unit_profit": -0.966,
"staked": 1.7,
"roi_pct": -56.82
}
},
"by_competition": {
"league": {
"n_total": 524,
"n_playable_settled": 524,
"wins": 287,
"losses": 237,
"hit_rate_pct": 54.77,
"unit_profit": -17.897,
"staked": 107.0,
"roi_pct": -16.73
}
}
},
"loss_diagnostics": {
"n_losses": 237,
"total_loss_units": -48.7,
"patterns": {
"high_htft_reversal_prob (>=0.20)": [
0,
0.0
],
"cup_match": [
0,
0.0
],
"low_league_reliability (<0.45)": [
42,
17.72
],
"v27_disagree": [
60,
25.32
],
"trap_market_flagged": [
81,
34.18
],
"low_calibrated_conf (<55)": [
7,
2.95
],
"high_odds_underdog (>=2.5)": [
4,
1.69
],
"low_data_quality (<0.55)": [
40,
16.88
],
"high_risk_level": [
20,
8.44
],
"inferred_features": [
0,
0.0
]
},
"by_market": [
[
"OU25",
102
],
[
"BTTS",
100
],
[
"MS",
32
],
[
"OU35",
3
]
],
"by_league": [
[
null,
237
]
],
"top_bb_issues_in_losses": [
[
"triple_value_not_confirmed",
230
],
[
"trap_market_market_overpriced",
81
],
[
"low_reliability_league",
40
],
[
"v25_v27_soft_disagreement",
10
],
[
"engine_consensus_disagree",
5
],
[
"historical_sample_too_low",
3
],
[
"very_low_reliability_league",
2
],
[
"htft_reversal_prob_minor=0.13",
1
]
],
"top_bb_vetoes_in_losses": []
},
"recommendations": [
{
"type": "raise_confidence_threshold",
"confidence_band": "65-70",
"evidence": "n=420, roi=-16.52%",
"suggested_fix": "Raise MIN_BET_SCORE or market_min_conf above 65"
}
],
"errors_sample": []
}
@@ -0,0 +1,86 @@
==============================================================================
DIAGNOSTIC BACKTEST REPORT
==============================================================================
Generated: 2026-05-25T03:56:49
Sample window: start=-14d, end=now
Max matches: 1000
Excluded days: ['2026-04-29', '2026-05-03']
OVERALL
------------------------------------------------------------------------------
n_total : 1000
n_playable_settled : 524
wins : 287
losses : 237
hit_rate_pct : 54.77
unit_profit : -17.897
staked : 107.0
roi_pct : -16.73
PER MARKET
------------------------------------------------------------------------------
market n hit% profit roi%
OU25 236 56.78 -6.271 -12.93
BTTS 205 51.22 -8.89 -21.63
MS 76 57.89 -2.396 -14.98
DC 4 100.0 0.26 32.5
OU35 3 0.0 -0.6 -100.0
PER CALIBRATED CONFIDENCE BAND
------------------------------------------------------------------------------
band n hit% roi%
45-50 8 50.0 -20.11
50-55 5 40.0 -49.23
55-60 52 53.85 -6.36
60-65 33 48.48 -24.39
65-70 420 55.48 -16.52
70-80 6 66.67 -33.75
PER ODDS BAND
------------------------------------------------------------------------------
band n hit% roi%
1.3-1.5 241 61.41 -15.12
1.5-1.8 221 52.04 -15.63
1.8-2.2 56 41.07 -22.86
2.2-3.0 5 20.0 -44.15
3.0-5.0 1 0.0 -100.0
LEAGUE vs CUP
------------------------------------------------------------------------------
league n= 524 hit=54.77% roi=-16.73%
LOSS DIAGNOSTICS
------------------------------------------------------------------------------
total losses: 237
total lost units: -48.7
By market: [('OU25', 102), ('BTTS', 100), ('MS', 32), ('OU35', 3)]
Loss patterns (count, % of losses):
high_htft_reversal_prob (>=0.20) 0 (0.0%)
cup_match 0 (0.0%)
low_league_reliability (<0.45) 42 (17.72%)
v27_disagree 60 (25.32%)
trap_market_flagged 81 (34.18%)
low_calibrated_conf (<55) 7 (2.95%)
high_odds_underdog (>=2.5) 4 (1.69%)
low_data_quality (<0.55) 40 (16.88%)
high_risk_level 20 (8.44%)
inferred_features 0 (0.0%)
Top betting_brain issues seen in losses:
triple_value_not_confirmed 230
trap_market_market_overpriced 81
low_reliability_league 40
v25_v27_soft_disagreement 10
engine_consensus_disagree 5
historical_sample_too_low 3
very_low_reliability_league 2
htft_reversal_prob_minor=0.13 1
Top betting_brain vetoes (in losses — i.e. veto fired but bet still went through value-sniper override):
RECOMMENDATIONS
------------------------------------------------------------------------------
• [raise_confidence_threshold]
confidence_band: 65-70
evidence: n=420, roi=-16.52%
suggested_fix: Raise MIN_BET_SCORE or market_min_conf above 65
==============================================================================
@@ -0,0 +1,38 @@
{
"BTTS": {
"min_calibrated_confidence": 65,
"min_ev_edge": -1.0,
"max_ev_edge": 0.1,
"min_odds": 1.4,
"max_odds": 10.0,
"min_odds_reliability": 0.55,
"require_v27_agree": true,
"expected_n_bets": 54,
"expected_hit_pct": 55.56,
"expected_roi_pct": -10.96
},
"MS": {
"min_calibrated_confidence": 0,
"min_ev_edge": -0.05,
"max_ev_edge": 0.15,
"min_odds": 1.2,
"max_odds": 10.0,
"min_odds_reliability": 0.0,
"require_v27_agree": true,
"expected_n_bets": 21,
"expected_hit_pct": 61.9,
"expected_roi_pct": 8.23
},
"OU25": {
"min_calibrated_confidence": 0,
"min_ev_edge": -1.0,
"max_ev_edge": 0.15,
"min_odds": 1.8,
"max_odds": 10.0,
"min_odds_reliability": 0.0,
"require_v27_agree": false,
"expected_n_bets": 20,
"expected_hit_pct": 65.0,
"expected_roi_pct": 28.91
}
}
+227
View File
@@ -0,0 +1,227 @@
"""
Deep root-cause analysis on diagnostic_backtest CSV.
Tests specific hypotheses with hard numbers and proposes actionable
filter rules with estimated impact (units saved, ROI shift).
"""
import sys, os, glob
import pandas as pd
import numpy as np
REPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "reports")
def latest_csv():
files = sorted(glob.glob(os.path.join(REPORTS_DIR, "diagnostic_backtest_*.csv")),
key=os.path.getmtime, reverse=True)
return files[0] if files else None
def fmt_pct(x):
return f"{x:>6.2f}%" if pd.notna(x) else " ----"
def cell(df, label, mask):
sub = df[mask]
n = len(sub)
if n == 0:
return f" {label:<60} n=0"
wins = (sub["won"] == True).sum()
losses = (sub["won"] == False).sum()
settled = wins + losses
hr = 100.0 * wins / settled if settled else 0
profit = sub["unit_profit"].sum()
staked = sub["stake_units"].sum()
roi = 100.0 * profit / staked if staked else 0
return (f" {label:<60} n={n:>4} hit={hr:>6.2f}% "
f"profit={profit:>+7.2f}u roi={roi:>+7.2f}%")
def hypothesis_block(title, rows):
print(f"\n{'' * 78}")
print(f" {title}")
print(f"{'' * 78}")
for row in rows:
print(row)
def main():
csv_path = latest_csv()
if not csv_path:
print("No backtest CSV found")
return
print(f"Reading {csv_path}")
df = pd.read_csv(csv_path)
print(f"Loaded {len(df)} rows")
# Filter only playable + settled
pdf = df[(df["playable"] == True) & (df["won"].notna())].copy()
pdf["won"] = pdf["won"].astype(bool)
print(f"Playable + settled: {len(pdf)}")
overall_hr = (pdf["won"].sum() / len(pdf)) * 100
overall_roi = 100.0 * pdf["unit_profit"].sum() / pdf["stake_units"].sum()
print(f"\nOVERALL: hit={overall_hr:.2f}% roi={overall_roi:.2f}%")
# ─────────────────────────────────────────────────────────────────────
# H1: TRIPLE VALUE CONFIRMATION
# ─────────────────────────────────────────────────────────────────────
triple_confirmed_mask = ~pdf["bb_issues"].fillna("").str.contains(
"triple_value_not_confirmed", na=False
)
hypothesis_block(
"H1: TRIPLE VALUE CONFIRMED vs NOT CONFIRMED",
[
cell(pdf, "triple_value CONFIRMED", triple_confirmed_mask),
cell(pdf, "triple_value NOT CONFIRMED", ~triple_confirmed_mask),
]
)
# ─────────────────────────────────────────────────────────────────────
# H2: TRAP MARKET FLAG
# ─────────────────────────────────────────────────────────────────────
trap_mask = pdf["bb_trap_market"] == True
hypothesis_block(
"H2: TRAP MARKET FLAG (model says band rate < implied → market overpriced)",
[
cell(pdf, "trap_market_flag = TRUE (model warned)", trap_mask),
cell(pdf, "trap_market_flag = FALSE", ~trap_mask),
]
)
# ─────────────────────────────────────────────────────────────────────
# H3: V25/V27 CONSENSUS
# ─────────────────────────────────────────────────────────────────────
agree_mask = pdf["v27_consensus"] == "AGREE"
disagree_mask = pdf["v27_consensus"] == "DISAGREE"
hypothesis_block(
"H3: V25 ↔ V27 CONSENSUS",
[
cell(pdf, "AGREE", agree_mask),
cell(pdf, "DISAGREE", disagree_mask),
cell(pdf, "neither/null", ~(agree_mask | disagree_mask)),
]
)
# ─────────────────────────────────────────────────────────────────────
# H4: ODDS RELIABILITY (league quality)
# ─────────────────────────────────────────────────────────────────────
pdf["rel_band"] = pd.cut(
pdf["odds_reliability"].fillna(0.35),
[0, 0.30, 0.45, 0.55, 1.0],
labels=["<0.30 verylow", "0.30-0.45 low", "0.45-0.55 mid", ">=0.55 high"]
)
hypothesis_block(
"H4: LEAGUE ODDS RELIABILITY",
[cell(pdf, str(b), pdf["rel_band"] == b) for b in pdf["rel_band"].cat.categories]
)
# ─────────────────────────────────────────────────────────────────────
# H5: CALIBRATOR IMPACT (raw vs calibrated)
# ─────────────────────────────────────────────────────────────────────
pdf["calib_delta"] = pdf["calibrated_confidence"] - pdf["raw_confidence"]
pdf["delta_band"] = pd.cut(
pdf["calib_delta"].fillna(0),
[-100, -10, -3, 3, 10, 100],
labels=["cal<<raw (-10+)", "cal<raw (-3..-10)", "≈equal (±3)",
"cal>raw (3..10)", "cal>>raw (+10+)"]
)
hypothesis_block(
"H5: CALIBRATOR DELTA (calibrated_conf - raw_conf)",
[cell(pdf, str(b), pdf["delta_band"] == b) for b in pdf["delta_band"].cat.categories]
)
# ─────────────────────────────────────────────────────────────────────
# H6: EV EDGE
# ─────────────────────────────────────────────────────────────────────
pdf["edge_band"] = pd.cut(
pdf["ev_edge"].fillna(0),
[-10, -0.05, 0.0, 0.05, 0.10, 0.20, 10],
labels=["edge<-5%", "-5%-0%", "0-5%", "5-10%", "10-20%", ">20%"]
)
hypothesis_block(
"H6: EV EDGE (model_prob - implied_prob)",
[cell(pdf, str(b), pdf["edge_band"] == b) for b in pdf["edge_band"].cat.categories]
)
# ─────────────────────────────────────────────────────────────────────
# H7: ODDS x MARKET cross
# ─────────────────────────────────────────────────────────────────────
pdf["odds_band"] = pd.cut(
pdf["odds"].fillna(0),
[0, 1.30, 1.50, 1.80, 2.20, 3.00, 100],
labels=["<1.30", "1.30-1.50", "1.50-1.80", "1.80-2.20", "2.20-3.00", ">3.00"]
)
print(f"\n{'' * 78}")
print(f" H7: ODDS BAND × MARKET (per cell hit% / roi% / n)")
print(f"{'' * 78}")
pivot_n = pdf.pivot_table(index="market", columns="odds_band",
values="match_id", aggfunc="count", fill_value=0,
observed=False)
pivot_roi = pdf.pivot_table(index="market", columns="odds_band",
values="unit_profit", aggfunc="sum", fill_value=0,
observed=False)
pivot_stake = pdf.pivot_table(index="market", columns="odds_band",
values="stake_units", aggfunc="sum", fill_value=0,
observed=False)
pivot_roi_pct = (100.0 * pivot_roi / pivot_stake.replace(0, np.nan)).round(1)
print("\n Bet count per cell:")
print(pivot_n.to_string())
print("\n ROI% per cell:")
print(pivot_roi_pct.to_string())
# ─────────────────────────────────────────────────────────────────────
# H8: COMBINED FILTER SIMULATION
# ─────────────────────────────────────────────────────────────────────
print(f"\n{'' * 78}")
print(" H8: COMBINED FILTER SIMULATION (what if we add rules)")
print(f"{'' * 78}")
def simulate(filter_name, keep_mask):
kept = pdf[keep_mask]
rejected = pdf[~keep_mask]
if len(kept) == 0:
return f" {filter_name:<55} → 0 bet remain"
kept_hr = 100.0 * kept["won"].sum() / len(kept)
kept_profit = kept["unit_profit"].sum()
kept_staked = kept["stake_units"].sum()
kept_roi = 100.0 * kept_profit / kept_staked if kept_staked else 0
saved = -rejected["unit_profit"].sum() # money we WOULD HAVE LOST
return (f" {filter_name:<55} keep={len(kept):>3} hit={kept_hr:>5.1f}% "
f"roi={kept_roi:>+6.2f}% saved={saved:>+6.2f}u")
print(simulate("BASELINE (no extra filter)", pd.Series([True] * len(pdf), index=pdf.index)))
print(simulate("REJECT triple_value_not_confirmed",
~pdf["bb_issues"].fillna("").str.contains("triple_value_not_confirmed")))
print(simulate("REJECT trap_market_flag",
~(pdf["bb_trap_market"] == True)))
print(simulate("REJECT v27 DISAGREE",
pdf["v27_consensus"] != "DISAGREE"))
print(simulate("REJECT odds_reliability < 0.45",
pdf["odds_reliability"].fillna(1.0) >= 0.45))
print(simulate("REJECT odds in 1.80-2.20",
(pdf["odds"].fillna(0) < 1.80) | (pdf["odds"].fillna(0) >= 2.20)))
print(simulate("REJECT ev_edge < 0",
pdf["ev_edge"].fillna(0) >= 0))
print(simulate("REJECT ev_edge < 0.05",
pdf["ev_edge"].fillna(0) >= 0.05))
print()
print(" COMBINED rules:")
# Stack 1: drop triple_not_confirmed + trap_market + DISAGREE
s1 = (
~pdf["bb_issues"].fillna("").str.contains("triple_value_not_confirmed")
& ~(pdf["bb_trap_market"] == True)
& (pdf["v27_consensus"] != "DISAGREE")
)
print(simulate("STACK1: !triple_not_conf & !trap & !disagree", s1))
# Stack 2: + edge>=0
s2 = s1 & (pdf["ev_edge"].fillna(0) >= 0)
print(simulate("STACK2: STACK1 + edge >= 0", s2))
# Stack 3: + reliability>=0.45
s3 = s2 & (pdf["odds_reliability"].fillna(1.0) >= 0.45)
print(simulate("STACK3: STACK2 + reliability >= 0.45", s3))
# Stack 4: + odds outside 1.80-2.20
s4 = s3 & ((pdf["odds"].fillna(0) < 1.80) | (pdf["odds"].fillna(0) >= 2.20))
print(simulate("STACK4: STACK3 + odds NOT in 1.80-2.20", s4))
print(f"\n{'' * 78}")
print("DONE.")
if __name__ == "__main__":
main()
+137
View File
@@ -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()
+113
View File
@@ -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()
+112
View File
@@ -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()
+261
View File
@@ -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()
+136
View File
@@ -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())
+224
View File
@@ -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())
+134
View File
@@ -0,0 +1,134 @@
"""
Compare two diagnostic_backtest CSV outputs side-by-side.
Used to validate that a filter change actually improved ROI vs the
baseline run — and to detect overfitting (in-sample success but
out-of-sample collapse).
Usage:
python scripts/compare_backtests.py <baseline.csv> <validation.csv>
python scripts/compare_backtests.py (auto-picks 2 most recent CSVs)
"""
import sys, os, glob
import pandas as pd
from typing import Dict
REPORTS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "reports")
def load(path: str) -> pd.DataFrame:
df = pd.read_csv(path)
df["won_bool"] = df["won"].map(
{True: True, False: False, "True": True, "False": False, 1: True, 0: False}
)
return df
def stats(df: pd.DataFrame, mask=None) -> Dict:
if mask is not None:
df = df[mask]
playable = df[(df["playable"] == True) & (df["won_bool"].notna())]
if len(playable) == 0:
return {"n_total": len(df), "n_playable": 0, "hit": 0, "profit": 0,
"staked": 0, "roi": 0}
wins = playable["won_bool"].sum()
profit = playable["unit_profit"].sum()
staked = playable["stake_units"].sum()
return {
"n_total": int(len(df)),
"n_playable": int(len(playable)),
"wins": int(wins),
"losses": int(len(playable) - wins),
"hit": round(100.0 * wins / len(playable), 2),
"profit": round(profit, 2),
"staked": round(staked, 2),
"roi": round(100.0 * profit / staked, 2) if staked else 0,
}
def line(label: str, a: Dict, b: Dict, suffix: str = ""):
fields = ["n_total", "n_playable", "hit", "profit", "staked", "roi"]
parts = [f"{label:<28}"]
for f in fields:
va = a.get(f, "-")
vb = b.get(f, "-")
parts.append(f"{f}: {str(va):>8}{str(vb):>8}")
print(" " + " | ".join(parts) + suffix)
def main():
if len(sys.argv) == 3:
a_path, b_path = sys.argv[1], sys.argv[2]
else:
files = sorted(glob.glob(os.path.join(REPORTS_DIR, "diagnostic_backtest_*.csv")),
key=os.path.getmtime, reverse=True)
if len(files) < 2:
print("Need at least 2 backtest CSVs in reports/. Pass paths manually.")
return
b_path, a_path = files[0], files[1] # newest first as "validation"
print(f"Baseline A: {os.path.basename(a_path)}")
print(f"Validation B: {os.path.basename(b_path)}")
a = load(a_path)
b = load(b_path)
print(f"\n{'=' * 100}")
print(f" OVERALL")
print(f"{'=' * 100}")
line("ALL", stats(a), stats(b))
print(f"\n{'' * 100}")
print(f" PER MARKET")
print(f"{'' * 100}")
markets = sorted(set(a["market"].dropna().unique()) | set(b["market"].dropna().unique()))
for m in markets:
line(f"market={m}",
stats(a, a["market"] == m),
stats(b, b["market"] == m))
# New veto family check — did MUTED_MARKETS actually mute?
print(f"\n{'' * 100}")
print(f" NEW VETO IMPACT (look for new veto names in betting_brain.vetoes)")
print(f"{'' * 100}")
new_vetoes = ["market_muted_by_backtest", "negative_ev_edge", "ev_edge_too_high_trap",
"outside_envelope_edge_low", "outside_envelope_edge_high",
"outside_envelope_odds_low", "outside_envelope_v27_must_agree"]
for veto in new_vetoes:
a_hits = a["bb_vetoes"].fillna("").str.contains(veto).sum()
b_hits = b["bb_vetoes"].fillna("").str.contains(veto).sum()
print(f" {veto:<45} A={a_hits:>4} B={b_hits:>4}")
# Top issue tags
print(f"\n{'' * 100}")
print(f" BTTS MUTE CHECK — should be ~0 playable in validation")
print(f"{'' * 100}")
a_btts_play = ((a["market"] == "BTTS") & (a["playable"] == True)).sum()
b_btts_play = ((b["market"] == "BTTS") & (b["playable"] == True)).sum()
print(f" BTTS playable bets: A={a_btts_play} → B={b_btts_play} "
f"(should be 0 in B if MUTE works)")
# Verdict
print(f"\n{'=' * 100}")
a_s = stats(a)
b_s = stats(b)
roi_delta = b_s["roi"] - a_s["roi"]
if b_s["n_playable"] < 20:
verdict = "TOO FEW BETS — sample insufficient"
elif roi_delta > 5 and b_s["roi"] > 0:
verdict = "✅ FILTERS WORK — ROI improved AND positive"
elif roi_delta > 5:
verdict = "🟡 PARTIAL — ROI improved but still negative"
elif roi_delta > 0:
verdict = "🟡 SLIGHT IMPROVEMENT"
elif roi_delta < -5:
verdict = "❌ OVERFITTING — validation ROI collapsed"
else:
verdict = "❌ NO MATERIAL CHANGE"
print(f" VERDICT: {verdict}")
print(f" ROI: {a_s['roi']}% → {b_s['roi']}% (Δ {roi_delta:+.2f}pp)")
print(f"{'=' * 100}")
if __name__ == "__main__":
main()
+738
View File
@@ -0,0 +1,738 @@
"""
Diagnostic Backtest
===================
Run the full V28 orchestrator (in-process — no HTTP) on a window of completed
matches, capture the recommendation + key signal features + the actual outcome,
and produce a *diagnostic* report: not just "what was the hit rate" but
"which feature clusters drive the losing bets".
Outputs:
- reports/diagnostic_backtest_YYYYMMDD.csv (per-bet detail)
- reports/diagnostic_backtest_YYYYMMDD.json (aggregate metrics)
- reports/diagnostic_backtest_YYYYMMDD.txt (human-readable summary)
Usage:
python scripts/diagnostic_backtest.py --days 14 --max-matches 2000
python scripts/diagnostic_backtest.py --start 2026-05-10 --end 2026-05-24
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import traceback
from collections import defaultdict, Counter
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
import psycopg2
from psycopg2.extras import RealDictCursor
# Path bootstrap so we can import the ai-engine package from anywhere
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
sys.path.insert(0, AI_ENGINE_DIR)
from data.db import get_clean_dsn
from services.single_match_orchestrator import get_single_match_orchestrator
REPORTS_DIR = os.path.join(AI_ENGINE_DIR, "reports")
os.makedirs(REPORTS_DIR, exist_ok=True)
# Days with confirmed feeder gaps — exclude from sample
EXCLUDED_DATES = {"2026-05-03", "2026-04-29"}
# ── Outcome resolution ────────────────────────────────────────────────
def _norm_pick(pick: Optional[str]) -> str:
return str(pick or "").strip().casefold()
def resolve_outcome(market: str, pick: str, sh: int, sa: int,
htsh: Optional[int], htsa: Optional[int]) -> Optional[bool]:
"""Mirror of prediction-settlement.market-resolver.ts (TS side).
Returns True/False on settle, None if cannot resolve."""
m = (market or "").upper().replace(" ", "").replace("-", "_")
p = _norm_pick(pick)
if m in ("MS", "ML", "1X2"):
outcome = "1" if sh > sa else "2" if sa > sh else "x"
return p in {outcome, outcome.upper(), outcome.lower(), "0" if outcome == "x" else outcome}
if m in ("HT", "IY"):
if htsh is None or htsa is None:
return None
outcome = "1" if htsh > htsa else "2" if htsa > htsh else "x"
return p in {outcome, "0" if outcome == "x" else outcome}
if m in ("OU05", "OU15", "OU25", "OU35", "OU45", "TOTAL"):
line = {"OU05": 0.5, "OU15": 1.5, "OU25": 2.5, "OU35": 3.5,
"OU45": 4.5, "TOTAL": 2.5}[m]
total = sh + sa
if total == line:
return None
is_over = total > line
if "over" in p or "üst" in p or "ust" in p:
return is_over
if "alt" in p or "under" in p:
return not is_over
return None
if m in ("OU05_HT", "OU15_HT", "OU25_HT", "HT_OU05", "HT_OU15", "HT_OU25"):
if htsh is None or htsa is None:
return None
line = {"OU05_HT": 0.5, "OU15_HT": 1.5, "OU25_HT": 2.5,
"HT_OU05": 0.5, "HT_OU15": 1.5, "HT_OU25": 2.5}[m]
total = htsh + htsa
if total == line:
return None
is_over = total > line
if "over" in p or "üst" in p or "ust" in p:
return is_over
if "alt" in p or "under" in p:
return not is_over
return None
if m in ("BTTS", "KG"):
both = sh > 0 and sa > 0
if "yes" in p or "var" in p:
return both
if "no" in p or "yok" in p:
return not both
return None
if m in ("HTFT", "IYMS"):
if htsh is None or htsa is None or "/" not in p:
return None
ht_p, ft_p = p.split("/", 1)
ht_actual = "1" if htsh > htsa else "2" if htsa > htsh else "x"
ft_actual = "1" if sh > sa else "2" if sa > sh else "x"
return ht_p.strip() == ht_actual and ft_p.strip() == ft_actual
if m in ("DC", "CIFTE_SANS"):
ft = "1" if sh > sa else "2" if sa > sh else "X"
raw = p.upper().replace("-", "").replace("/", "")
if raw in ("1X", "X1"):
pair = ["1", "X"]
elif raw in ("X2", "2X"):
pair = ["X", "2"]
elif raw in ("12", "21"):
pair = ["1", "2"]
else:
return None
return ft in pair
if m in ("OE", "TEKCIFT"):
is_odd = (sh + sa) % 2 == 1
if "tek" in p or "odd" in p:
return is_odd
if "cift" in p or "çift" in p or "even" in p:
return not is_odd
return None
return None
def compute_unit_profit(won: Optional[bool], stake: float, odds: Optional[float]) -> float:
if won is None:
return 0.0
if not won:
return -abs(stake) if stake else -1.0
if not odds or odds <= 1.0:
return 0.0
return round(stake * (odds - 1.0), 4)
# ── Data fetch ────────────────────────────────────────────────────────
def fetch_match_window(args) -> List[Dict]:
dsn = get_clean_dsn()
if "?schema=" in dsn:
dsn = dsn.split("?schema=")[0]
if args.start and args.end:
start = datetime.strptime(args.start, "%Y-%m-%d")
end = datetime.strptime(args.end, "%Y-%m-%d") + timedelta(days=1)
else:
end = datetime.now(timezone.utc).replace(tzinfo=None)
start = end - timedelta(days=args.days)
start_ms = int(start.timestamp() * 1000)
end_ms = int(end.timestamp() * 1000)
excluded = sorted(EXCLUDED_DATES)
excluded_clause = ""
if excluded:
ex_csv = ",".join(f"'{d}'" for d in excluded)
excluded_clause = (
f" AND to_timestamp(mst_utc/1000)::date "
f"NOT IN ({ex_csv})"
)
with psycopg2.connect(dsn) as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(
f"""
SELECT id AS match_id,
score_home, score_away,
ht_score_home, ht_score_away,
league_id,
to_timestamp(mst_utc/1000)::date AS match_date
FROM matches
WHERE sport='football'
AND status='FT'
AND score_home IS NOT NULL
AND score_away IS NOT NULL
AND mst_utc >= %s
AND mst_utc < %s
{excluded_clause}
ORDER BY mst_utc DESC
LIMIT %s
""",
(start_ms, end_ms, args.max_matches),
)
return cur.fetchall()
# ── Per-bet capture ───────────────────────────────────────────────────
def capture_bet_row(match: Dict, package: Dict) -> Dict[str, Any]:
"""Distill orchestrator response + ground truth into one analytic row."""
main = package.get("main_pick") or {}
bb = main.get("betting_brain") or {}
advice = package.get("bet_advice") or {}
v27 = package.get("v27_engine") or {}
triple = (v27.get("triple_value") or {})
risk = package.get("risk") or {}
quality = package.get("data_quality") or {}
htft_payload = ((package.get("market_board") or {}).get("HTFT") or {})
htft_probs = htft_payload.get("probs") or {}
sh, sa = match["score_home"], match["score_away"]
htsh, htsa = match["ht_score_home"], match["ht_score_away"]
market = main.get("market")
pick = main.get("pick")
odds_val = _f(main.get("odds"))
stake = _f(main.get("stake_units"), 1.0)
playable = bool(main.get("playable")) and bool(advice.get("playable"))
won = resolve_outcome(market, pick, sh, sa, htsh, htsa) if market and pick else None
profit = compute_unit_profit(won, stake, odds_val) if playable else 0.0
# Reversal context (only meaningful for MS picks)
rev_prob = None
if market == "MS" and pick in ("1", "2"):
if pick == "1":
rev_prob = _f(htft_probs.get("1/2"), 0.0) + _f(htft_probs.get("1/X"), 0.0)
else:
rev_prob = _f(htft_probs.get("2/1"), 0.0) + _f(htft_probs.get("2/X"), 0.0)
return {
"match_id": match["match_id"],
"match_date": str(match["match_date"]),
"league_id": match.get("league_id"),
"score_home": sh,
"score_away": sa,
"ht_score_home": htsh,
"ht_score_away": htsa,
"market": market,
"pick": pick,
"odds": odds_val,
"stake_units": stake,
"playable": playable,
"won": won,
"unit_profit": profit,
"raw_confidence": _f(main.get("raw_confidence")),
"calibrated_confidence": _f(main.get("calibrated_confidence")),
"play_score": _f(main.get("play_score")),
"ev_edge": _f(main.get("ev_edge")),
"bet_grade": main.get("bet_grade"),
"is_value_sniper": bool(main.get("is_value_sniper")),
"bb_score": _f(bb.get("score")),
"bb_action": bb.get("action"),
"bb_vetoes": ";".join(bb.get("vetoes") or []),
"bb_issues": ";".join(bb.get("issues") or []),
"bb_positives": ";".join(bb.get("positives") or []),
"bb_model_prob": _f(bb.get("model_prob")),
"bb_implied_prob": _f(bb.get("implied_prob")),
"bb_model_market_gap": _f(bb.get("model_market_gap")),
"bb_divergence": _f(bb.get("divergence")),
"bb_trap_market": bool(bb.get("trap_market_flag")),
"v27_consensus": v27.get("consensus"),
"data_quality_score": _f(quality.get("score")),
"data_quality_flags": ";".join(quality.get("flags") or []),
"risk_level": (risk.get("level") if isinstance(risk, dict) else None),
"odds_reliability": _f(main.get("odds_reliability")),
"htft_reversal_prob": rev_prob,
"htft_top_pick": _argmax(htft_probs),
"league_name": (package.get("match_info") or {}).get("league_name"),
"is_cup": _is_cup((package.get("match_info") or {}).get("league_name") or ""),
"model_version": package.get("model_version"),
"decision_reason": main.get("pick_reason") or advice.get("reason"),
}
def _f(x: Any, default: Optional[float] = None) -> Optional[float]:
try:
return float(x) if x is not None else default
except (TypeError, ValueError):
return default
def _argmax(d: Dict[str, Any]) -> Optional[str]:
best, val = None, -1.0
for k, v in d.items():
fv = _f(v, 0.0) or 0.0
if fv > val:
best, val = k, fv
return best
_CUP_KEYWORDS = ("kupa", "cup", "coupe", "copa", "coppa", "pokal", "trophy",
"shield", "ziraat", "süper kupa", "super cup", "beker", "taça", "taca")
def _is_cup(name: str) -> bool:
n = (name or "").lower()
return any(kw in n for kw in _CUP_KEYWORDS)
# ── Aggregation helpers ────────────────────────────────────────────────
def _bucket(value: Optional[float], edges: List[float]) -> Optional[str]:
if value is None:
return None
for i, edge in enumerate(edges):
if value < edge:
if i == 0:
return f"<{edge}"
return f"{edges[i-1]}-{edge}"
return f">={edges[-1]}"
def _summary_stats(rows: List[Dict]) -> Dict[str, Any]:
if not rows:
return {"n": 0}
settled = [r for r in rows if r["playable"] and r["won"] is not None]
won = sum(1 for r in settled if r["won"])
lost = sum(1 for r in settled if not r["won"])
profit = sum(float(r["unit_profit"]) for r in settled)
staked = sum(float(r["stake_units"]) for r in settled)
return {
"n_total": len(rows),
"n_playable_settled": len(settled),
"wins": won,
"losses": lost,
"hit_rate_pct": round(100.0 * won / len(settled), 2) if settled else None,
"unit_profit": round(profit, 3),
"staked": round(staked, 3),
"roi_pct": round(100.0 * profit / staked, 2) if staked else None,
}
def aggregate(rows: List[Dict]) -> Dict[str, Any]:
out: Dict[str, Any] = {"overall": _summary_stats(rows)}
by = lambda key_fn: defaultdict(list)
market_buckets = by(None)
conf_buckets = by(None)
odds_buckets = by(None)
grade_buckets = by(None)
cup_buckets = by(None)
motivation_buckets = by(None)
for r in rows:
if r["playable"]:
market_buckets[r["market"] or "?"].append(r)
conf_buckets[_bucket(r["calibrated_confidence"],
[45, 50, 55, 60, 65, 70, 80])].append(r)
odds_buckets[_bucket(r["odds"], [1.3, 1.5, 1.8, 2.2, 3.0, 5.0])].append(r)
grade_buckets[r["bet_grade"] or "?"].append(r)
cup_buckets["cup" if r["is_cup"] else "league"].append(r)
out["by_market"] = {k: _summary_stats(v) for k, v in market_buckets.items()}
out["by_confidence"] = {k: _summary_stats(v) for k, v in conf_buckets.items() if k}
out["by_odds"] = {k: _summary_stats(v) for k, v in odds_buckets.items() if k}
out["by_grade"] = {k: _summary_stats(v) for k, v in grade_buckets.items()}
out["by_competition"] = {k: _summary_stats(v) for k, v in cup_buckets.items()}
return out
def loss_diagnostics(rows: List[Dict]) -> Dict[str, Any]:
losses = [r for r in rows if r["playable"] and r["won"] is False]
if not losses:
return {"n_losses": 0}
n = len(losses)
def share(predicate) -> Tuple[int, float]:
c = sum(1 for r in losses if predicate(r))
return c, round(100.0 * c / n, 2)
diagnostics = {
"n_losses": n,
"total_loss_units": round(sum(float(r["unit_profit"]) for r in losses), 3),
"patterns": {
"high_htft_reversal_prob (>=0.20)": share(
lambda r: (r.get("htft_reversal_prob") or 0) >= 0.20
),
"cup_match": share(lambda r: r["is_cup"]),
"low_league_reliability (<0.45)": share(
lambda r: (r.get("odds_reliability") or 1) < 0.45
),
"v27_disagree": share(lambda r: r.get("v27_consensus") == "DISAGREE"),
"trap_market_flagged": share(lambda r: r.get("bb_trap_market")),
"low_calibrated_conf (<55)": share(
lambda r: (r.get("calibrated_confidence") or 0) < 55
),
"high_odds_underdog (>=2.5)": share(
lambda r: (r.get("odds") or 0) >= 2.5
),
"low_data_quality (<0.55)": share(
lambda r: (r.get("data_quality_score") or 1) < 0.55
),
"high_risk_level": share(
lambda r: r.get("risk_level") in ("HIGH", "EXTREME")
),
"inferred_features": share(
lambda r: "ai_features_inferred_from_history" in (r.get("data_quality_flags") or "")
),
},
"by_market": Counter(r["market"] for r in losses).most_common(),
"by_league": Counter(r.get("league_name") for r in losses).most_common(10),
}
# Top issue tags from betting_brain across losses
issue_counter = Counter()
veto_counter = Counter()
for r in losses:
for tag in (r.get("bb_issues") or "").split(";"):
if tag:
issue_counter[tag] += 1
for tag in (r.get("bb_vetoes") or "").split(";"):
if tag:
veto_counter[tag] += 1
diagnostics["top_bb_issues_in_losses"] = issue_counter.most_common(15)
diagnostics["top_bb_vetoes_in_losses"] = veto_counter.most_common(15)
return diagnostics
# ── Recommendations ────────────────────────────────────────────────────
def make_recommendations(rows: List[Dict], agg: Dict[str, Any],
diag: Dict[str, Any]) -> List[Dict[str, Any]]:
recs: List[Dict[str, Any]] = []
overall = agg.get("overall") or {}
if not overall.get("n_playable_settled"):
return recs
# Cross-reference market hit rate vs overall — flag chronic losers.
overall_hit = overall.get("hit_rate_pct") or 0.0
for market, stats in (agg.get("by_market") or {}).items():
n = stats.get("n_playable_settled") or 0
hit = stats.get("hit_rate_pct")
roi = stats.get("roi_pct")
if n < 30:
continue
if hit is not None and roi is not None and roi < -10 and hit < overall_hit - 10:
recs.append({
"type": "drop_market",
"market": market,
"evidence": f"hit={hit}%, roi={roi}%, n={n} — chronic loser",
"suggested_fix": f"Add veto in betting_brain when market=={market} unless overwhelming evidence",
"estimated_loss_prevented_units": round(-(stats.get("unit_profit") or 0), 2),
})
# Confidence band tuning — flag bands where ROI < 0 despite passing eşik
for band, stats in (agg.get("by_confidence") or {}).items():
n = stats.get("n_playable_settled") or 0
roi = stats.get("roi_pct")
if n >= 40 and roi is not None and roi < -8:
recs.append({
"type": "raise_confidence_threshold",
"confidence_band": band,
"evidence": f"n={n}, roi={roi}%",
"suggested_fix": f"Raise MIN_BET_SCORE or market_min_conf above {band.split('-')[0]}",
})
# Loss diagnostic — if cup matches dominate losses, recommend cup-aware filter
patterns = (diag.get("patterns") or {})
cup_share = patterns.get("cup_match", (0, 0))[1]
if cup_share >= 25:
recs.append({
"type": "cup_match_filter",
"evidence": f"{cup_share}% of losses are cup matches",
"suggested_fix": "Tighten betting_brain thresholds for is_cup_match=True picks",
})
rev_share = patterns.get("high_htft_reversal_prob (>=0.20)", (0, 0))[1]
if rev_share >= 15:
recs.append({
"type": "tighten_reversal_check",
"evidence": f"{rev_share}% of losses had HTFT reversal prob >=0.20 (already partial fix)",
"suggested_fix": "Lower reversal threshold in betting_brain from 0.25 to 0.20 for veto trigger",
})
rel_share = patterns.get("low_league_reliability (<0.45)", (0, 0))[1]
if rel_share >= 20:
recs.append({
"type": "league_reliability_filter",
"evidence": f"{rel_share}% of losses in low-reliability leagues (<0.45)",
"suggested_fix": "Add hard veto when odds_reliability<0.45 for non-value-sniper picks",
})
return recs
# ── CSV / report writers ───────────────────────────────────────────────
def write_csv(rows: List[Dict], path: str):
if not rows:
return
import csv
fields = list(rows[0].keys())
with open(path, "w", newline="", encoding="utf-8") as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for r in rows:
w.writerow(r)
def write_text_summary(rows: List[Dict], agg: Dict, diag: Dict,
recs: List[Dict], path: str, args):
lines: List[str] = []
push = lines.append
push("=" * 78)
push("DIAGNOSTIC BACKTEST REPORT")
push("=" * 78)
push(f"Generated: {datetime.now().isoformat(timespec='seconds')}")
push(f"Sample window: start={args.start or f'-{args.days}d'}, end={args.end or 'now'}")
push(f"Max matches: {args.max_matches}")
push(f"Excluded days: {sorted(EXCLUDED_DATES)}")
push("")
push("OVERALL")
push("-" * 78)
overall = agg.get("overall") or {}
for k in ("n_total", "n_playable_settled", "wins", "losses",
"hit_rate_pct", "unit_profit", "staked", "roi_pct"):
push(f" {k:25}: {overall.get(k)}")
push("")
push("PER MARKET")
push("-" * 78)
push(f" {'market':<8} {'n':>6} {'hit%':>7} {'profit':>9} {'roi%':>7}")
for market, s in sorted((agg.get("by_market") or {}).items(),
key=lambda kv: -(kv[1].get("n_playable_settled") or 0)):
push(f" {market:<8} {s.get('n_playable_settled',0):>6} "
f"{str(s.get('hit_rate_pct','')):>7} "
f"{str(s.get('unit_profit','')):>9} "
f"{str(s.get('roi_pct','')):>7}")
push("")
push("PER CALIBRATED CONFIDENCE BAND")
push("-" * 78)
push(f" {'band':<10} {'n':>6} {'hit%':>7} {'roi%':>7}")
for band, s in sorted((agg.get("by_confidence") or {}).items()):
push(f" {band:<10} {s.get('n_playable_settled',0):>6} "
f"{str(s.get('hit_rate_pct','')):>7} "
f"{str(s.get('roi_pct','')):>7}")
push("")
push("PER ODDS BAND")
push("-" * 78)
push(f" {'band':<10} {'n':>6} {'hit%':>7} {'roi%':>7}")
for band, s in sorted((agg.get("by_odds") or {}).items()):
push(f" {band:<10} {s.get('n_playable_settled',0):>6} "
f"{str(s.get('hit_rate_pct','')):>7} "
f"{str(s.get('roi_pct','')):>7}")
push("")
push("LEAGUE vs CUP")
push("-" * 78)
for k, s in (agg.get("by_competition") or {}).items():
push(f" {k:<8} n={s.get('n_playable_settled',0):>4} "
f"hit={s.get('hit_rate_pct','-')}% roi={s.get('roi_pct','-')}%")
push("")
push("LOSS DIAGNOSTICS")
push("-" * 78)
push(f" total losses: {diag.get('n_losses')}")
push(f" total lost units: {diag.get('total_loss_units')}")
push(f" By market: {diag.get('by_market')}")
push(" Loss patterns (count, % of losses):")
for pattern, (c, pct) in (diag.get("patterns") or {}).items():
push(f" {pattern:<55} {c:>4} ({pct}%)")
push(" Top betting_brain issues seen in losses:")
for issue, c in (diag.get("top_bb_issues_in_losses") or []):
push(f" {issue:<55} {c}")
push(" Top betting_brain vetoes (in losses — i.e. veto fired but bet still went through value-sniper override):")
for veto, c in (diag.get("top_bb_vetoes_in_losses") or []):
push(f" {veto:<55} {c}")
push("")
push("RECOMMENDATIONS")
push("-" * 78)
if not recs:
push(" (none surfaced — sample too small or no clear pattern)")
for r in recs:
push(f" • [{r['type']}]")
for k, v in r.items():
if k == "type":
continue
push(f" {k}: {v}")
push("")
push("=" * 78)
with open(path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
# ── Main loop ─────────────────────────────────────────────────────────
def _checkpoint_paths(args) -> Tuple[str, str]:
"""Stable checkpoint paths derived from the run's date window so a
re-run with the same args picks up the same checkpoint."""
key = f"{args.start or 'd' + str(args.days)}_{args.end or 'now'}_{args.max_matches}"
key = key.replace("-", "").replace(":", "")
ckpt_csv = os.path.join(REPORTS_DIR, f"_checkpoint_{key}.csv")
ckpt_state = os.path.join(REPORTS_DIR, f"_checkpoint_{key}.state")
return ckpt_csv, ckpt_state
def _load_checkpoint(args) -> Tuple[List[Dict], set]:
"""Read partial CSV + processed-IDs set if a previous run was interrupted."""
ckpt_csv, _ = _checkpoint_paths(args)
if not os.path.exists(ckpt_csv):
return [], set()
import csv
rows: List[Dict] = []
seen: set = set()
try:
with open(ckpt_csv, "r", encoding="utf-8", newline="") as f:
reader = csv.DictReader(f)
for row in reader:
rows.append(row)
seen.add(str(row.get("match_id") or ""))
except Exception as e:
print(f" checkpoint read failed ({e}); starting fresh")
return [], set()
return rows, seen
def _flush_checkpoint(args, rows: List[Dict]) -> None:
"""Atomic-ish overwrite of the partial CSV. Cheap enough at every 100 rows."""
if not rows:
return
ckpt_csv, _ = _checkpoint_paths(args)
import csv
tmp = ckpt_csv + ".tmp"
fields = list(rows[0].keys())
with open(tmp, "w", encoding="utf-8", newline="") as f:
w = csv.DictWriter(f, fieldnames=fields)
w.writeheader()
for r in rows:
w.writerow(r)
os.replace(tmp, ckpt_csv)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--days", type=int, default=14,
help="Backwards window from now (default 14)")
parser.add_argument("--max-matches", type=int, default=2000,
help="Hard cap on matches processed (default 2000)")
parser.add_argument("--start", help="Start date YYYY-MM-DD (overrides --days)")
parser.add_argument("--end", help="End date YYYY-MM-DD")
parser.add_argument("--progress-interval", type=int, default=50)
parser.add_argument("--checkpoint-every", type=int, default=100,
help="Flush partial CSV every N matches (default 100)")
parser.add_argument("--no-resume", action="store_true",
help="Ignore any prior checkpoint and start fresh")
args = parser.parse_args()
print("=" * 70)
print("DIAGNOSTIC BACKTEST")
print("=" * 70)
print(f"Loading orchestrator...")
orch = get_single_match_orchestrator()
# Warm V25 + V27 + basketball loaders so the first match doesn't pay it
try:
orch._get_v25_predictor()
except Exception as e:
print(f" v25 warmup: {e}")
try:
orch._get_v27_predictor()
except Exception as e:
print(f" v27 warmup: {e}")
print(f"Fetching match window...")
matches = fetch_match_window(args)
n = len(matches)
print(f" {n} matches selected")
if not matches:
print("No matches to process. Exiting.")
return
# ── Resume from prior checkpoint if available ──
rows: List[Dict[str, Any]] = []
seen_ids: set = set()
if not args.no_resume:
rows, seen_ids = _load_checkpoint(args)
if rows:
print(f" Resuming from checkpoint: {len(rows)} matches already done")
errors: List[Tuple[str, str]] = []
t0 = time.time()
for i, m in enumerate(matches, start=1):
mid = str(m["match_id"])
if mid in seen_ids:
continue
try:
pkg = orch.analyze_match(mid)
if pkg is None:
continue
row = capture_bet_row(m, pkg)
rows.append(row)
except KeyboardInterrupt:
print("\nInterrupted, flushing checkpoint...")
_flush_checkpoint(args, rows)
break
except Exception as e:
errors.append((mid, str(e)))
if len(errors) <= 5:
traceback.print_exc()
# ── Periodic checkpoint flush so a crash doesn't lose everything ──
if i % args.checkpoint_every == 0:
_flush_checkpoint(args, rows)
if i % args.progress_interval == 0:
elapsed = time.time() - t0
rate = i / elapsed
eta = (n - i) / rate if rate else 0
playable_so_far = sum(1 for r in rows if r["playable"])
print(f" [{i}/{n}] rate={rate:.1f}/s eta={eta/60:.1f}min "
f"playable={playable_so_far} errors={len(errors)} "
f"(checkpoint at every {args.checkpoint_every})")
print(f"\nProcessed {len(rows)} rows in {(time.time()-t0):.1f}s "
f"({len(errors)} errors)")
# Aggregate
print("Aggregating...")
agg = aggregate(rows)
diag = loss_diagnostics(rows)
recs = make_recommendations(rows, agg, diag)
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
csv_path = os.path.join(REPORTS_DIR, f"diagnostic_backtest_{stamp}.csv")
json_path = os.path.join(REPORTS_DIR, f"diagnostic_backtest_{stamp}.json")
txt_path = os.path.join(REPORTS_DIR, f"diagnostic_backtest_{stamp}.txt")
write_csv(rows, csv_path)
with open(json_path, "w", encoding="utf-8") as f:
json.dump({"args": vars(args), "aggregate": agg, "loss_diagnostics": diag,
"recommendations": recs, "errors_sample": errors[:20]},
f, indent=2, default=str)
write_text_summary(rows, agg, diag, recs, txt_path, args)
print(f"\nOutputs:")
print(f" CSV: {csv_path}")
print(f" JSON: {json_path}")
print(f" TXT: {txt_path}")
print("\nOverall:", agg.get("overall"))
if __name__ == "__main__":
main()
+181
View File
@@ -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()
+255
View File
@@ -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()
+154
View File
@@ -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()
+253
View File
@@ -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()
+162
View File
@@ -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()
+151
View File
@@ -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()
+134
View File
@@ -0,0 +1,134 @@
"""
Odds Movement Monitor openingclosing 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 openingclosing 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()
+182
View File
@@ -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()
+254
View File
@@ -0,0 +1,254 @@
"""
Filter Optimizer
================
Grid-search over filter thresholds (per market) using the existing
diagnostic_backtest CSV. Finds the (confidence, edge, odds, reliability)
combination that maximizes ROI while keeping bet volume reasonable.
No re-prediction needed pure offline simulation on the bets already
captured. Output: per-market optimal thresholds + projected ROI lift +
JSON patch ready to drop into config/market_thresholds.json.
Usage:
python scripts/optimize_filters.py
python scripts/optimize_filters.py --csv reports/diagnostic_backtest_X.csv
python scripts/optimize_filters.py --min-bets 20 --apply
"""
import argparse
import json
import os
import sys
import glob
import itertools
from typing import List, Dict, Tuple, Optional
import pandas as pd
import numpy as np
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
AI_ENGINE_DIR = os.path.dirname(SCRIPT_DIR)
sys.path.insert(0, AI_ENGINE_DIR)
REPORTS_DIR = os.path.join(AI_ENGINE_DIR, "reports")
CONFIG_PATH = os.path.join(AI_ENGINE_DIR, "config", "market_thresholds.json")
def latest_csv() -> Optional[str]:
files = sorted(glob.glob(os.path.join(REPORTS_DIR, "diagnostic_backtest_*.csv")),
key=os.path.getmtime, reverse=True)
return files[0] if files else None
def load_backtest(path: str) -> pd.DataFrame:
df = pd.read_csv(path)
# Keep only playable + settled bets — these are what the SYSTEM
# actually placed and got an outcome on.
pdf = df[(df["playable"] == True) & (df["won"].notna())].copy()
pdf["won"] = pdf["won"].astype(bool)
pdf["calibrated_confidence"] = pdf["calibrated_confidence"].fillna(0)
pdf["ev_edge"] = pdf["ev_edge"].fillna(0)
pdf["odds"] = pdf["odds"].fillna(0)
pdf["odds_reliability"] = pdf["odds_reliability"].fillna(0.35)
return pdf
def evaluate(pdf: pd.DataFrame, mask) -> Dict:
kept = pdf[mask]
if len(kept) == 0:
return {"n": 0, "hit_pct": 0, "profit": 0, "staked": 0, "roi_pct": 0}
wins = kept["won"].sum()
profit = kept["unit_profit"].sum()
staked = kept["stake_units"].sum()
return {
"n": int(len(kept)),
"hit_pct": round(100.0 * wins / len(kept), 2),
"profit": round(profit, 3),
"staked": round(staked, 3),
"roi_pct": round(100.0 * profit / staked, 2) if staked else 0,
}
def grid_search_market(
market_df: pd.DataFrame,
market: str,
min_bets: int = 15,
) -> List[Dict]:
"""Try a wide grid of (min_conf, min_edge, max_edge, min_odds, max_odds,
min_reliability) combinations. Return all candidates with n >= min_bets,
sorted by ROI descending."""
conf_options = [0, 45, 50, 55, 60, 65, 70]
min_edge_options = [-1.0, -0.05, 0.0, 0.03, 0.05, 0.08]
max_edge_options = [10.0, 0.30, 0.20, 0.15, 0.10]
min_odds_options = [1.20, 1.30, 1.40, 1.50, 1.60, 1.80]
max_odds_options = [10.0, 3.0, 2.5, 2.2, 2.0]
rel_options = [0.0, 0.30, 0.45, 0.55]
consensus_options = ["any", "agree_or_null"]
candidates: List[Dict] = []
for mc, mine, maxe, mino, maxo, mrel, cons in itertools.product(
conf_options, min_edge_options, max_edge_options,
min_odds_options, max_odds_options, rel_options, consensus_options,
):
if mine >= maxe or mino >= maxo:
continue
mask = (
(market_df["calibrated_confidence"] >= mc)
& (market_df["ev_edge"] >= mine)
& (market_df["ev_edge"] <= maxe)
& (market_df["odds"] >= mino)
& (market_df["odds"] <= maxo)
& (market_df["odds_reliability"] >= mrel)
)
if cons == "agree_or_null":
mask &= market_df["v27_consensus"] != "DISAGREE"
result = evaluate(market_df, mask)
if result["n"] >= min_bets:
candidates.append({
"market": market,
"min_conf": mc,
"min_edge": mine,
"max_edge": maxe,
"min_odds": mino,
"max_odds": maxo,
"min_reliability": mrel,
"consensus": cons,
**result,
})
candidates.sort(key=lambda r: (r["roi_pct"], r["n"]), reverse=True)
return candidates
def baseline(pdf: pd.DataFrame, market: str) -> Dict:
m = pdf[pdf["market"] == market]
return evaluate(m, pd.Series([True] * len(m), index=m.index))
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--csv", default=None, help="Override CSV path")
parser.add_argument("--min-bets", type=int, default=15,
help="Min bet count to consider a config valid")
parser.add_argument("--top-k", type=int, default=3,
help="Show top K configs per market")
parser.add_argument("--apply", action="store_true",
help="Patch config/market_thresholds.json with winners")
args = parser.parse_args()
csv_path = args.csv or latest_csv()
if not csv_path or not os.path.exists(csv_path):
print("No backtest CSV found.")
return
print(f"Loading: {csv_path}")
pdf = load_backtest(csv_path)
print(f"Playable + settled bets: {len(pdf)}")
markets = sorted(pdf["market"].dropna().unique())
print(f"Markets: {markets}\n")
all_winners: Dict[str, Dict] = {}
for market in markets:
market_df = pdf[pdf["market"] == market]
n_total = len(market_df)
base = baseline(pdf, market)
print(f"\n{'=' * 78}")
print(f"MARKET: {market} (n={n_total} baseline_roi={base['roi_pct']}%)")
print(f"{'=' * 78}")
if n_total < args.min_bets * 2:
print(f" Sample too small to grid-search reliably (n={n_total}). Skip.")
continue
candidates = grid_search_market(market_df, market, args.min_bets)
if not candidates:
print(f" No config kept >= {args.min_bets} bets. Skip.")
continue
# Pareto-ish: show top-K by ROI but also one that keeps higher bet count
winners = candidates[:args.top_k]
keep_high_volume = None
for c in candidates:
if c["n"] >= max(40, n_total // 3) and c["roi_pct"] > base["roi_pct"]:
keep_high_volume = c
break
print(f" {'rank':<5}{'n':>5}{'hit%':>7}{'roi%':>8} "
f"{'min_conf':>9}{'min_edge':>10}{'max_edge':>10}"
f"{'min_odds':>10}{'max_odds':>10}{'min_rel':>9}{'cons':>15}")
for i, w in enumerate(winners, 1):
print(f" {i:<5}{w['n']:>5}{w['hit_pct']:>7}{w['roi_pct']:>+8}"
f" {w['min_conf']:>9}{w['min_edge']:>+10.3f}{w['max_edge']:>+10.3f}"
f"{w['min_odds']:>10.2f}{w['max_odds']:>10.2f}"
f"{w['min_reliability']:>9.2f}{w['consensus']:>15}")
if keep_high_volume and keep_high_volume not in winners:
print(f" high {keep_high_volume['n']:>5}{keep_high_volume['hit_pct']:>7}"
f"{keep_high_volume['roi_pct']:>+8}"
f" {keep_high_volume['min_conf']:>9}"
f"{keep_high_volume['min_edge']:>+10.3f}"
f"{keep_high_volume['max_edge']:>+10.3f}"
f"{keep_high_volume['min_odds']:>10.2f}"
f"{keep_high_volume['max_odds']:>10.2f}"
f"{keep_high_volume['min_reliability']:>9.2f}"
f"{keep_high_volume['consensus']:>15}")
# Pick a "good" recommendation: best ROI with n >= min_bets
# If best ROI is still negative, flag the market as unprofitable.
best = winners[0]
all_winners[market] = best
if best["roi_pct"] <= 0:
print(f" ⚠️ Best config still loses money (ROI={best['roi_pct']}%) "
f"— consider muting this market entirely.")
else:
print(f" ✅ Best config: ROI={best['roi_pct']}% on {best['n']} bets "
f"(vs baseline {base['roi_pct']}% on {n_total}).")
# ─── Aggregate impact ────────────────────────────────────────────────
print(f"\n{'=' * 78}")
print("AGGREGATE IMPACT (if we apply each market's best config)")
print(f"{'=' * 78}")
total_old_bets = total_old_profit = total_old_staked = 0
total_new_bets = total_new_profit = total_new_staked = 0
for market, win in all_winners.items():
base = baseline(pdf, market)
total_old_bets += base["n"]
total_old_profit += base["profit"]
total_old_staked += base["staked"]
total_new_bets += win["n"]
total_new_profit += win["profit"]
total_new_staked += win["staked"]
base_roi = 100.0 * total_old_profit / total_old_staked if total_old_staked else 0
new_roi = 100.0 * total_new_profit / total_new_staked if total_new_staked else 0
print(f" Baseline: {total_old_bets:>4} bets, "
f"profit={total_old_profit:+.2f}u, ROI={base_roi:+.2f}%")
print(f" Optimized: {total_new_bets:>4} bets, "
f"profit={total_new_profit:+.2f}u, ROI={new_roi:+.2f}%")
print(f" Δ: {total_new_bets - total_old_bets:+d} bets, "
f"{total_new_profit - total_old_profit:+.2f}u, "
f"{new_roi - base_roi:+.2f}pp")
# ─── Write JSON patch ────────────────────────────────────────────────
patch_path = os.path.join(REPORTS_DIR, "filter_optimization_patch.json")
patch = {market: {
"min_calibrated_confidence": win["min_conf"],
"min_ev_edge": win["min_edge"],
"max_ev_edge": win["max_edge"],
"min_odds": win["min_odds"],
"max_odds": win["max_odds"],
"min_odds_reliability": win["min_reliability"],
"require_v27_agree": win["consensus"] == "agree_or_null",
"expected_n_bets": win["n"],
"expected_hit_pct": win["hit_pct"],
"expected_roi_pct": win["roi_pct"],
} for market, win in all_winners.items()}
with open(patch_path, "w", encoding="utf-8") as f:
json.dump(patch, f, indent=2)
print(f"\nPatch saved: {patch_path}")
if args.apply:
print("\n--apply flag set. Patching not implemented yet — "
"review the patch JSON and update config/market_thresholds.json manually.")
if __name__ == "__main__":
main()
+54
View File
@@ -0,0 +1,54 @@
"""Smoke test for the score-coherence filter using the LAFC vs Sounders
1-0 scenario from production. Verifies that markets that contradict the
predicted score are correctly excluded from the coherent set, and that
the markets the model got right are all included.
"""
import os, sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from services.betting_brain import BettingBrain
brain = BettingBrain()
pkg = {
"score_prediction": {"ft": "1-0", "ht": "0-0"},
}
coh = brain._score_consistent_markets(pkg)
print(f"Predicted: 1-0 (HT 0-0)")
print(f"Coherent set size: {len(coh)}")
print()
# Each pick the system actually offered for the LAFC match, with whether
# it was the *actual* winning pick.
test_picks = [
("MS", "1", True, "correct"),
("MS", "2", False, "wrong"),
("MS", "X", False, "wrong"),
("DC", "1X", True, "correct"),
("DC", "12", True, "correct"),
("DC", "X2", False, "wrong"),
("OU25", "Üst", False, "WRONG — system featured this"),
("OU25", "Alt", True, "correct"),
("OU35", "Alt", True, "correct"),
("OU35", "Üst", False, "wrong"),
("BTTS", "Var", False, "wrong"),
("BTTS", "Yok", True, "correct"),
("HT", "X", True, "correct"),
("HT", "1", False, "wrong"),
("HTFT", "X/1", True, "correct"),
("HTFT", "1/1", False, "wrong (HT was 0-0)"),
("HT_OU05", "Üst", False, "wrong"),
("HT_OU05", "Alt", True, "correct"),
("OE", "Çift", False, "wrong (1 is odd)"),
("OE", "Tek", True, "correct"),
]
print(f"{'market':<10}{'pick':<10}{'real-win?':<12}{'in-coherent?':<14}{'match?'}")
print("-" * 60)
ok = 0
for market, pick, would_win, note in test_picks:
in_coh = (market, pick) in coh
match = "" if in_coh == would_win else "✗ MISMATCH"
if in_coh == would_win: ok += 1
print(f"{market:<10}{pick:<10}{str(would_win):<12}{str(in_coh):<14}{match} {note}")
print()
print(f"Result: {ok}/{len(test_picks)} picks correctly classified")
+112
View File
@@ -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()
+191
View File
@@ -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())
+623 -28
View File
@@ -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.07.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,6 +75,181 @@ class BettingBrain:
SNIPER_BLOCKED_MARKETS = {"HT", "HTFT", "OE", "CARDS", "HT_OU05", "HT_OU15"}
# V30: NO markets muted — backtest tüm marketlerin gerçek ROI'sini görmeli.
# Tier sistemi zaten filtreleme yapıyor; mute etmek veri kaybına yol açar.
MUTED_MARKETS = set()
# ═══════════════════════════════════════════════════════════════════
# V31d: KANITA DAYALI KADEMELİ DEĞER SİSTEMİ (Evidence-Based Tiers)
# ═══════════════════════════════════════════════════════════════════
# User directive: show 3 quality levels so the bettor picks by risk
# appetite ("hangi bahis hangi oranda tutuyor"). Each MS underdog tier
# carries a `value_tier` label that propagates to the UI. Only the
# PREMIUM band is auto-staked (BET); strong/standard surface as WATCH
# (full analysis shown, not staked) because the data says they break
# even — see new_gate_sim.py.
#
# Validated on 60-day, 72,582-settled-row multi-pick backtest
# (ms_envelope.py + new_gate_sim.py, span 2026-04-17..05-28):
# PREMIUM (6.0-7.5, gap≥0, protective vetoes kept):
# 602 bets, +32.7% ROI, +39.4u, 20.6% hit, avgOdd 6.50
# ALL 6 weeks positive (+13.7%..+52.9%); OOS(>05-24) +47.4%;
# survives dropping top-5 wins (+24%). 14.3 bets/day.
# STRONG (5.0-6.0, gap≥0): ~breakeven (-1%) → WATCH, not staked.
# STANDARD (3.0-5.0, gap≥0): +0.5% breakeven → WATCH, volume zone.
#
# WHY 6.0-7.5 (not 6.0-50.0 as in V31c): the edge is concentrated.
# odds 6.0-7.0 +35% | 7.0-8.0 ~breakeven | 8.0+ NEGATIVE (longshot
# graveyard, -10..-26% ROI). The old wide premium tier let losing
# longshots in. Above 7.5 the model's edge evaporates.
#
# ROOT-CAUSE FIX (the volume crisis): underdogs were structurally
# un-bettable. Two vetoes (calibrated_confidence_too_low,
# play_score_too_low) auto-REJECTED every dog because they demand
# >45% model confidence — a FAVORITE-picking rule. A 6.5 dog wins
# ~20% of the time; that IS the edge (odds premium), not a defect.
# For value-tier matches we bypass those two vetoes and score from
# the validated tier quality instead of win-probability. Genuine
# protections stay: extreme_neg_ev, ev_too_high_trap, htft_reversal
# _risk_high, v25_v27_hard_disagreement. Result: 28 → 602 staked
# bets (22x volume), -1.6u → +39.4u profit. ALL rich analysis data
# (market_board, v25/v27, triple_value, probs) is untouched — only
# the `playable` flag changes.
#
# MULTI-LEG VERDICT (definitive): parlays DESTROY edge.
# 1-leg +3.4% → 2-leg -32% → 3-leg -67% → 4-leg -83%.
# System must bet SINGLES only. No combo recommendations.
#
# Non-MS markets: ultrastrict tiers (rarely pass BET) → info-only.
# All non-MS configurations showed negative ROI in backtest.
# ═══════════════════════════════════════════════════════════════════
MARKET_ODDS_TIERS = {
# ── MS (Match Score / 1X2) — the ONLY profitable market ────────
"MS": [
# PREMIUM — the validated edge. 6.0-7.5 odds, model >= market.
# 60d: 602 bets, +32.7% ROI, all weeks positive. AUTO-STAKED.
# Low hit (~20%) → high variance; stake stays small (see _brain_stake).
{"min_odds": 6.00, "max_odds": 7.50, "min_edge": -0.20,
"max_edge": 0.25, "min_reliability": 0.30,
"min_model_gap": 0.0,
"require_v27_agree": False, "require_no_trap": False,
"value_tier": "premium",
"label": "ms_underdog_premium"},
# STRONG — 5.0-6.0. Breakeven (-1%) → WATCH (visible, not staked).
{"min_odds": 5.00, "max_odds": 6.00, "min_edge": -0.20,
"max_edge": 0.25, "min_reliability": 0.30,
"min_model_gap": 0.0,
"require_v27_agree": False, "require_no_trap": False,
"value_tier": "strong",
"label": "ms_underdog_strong"},
# STANDARD — 3.0-5.0 volume zone. +0.5% breakeven → WATCH.
{"min_odds": 3.00, "max_odds": 5.00, "min_edge": -0.18,
"max_edge": 0.25, "min_reliability": 0.30,
"min_model_gap": 0.0,
"require_v27_agree": False, "require_no_trap": False,
"value_tier": "standard",
"label": "ms_underdog_standard"},
],
# ── Non-MS markets: visible but NOT playable ───────────────────
# All non-MS markets showed negative ROI in 50K-row backtest.
# Tiers exist so the model's read is surfaced (bet_summary),
# but criteria are strict enough that almost nothing passes BET.
# The user sees info; the system doesn't lose money on them.
"DC": [
{"min_odds": 1.15, "max_odds": 1.60, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.55,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "dc_ultrastrict"},
],
"OU25": [
{"min_odds": 1.60, "max_odds": 2.20, "min_edge": 0.02,
"max_edge": 0.10, "min_reliability": 0.55,
"max_model_gap": -0.03,
"require_v27_agree": False, "require_no_trap": True,
"label": "ou25_ultrastrict"},
],
"OU35": [
{"min_odds": 1.50, "max_odds": 2.50, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ou35_ultrastrict"},
],
"BTTS": [
{"min_odds": 1.60, "max_odds": 2.10, "min_edge": 0.02,
"max_edge": 0.10, "min_reliability": 0.55,
"max_model_gap": -0.03,
"require_v27_agree": False, "require_no_trap": True,
"label": "btts_ultrastrict"},
],
"HT": [
{"min_odds": 2.00, "max_odds": 3.50, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ht_ultrastrict"},
],
"OU15": [
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ou15_ultrastrict"},
],
"HTFT": [
{"min_odds": 4.00, "max_odds": 15.00, "min_edge": 0.03,
"max_edge": 0.15, "min_reliability": 0.45,
"require_v27_agree": False, "require_no_trap": True,
"label": "htft_ultrastrict"},
],
"OE": [
{"min_odds": 1.80, "max_odds": 2.10, "min_edge": 0.02,
"max_edge": 0.08, "min_reliability": 0.55,
"max_model_gap": -0.03,
"require_v27_agree": False, "require_no_trap": True,
"label": "oe_ultrastrict"},
],
"HT_OU05": [
{"min_odds": 1.30, "max_odds": 2.00, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ht_ou05_ultrastrict"},
],
"HT_OU15": [
{"min_odds": 1.60, "max_odds": 3.00, "min_edge": 0.02,
"max_edge": 0.12, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "ht_ou15_ultrastrict"},
],
"CARDS": [
{"min_odds": 1.60, "max_odds": 2.50, "min_edge": 0.02,
"max_edge": 0.10, "min_reliability": 0.50,
"max_model_gap": -0.02,
"require_v27_agree": False, "require_no_trap": True,
"label": "cards_ultrastrict"},
],
}
# Legacy flat envelope (backward compat for markets not in tiered system)
MARKET_OPTIMAL_FILTERS = {}
MARKET_PRIORS = {
"DC": 4.0,
"OU15": 3.0,
@@ -52,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
@@ -86,6 +304,36 @@ class BettingBrain:
watchlist.sort(key=self._candidate_sort_key, reverse=True)
no_value.sort(key=self._candidate_sort_key, reverse=True)
# ── SCORE COHERENCE FILTER ──────────────────────────────────────
# If the model also produced a score prediction (e.g. 1-0), pick
# main_pick from the subset of candidates that would WIN at that
# score. Stops the system from recommending OU25 Üst while also
# predicting 1-0 (only 1 goal). Falls back to original list if no
# coherent candidate exists.
coherent_set = self._score_consistent_markets(guarded)
coherent_flag = False
if coherent_set:
def is_coherent(row: Dict[str, Any]) -> bool:
m = str(row.get("market") or "")
p = str(row.get("pick") or "")
return (m, p) in coherent_set
approved_coh = [r for r in approved if is_coherent(r)]
watchlist_coh = [r for r in watchlist if is_coherent(r)]
if approved_coh:
approved = approved_coh
coherent_flag = True
elif watchlist_coh:
# No coherent BET candidates — at least promote a coherent
# watch over an incoherent BET.
watchlist = watchlist_coh + [r for r in watchlist if not is_coherent(r)]
coherent_flag = True
# Tag every row so the UI/diagnostics can see what happened
for row in judged_rows.values():
row.setdefault("betting_brain", {})
row["betting_brain"]["score_coherent"] = is_coherent(row)
original_main = guarded.get("main_pick") or {}
main_pick = None
decision = "NO_BET"
@@ -142,10 +390,11 @@ class BettingBrain:
rejected = [d for d in decisions if d.get("action") == "REJECT"]
guarded["betting_brain"] = {
"version": "judge-v1",
"version": "judge-v31f-national-regime",
"decision": decision,
"reason": decision_reason,
"main_pick_key": main_key or None,
"score_coherent_filter_applied": coherent_flag,
"approved_count": len(approved),
"watchlist_count": len(watchlist),
"rejected_count": len(rejected),
@@ -171,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
@@ -184,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] = []
@@ -200,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))
@@ -220,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
@@ -243,6 +511,85 @@ class BettingBrain:
if play_score < 50.0 and not is_value_sniper:
vetoes.append("play_score_too_low")
# ── HARD EV-EDGE VETO ───────────────────────────────────────────
# Diagnostic backtest (1000 maç, 524 settled bet) gösterdi ki
# ev_edge < 0 olan bahisler %76 of all picks ve ROI yaklaşık -%16.
# ev_edge < 0 = "model market'in altında olasılık veriyor" = vig'i
# yiyemeyeceğimiz negative-EV bahis. Hard veto: oynama.
# Sniper override hâlâ geçer (yüksek convicted alternatif pick'ler).
# V29b: negative_ev_edge hard veto REMOVED — tier system handles
# edge bounds per-market via min_edge. MS underdog tier allows
# ev >= -0.15, so a universal ev<0 veto would kill profitable bets.
if ev_edge < -0.20: # Only veto truly extreme negative edge
vetoes.append("extreme_negative_ev_edge")
issues.append(f"ev_edge={ev_edge:.3f}_extreme_negative")
# Trap edge: bizim diagnostic backtest'te ev_edge >= 0.20 olan tüm
# bahisler kaybediyordu (n=10, hepsi -%25+ ROI). Model market'i bu
# kadar yanlış buluyorsa muhtemelen modelin kendisinin yanlış olduğu
# bir senaryo (eksik info, tuhaf maç, vs.) — oynama.
if ev_edge >= 0.30: # V29b: raised from 0.20, tiers cap at 0.25
vetoes.append("ev_edge_too_high_trap")
issues.append(f"ev_edge={ev_edge:.3f}_trap_range")
# ── MUTED MARKETS (grid search showed no profitable config) ──
if market in self.MUTED_MARKETS: # V29: hard veto, no sniper bypass
vetoes.append("market_muted_by_backtest")
issues.append(f"market_{market}_muted")
# ── V30: ODDS-TIERED ENVELOPE (from 7K backtest grid search) ──
# Each market has multiple odds zones with different filters.
# If a bet doesn't fit ANY tier, it gets vetoed.
# V30: added model_gap filtering — data shows model>market is
# inversely correlated with winning for BTTS/OU25.
tiers = self.MARKET_ODDS_TIERS.get(market, [])
# Also check legacy flat envelope for backward compat
legacy_env = self.MARKET_OPTIMAL_FILTERS.get(market)
tier_matched = False
tier_label = None
tier_value = None # V31c: quality tier (premium/strong/standard)
if tiers:
for tier in tiers:
if not (tier["min_odds"] <= odds <= tier["max_odds"]):
continue
if ev_edge < tier["min_edge"] or ev_edge > tier["max_edge"]:
continue
if odds_rel < tier["min_reliability"]:
continue
if tier.get("require_v27_agree") and consensus != "AGREE":
continue
if tier.get("require_no_trap") and trap_market_flag:
continue
# V30: model-market gap filter
if model_gap is not None:
if "min_model_gap" in tier and model_gap < tier["min_model_gap"]:
continue
if "max_model_gap" in tier and model_gap > tier["max_model_gap"]:
continue
tier_matched = True
tier_label = tier.get("label")
tier_value = tier.get("value_tier") # V31c
break
if not tier_matched:
vetoes.append("outside_all_odds_tiers")
issues.append(f"no_profitable_tier_for_{market}_at_odds_{odds:.2f}")
elif legacy_env:
if ev_edge < legacy_env["min_edge"]:
vetoes.append("outside_envelope_edge_low")
if ev_edge > legacy_env["max_edge"]:
vetoes.append("outside_envelope_edge_high")
if odds and odds < legacy_env["min_odds"]:
vetoes.append("outside_envelope_odds_low")
if odds and odds > legacy_env["max_odds"]:
vetoes.append("outside_envelope_odds_high")
if odds_rel < legacy_env["min_reliability"]:
vetoes.append("outside_envelope_reliability_low")
if legacy_env.get("require_v27_agree") and consensus != "AGREE":
vetoes.append("outside_envelope_v27_must_agree")
# V31d: a matched value tier is the validated profitable signal.
# It unlocks the value-betting regime (veto bypass + score floor).
is_value_tier = tier_value in self.VALUE_TIER_NAMES
if divergence is not None:
if divergence >= self.HARD_DIVERGENCE and not is_value_sniper:
score -= 42.0
@@ -254,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:
@@ -371,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:
@@ -393,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,
@@ -407,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)}")
@@ -465,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),
@@ -481,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]:
@@ -506,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),
@@ -521,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 {}
@@ -563,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
@@ -635,6 +1124,112 @@ class BettingBrain:
return self._safe_float(ou25.get(key)) if key else None
return None
def _score_consistent_markets(self, package: Dict[str, Any]) -> Optional[set]:
"""Build the set of (market, pick) tuples that WOULD WIN if the
model's own score prediction came true. We use this as a coherence
gate: if the model is confident about a 1-0 outcome but also wants
to play OU25 Üst, those two beliefs contradict each other and the
score prediction is the more informative one because it aggregates
all market signals into a single most-likely scenario.
Returns None if the score prediction is missing or malformed; in
that case we skip the coherence check.
"""
score_pred = package.get("score_prediction") or {}
ft_raw = str(score_pred.get("ft") or score_pred.get("full_time") or "").strip()
ht_raw = str(score_pred.get("ht") or score_pred.get("half_time") or "").strip()
def parse(s: str) -> Optional[Tuple[int, int]]:
for sep in ("-", ":", ""):
if sep in s:
parts = s.split(sep, 1)
try:
return int(parts[0].strip()), int(parts[1].strip())
except (ValueError, IndexError):
return None
return None
ft = parse(ft_raw)
if ft is None:
return None
ht = parse(ht_raw)
fh, fa = ft
total = fh + fa
consistent: set = set()
# MS / 1X2 — single outcome
if fh > fa:
consistent.add(("MS", "1"))
consistent.add(("ML", "1"))
elif fh < fa:
consistent.add(("MS", "2"))
consistent.add(("ML", "2"))
else:
consistent.add(("MS", "X"))
consistent.add(("ML", "X"))
# DC — two of three legs win at any score
if fh >= fa:
consistent.add(("DC", "1X"))
if fh <= fa:
consistent.add(("DC", "X2"))
if fh != fa:
consistent.add(("DC", "12"))
# Over/Under main lines
for line, market in ((0.5, "OU05"), (1.5, "OU15"),
(2.5, "OU25"), (3.5, "OU35"), (4.5, "OU45")):
if total > line:
for p in ("Üst", "Ust", "Over", "OVER"):
consistent.add((market, p))
elif total < line:
for p in ("Alt", "Under", "UNDER"):
consistent.add((market, p))
# total == line → push, neither side wins → don't add
# BTTS — both teams score
if fh > 0 and fa > 0:
for p in ("Var", "KG Var", "Yes", "YES"):
consistent.add(("BTTS", p))
else:
for p in ("Yok", "KG Yok", "No", "NO"):
consistent.add(("BTTS", p))
# OE — total goals odd/even
if total % 2 == 1:
for p in ("Tek", "Odd", "ODD"):
consistent.add(("OE", p))
else:
for p in ("Çift", "Cift", "Even", "EVEN"):
consistent.add(("OE", p))
# HT-only markets (need HT score)
if ht is not None:
hh, ha = ht
ht_total = hh + ha
if hh > ha:
consistent.add(("HT", "1"))
elif hh < ha:
consistent.add(("HT", "2"))
else:
consistent.add(("HT", "X"))
for line, market in ((0.5, "HT_OU05"), (1.5, "HT_OU15"), (2.5, "HT_OU25")):
if ht_total > line:
for p in ("Üst", "Ust", "Over"):
consistent.add((market, p))
elif ht_total < line:
for p in ("Alt", "Under"):
consistent.add((market, p))
# HTFT — single combo
ht_o = "1" if hh > ha else "2" if hh < ha else "X"
ft_o = "1" if fh > fa else "2" if fh < fa else "X"
consistent.add(("HTFT", f"{ht_o}/{ft_o}"))
consistent.add(("HTFT", f"{ht_o}{ft_o}"))
return consistent
def _triple_value(self, package: Dict[str, Any], key: Optional[str]) -> Optional[Dict[str, Any]]:
if not key:
return None
+13 -1
View File
@@ -449,6 +449,12 @@ class DataLoaderMixin:
return 1.5, 1.2
return weighted_for / total_weight, weighted_against / total_weight
# Approximate European season window — Eredivisie/PL/La Liga start late
# July / mid-August, end May. Using 300 days as a buffer covers most
# competitions while excluding "career points" from previous seasons.
# When a proper seasons table lands this should query season boundaries.
_SEASON_LOOKBACK_MS = 300 * 24 * 60 * 60 * 1000
def _estimate_league_position(
self,
cur: RealDictCursor,
@@ -458,6 +464,7 @@ class DataLoaderMixin:
) -> int:
if not team_id or not league_id:
return 10
season_start_ms = before_date_ms - self._SEASON_LOOKBACK_MS
try:
cur.execute(
"""
@@ -478,6 +485,7 @@ class DataLoaderMixin:
AND m.score_home IS NOT NULL
AND m.score_away IS NOT NULL
AND m.mst_utc < %s
AND m.mst_utc >= %s
UNION ALL
SELECT
m.away_team_id AS team_id,
@@ -492,11 +500,15 @@ class DataLoaderMixin:
AND m.score_home IS NOT NULL
AND m.score_away IS NOT NULL
AND m.mst_utc < %s
AND m.mst_utc >= %s
) tm
GROUP BY tm.team_id
ORDER BY points DESC
""",
(league_id, before_date_ms, league_id, before_date_ms),
(
league_id, before_date_ms, season_start_ms,
league_id, before_date_ms, season_start_ms,
),
)
rows = cur.fetchall()
if not rows:
@@ -225,20 +225,43 @@ class FeatureBuilderMixin:
if enrichment_failures:
print(f"⚠️ Enrichment partial failures for {data.match_id}: {', '.join(enrichment_failures)}")
# ── Cup game detection (used by upset engine + elo dampening below) ──
_league_name_lower = (getattr(data, 'league_name', '') or '').lower()
_cup_keywords = ("kupa", "cup", "coupe", "copa", "coppa", "pokal",
"trophy", "shield", "ziraat", "süper kupa", "super cup",
"beker", "taça", "taca")
_is_cup_match = any(kw in _league_name_lower for kw in _cup_keywords)
# ── League size hint: top European leagues 18-20 teams, lower 16-24 ──
# We don't have a per-league team count, so fall back to 20 (standard).
# When standings infra lands this should pull from seasons table.
_league_total_teams = 20
# Upset engine features
upset_atmosphere, upset_motivation, upset_fatigue = 0.0, 0.0, 0.0
try:
upset_engine = get_upset_engine()
# Use the real position estimates from data_loader; fall back to mid-
# table (10) only when the loader couldn't compute one. Hardcoding 10
# for every team made motivation_score collapse to 0 for everyone.
_home_pos = getattr(data, 'home_position', None)
_away_pos = getattr(data, 'away_position', None)
if _home_pos is None or _home_pos <= 0:
_home_pos = 10
if _away_pos is None or _away_pos <= 0:
_away_pos = 10
upset_feats = upset_engine.get_features(
home_team_name=getattr(data, 'home_team_name', '') or '',
home_team_id=data.home_team_id,
away_team_name=getattr(data, 'away_team_name', '') or '',
league_name=getattr(data, 'league_name', '') or '',
home_position=10,
away_position=10,
home_position=_home_pos,
away_position=_away_pos,
match_date_ms=data.match_date_ms,
is_cup_match=_is_cup_match,
home_days_rest=int(home_rest),
away_days_rest=int(away_rest),
total_teams=_league_total_teams,
)
upset_atmosphere = upset_feats.get('upset_atmosphere', 0.0)
upset_motivation = upset_feats.get('upset_motivation', 0.0)
@@ -276,15 +299,10 @@ class FeatureBuilderMixin:
is_season_start = 1.0 if match_month in (7, 8, 9) else 0.0
is_season_end = 1.0 if match_month in (5, 6) else 0.0
# ── Cup game detection: dampen home advantage in feature space ──
_league_name = (getattr(data, 'league_name', '') or '').lower()
_cup_keywords = ("kupa", "cup", "coupe", "copa", "coppa", "pokal",
"trophy", "shield", "ziraat", "süper kupa", "super cup")
_is_cup = any(kw in _league_name for kw in _cup_keywords)
# ── Derived / Interaction features (V27) ──
# Cup games: home ELO advantage is ~30% weaker (rotation, lower motivation)
elo_diff = (home_elo - away_elo) * (0.70 if _is_cup else 1.0)
# Uses _is_cup_match computed earlier (before upset engine call).
elo_diff = (home_elo - away_elo) * (0.70 if _is_cup_match else 1.0)
form_elo_diff = home_form_elo_val - away_form_elo_val
attack_vs_defense_home = data.home_goals_avg - data.away_conceded_avg
attack_vs_defense_away = data.away_goals_avg - data.home_conceded_avg
+488 -14
View File
@@ -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)),
+17 -2
View File
@@ -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:
+80
View File
@@ -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.")
+139
View File
@@ -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.")
+84
View File
@@ -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.")
+60
View File
@@ -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 {}
+67
View File
@@ -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.07.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"
-173
View File
@@ -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)
-54
View File
@@ -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);
-212
View File
@@ -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);
-365
View File
@@ -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);
View File
-77
View File
@@ -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);
-10
View File
@@ -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
}
-62
View File
@@ -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
-4
View File
@@ -1,4 +0,0 @@

> Suggest-Bet-BE@0.0.1 lint
> eslint "{src,apps,libs,test}/**/*.ts" --fix
+305
View File
@@ -0,0 +1,305 @@
# SESSION HANDOFF — iddaai sistem durumu
**Son güncelleme**: 2026-05-25 ~23:00 (Windows'tan Mac'e geçiş öncesi)
**Hedef**: Başka makinede / yeni Claude session'ında bu doc tek başına okunup işin nerede kaldığı anlaşılabilmeli.
---
## 🚨 EN SON DURUM (Mac'e geçmeden önce oku)
### Validation backtest ÖLDÜ
- Pencere: 2026-05-01 → 2026-05-14, 1500 maç
- **1200/1500'de SSH tunnel düşünce process sessizce öldü**
- **CSV kayıp** — script eski versiyondu, sadece sonda yazıyordu
- Sebep: localhost:5432 erişimi kayboldu, psycopg2 connection error
### Script DÜZELTILDI (Mac'te kullanılabilir)
`scripts/diagnostic_backtest.py` artık **crash-safe**:
- `--checkpoint-every 100` → her 100 maçta partial CSV diske yazılır
- Crash sonrası tekrar koşulunca **otomatik kaldığı yerden devam**
- Checkpoint dosyası: `reports/_checkpoint_<window-key>.csv`
- `--no-resume` flag fresh başlamak için
### Git push BEKLİYOR
- 36 dosya **commit edildi (local)** — bkz "Bu seansta yapılan KOD değişiklikleri"
- Push **auth hatası** verdi (gitea credentials cached değil)
- **User Mac'te push yapacak** (Gitea Personal Access Token gerekli, repo write yetkisi)
### Mac'te yapılacaklar (öncelikli sırayla)
1. Repo'yu clone et veya OneDrive'dan kopyala (eğer Mac OneDrive senkronize ediyorsa)
2. `git push origin main` ile pending commit'i remote'a yolla
3. SSH tunnel kur (Pi @ 95.70.252.214, port 2222) → DB için tunnel localhost:5432
4. Yeni Claude session'ı başlat, bu dosyayı oku, devam et
5. Backtest tekrar koştur (çoktan eski versiyondu, şimdi crash-safe)
```bash
cd ai-engine
export DATABASE_URL="postgresql://iddaai_user:IddaA1_S4crET!@localhost:5432/iddaai_db?schema=public"
export PYTHONIOENCODING=utf-8
python scripts/diagnostic_backtest.py --start 2026-05-01 --end 2026-05-14 --max-matches 1500
# ölürse, aynı komutu tekrar koş — checkpoint'ten devam eder
```
### Mevcut sağlam veri
Validation kayıp ama elimizde **in-sample backtest** ve **grid search** çıktıları var:
- `reports/diagnostic_backtest_20260525_035649.{csv,json,txt}` — 1000 maç, May 11-24
- `reports/filter_optimization_patch.json` — grid search winners
- Bu data ile in-sample analiz tamamlandı, validation eksik
---
---
## 🎯 Üst-seviye hedef
Sistem **maç başı-1 saat** kullanıcı tetiklemesiyle çalışacak. Bahis uzmanı seviyesinde:
- **main_pick + value_pick** (sistemin önerdiği)
- **Tüm market olasılıkları** (MS, HT, OU05-45, BTTS, OE, DC, HTFT, HCAP, Cards, Corners)
- **Net HT + FT skoru** + **Top-5 olası skor dağılımı**
- **Evidence panel**: lineup impact, son 5 maç, h2h, hakem profili, benzer-oran-band geçmişi
Ürün modeli: hem user kendi bahisini oynar, hem sistem para kazanırsa abonelik satılır.
Hedef ROI: **≥%10**. Günde **3-5 kaliteli bahis**.
Detaylı requirements doc: bu dosyanın altında, "Requirements Spec" bölümü.
---
## 🟢 Şu an arka planda KOŞAN işler
### 1. Validation backtest (LOCAL — bu laptop)
- **Script**: `ai-engine/scripts/diagnostic_backtest.py`
- **Komut**: `python scripts/diagnostic_backtest.py --start 2026-05-01 --end 2026-05-14 --max-matches 1500`
- **Log**: `ai-engine/validation_full.log` (OneDrive senkronize)
- **Çıkış**: bittiğinde `ai-engine/reports/diagnostic_backtest_<timestamp>.{csv,json,txt}`
- **Tahmini bitiş**: 2026-05-25 ~22:00 (yaklaşık)
- **Amaç**: Yeni kodla (calibrator + ev_edge veto + envelope + coherence + BTTS mute) **out-of-sample** doğrulama
- **Risk**: Laptop uyursa ölür. Bitmesini beklemen lazım VEYA partial sonuçla devam.
```powershell
# Status check (kendin)
$log='C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\ai-engine\validation_full.log'
Select-String $log 'rate=|Outputs:' | Select-Object -Last 3 | ForEach-Object {$_.Line}
```
### 2. Feeder historical scan (REMOTE — Pi server)
- **Konum**: SSH @ haruncan@95.70.252.214:2222 → docker container `iddaai-be` → pm2
- **PM2 process**: `feeder-historical` (id=1)
- **Log rotation**: pm2-logrotate kurulu (max 30MB/dosya, 3 dosya, gzip)
- **Davranış**: 2026-05-03'ten geriye 2023-06-01'e kadar mackolik'ten odds/lineup patch
- **Otomatik restart**: 502 olunca 30 sn delay sonra restart (max 1000 kez)
- **Beklenen süre**: 24-72 saat
```bash
# Status (kendin SSH'la)
sudo docker exec iddaai-be pm2 list
sudo docker exec iddaai-be pm2 logs feeder-historical --lines 30 --nostream
```
---
## 📝 Bu seansta yapılan KOD değişiklikleri
Hepsi local repo'da, OneDrive senkronize edecek, başka makinede pull etmesen de açtığında orada olacak.
### A. Settlement / data layer
| Dosya | Değişiklik |
|---|---|
| `iddaai-be/prisma.config.ts` | `.env` fallback ekledim (`.env.local` üstüne) — `prisma generate` çalışsın diye |
| `iddaai-be/src/tasks/prediction-settlement.market-resolver.ts` | DC parser ayraçsız "1X/X2/12" kabul ediyor + HT_OU05/HT_OU15/HT_OU25 resolver eklendi |
| `iddaai-be/src/tasks/feature-enrichment.task.ts` **(YENİ)** | Cron 08:15 — eksik football_ai_features row insert + odds_movement SQL backfill |
| `iddaai-be/src/tasks/python-enrichment.task.ts` **(YENİ)** | Cron 08:25 — Python `enrich_ai_features.py` subprocess |
| `iddaai-be/src/tasks/tasks.module.ts` | İki yeni task register |
| `iddaai-be/src/scripts/run-feature-enrichment.ts` **(YENİ)** | Manuel one-shot trigger |
### B. AI engine — betting brain
`iddaai-be/ai-engine/services/betting_brain.py` — büyük revizyon:
- **HARD_MIN_SAMPLES = 50** floor (calibrator bypass <50 sample)
- **`ev_edge < 0.0` HARD VETO** (`negative_ev_edge`)
- **`ev_edge >= 0.20` HARD VETO** (`ev_edge_too_high_trap`)
- **`MUTED_MARKETS = {"BTTS"}`** — backtest no profitable config bulduğu için
- **`MARKET_OPTIMAL_FILTERS`** — MS ve OU25 için grid-search'ten gelen optimal envelope
- **`_score_consistent_markets()`** — skor tahminine uymayan picks elimine
- **`judge()` score coherence filter** — main_pick coherent set'ten seçilir
- **HTFT reversal cross-check** — Man City 1/2 senaryosu
### C. AI engine — model & calibration
| Dosya | Değişiklik |
|---|---|
| `ai-engine/models/calibration.py` | HARD_MIN_SAMPLES floor + sample-weighted blend formülü değişti |
| `ai-engine/models/calibration/*.pkl` | **10 calibrator retrain** (ms_home/draw/away, ou15/25/35, btts, ht_home/draw/away) — 4989-5000 sample her biri |
### D. AI engine — orchestrator feature builder
`ai-engine/services/orchestrator/feature_builder.py`:
- Hardcoded `home_position=10, away_position=10` → real `data.home_position` kullanılıyor
- Cup detection upper'a taşındı, `is_cup_match` UpsetEngine'e geçiyor
- Total teams parametresi UpsetEngine'e geçiyor
`ai-engine/services/orchestrator/data_loader.py`:
- `_estimate_league_position` artık **sezon filtresi** (son 300 gün) kullanıyor
### E. AI engine — scripts (yeni)
| Dosya | Ne yapıyor |
|---|---|
| `ai-engine/scripts/diagnostic_backtest.py` | Per-bet diagnostic backtest (CSV+JSON+TXT output) |
| `ai-engine/scripts/analyze_backtest_csv.py` | Backtest CSV üzerinde root-cause hipotez testleri |
| `ai-engine/scripts/optimize_filters.py` | Grid search per-market optimal threshold |
| `ai-engine/scripts/compare_backtests.py` | İki CSV karşılaştırması verdict ile |
| `ai-engine/scripts/test_score_coherence.py` | Coherence filter smoke test (LAFC senaryosu) |
### F. Social poster modülü (NestJS)
| Dosya | Değişiklik |
|---|---|
| `src/modules/social-poster/social-poster.service.ts` | Cron 15→10 dk, window 10-60, MAX_POSTS_PER_RUN, getHealthStatus() |
| `src/modules/social-poster/image-renderer.service.ts` | SEO filename + metadata sidecar (.json) |
| `src/modules/social-poster/caption-generator.service.ts` | SEO hashtag stratejisi (12 küratör tag) |
| `src/modules/social-poster/social-poster.controller.ts` | `/health` public + `/preview-png/:matchId` + `/run-now` endpoints |
| `mds/SOCIAL_POSTER_SETUP.md` **(YENİ)** | Env vars + API key alma adımları + test komutları |
### G. Modern image rendering (deneme)
| Dosya | Açıklama |
|---|---|
| `src/scripts/render-social-card-v3.ts` | satori + resvg-js ile modern HTML→PNG rendering (Twemoji top + bayrak) |
| `src/modules/social-poster/assets/*.svg` | Twemoji futbol/basket/bayrak SVG'leri |
### H. Yapılan DB değişiklikleri (idempotent — tekrar koşturulursa sorun yok)
| İşlem | Etki |
|---|---|
| `football_ai_features` 4008+ satır backfill | Son 60 günün FT maçları için feature row var artık (calculator_ver=feature_enrichment_task_v1) |
| Python enrichment koştu | h2h, referee, possession, league_avg, implied_* hepsi gerçek değerlerle dolu (181,614+ satır enriched) |
| Calibrator dosyaları yazıldı | `ai-engine/models/calibration/*.pkl` overwritten |
---
## 📂 Önemli dosya konumları (OneDrive synced)
```
iddaai-be/
├── mds/
│ ├── SESSION_HANDOFF.md ← BU DOSYA
│ └── SOCIAL_POSTER_SETUP.md ← social poster env+keys
├── ai-engine/
│ ├── reports/ ← BACKTEST CIKTILARI
│ │ ├── diagnostic_backtest_*.csv,json,txt
│ │ └── filter_optimization_patch.json
│ ├── validation_full.log ← validation backtest canlı log
│ ├── diagnostic_backtest_run.log ← önceki backtest log
│ ├── enrichment_run3.log ← enrichment koşma log
│ └── calibration_run.log ← calibrator retrain log
├── public/predictions/ ← render edilmiş social card PNG/JSON
└── src/scripts/ ← tüm yeni script'ler
```
---
## 🔑 Erişim bilgileri
### Pi sunucu (feeder + prod stack)
- **SSH**: `haruncan@95.70.252.214:2222`
- **Şifre**: `M594xH%$iM&4MM`
- **Plink kullan**: `~/plink.exe -ssh -P 2222 -pw '<password>' -hostkey 'SHA256:iq0YVI/4J897sf9dkksI7QzetpLCD0l57ZMX4UissI8' haruncan@95.70.252.214`
- **Docker**: `iddaai-be`, `iddaai-ai-engine`, `iddaai-fe`, `iddaai-postgres`, `iddaai-redis`, `gitea`
### DB (uzak Postgres @ Pi)
- **SSH tunnel function**: `iddaai-db` PowerShell fonksiyonu (yerel makinedeki profile'da kayıtlı)
- **Tunnel: localhost:5432 → Pi:5432**
- **Connection string**: `postgresql://iddaai_user:IddaA1_S4crET!@localhost:5432/iddaai_db?schema=public`
- **MCP**: Claude'un postgres MCP'si bu tunnel üzerinden çalışıyor (restricted mode, read-only)
---
## 📊 BACKTEST sonuçları geçmişi
### Backtest #1 — In-sample grid search (2026-05-11 → 05-24, 1000 maç)
- **CSV**: `ai-engine/reports/diagnostic_backtest_20260525_035649.csv`
- **TXT**: `ai-engine/reports/diagnostic_backtest_20260525_035649.txt`
- **Toplam playable**: 524 bet
- **Hit rate**: %54.77
- **ROI**: **%16.73** (baseline kötü)
- **Grid-search'ten çıkan optimal filtreler (in-sample)**:
- MS: edge [-5%, +15%], V27 AGREE zorunlu → +%8.23 (21 bet)
- OU25: odds ≥ 1.80, edge ≤ +15% → +%28.91 (20 bet)
- BTTS: tüm config'lerde kayıp → MUTE
- **Aggregate optimize**: 95 bet, ROI +%2.16 (in-sample)
### Backtest #2 — Validation (2026-05-01 → 05-14, KOŞUYOR)
- **Bitince konum**: `ai-engine/reports/diagnostic_backtest_<yeni_timestamp>.{csv,json,txt}`
- **Karşılaştırma çalıştır**: `python scripts/compare_backtests.py` (otomatik en yeni 2'yi alır)
- **Beklenen sonuç**: ROI ≥ 0 → out-of-sample doğrulama BAŞARILI; in-sample overfit değil
---
## ❓ Backtest BİTTİĞİNDE yapılacak (yeni session'da bu kısımdan başla)
### 1. Sonucu oku
```powershell
cd C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\ai-engine
Get-ChildItem reports\diagnostic_backtest_*.txt | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content
```
### 2. Karşılaştır
```powershell
python scripts\compare_backtests.py
```
Bu otomatik en yeni 2 backtest'i karşılaştırır, **VERDICT** verir:
- ✅ "FILTERS WORK" → ROI pozitif AND improved
- 🟡 "PARTIAL" → improved ama hâlâ negatif
- ❌ "OVERFITTING" → validation ROI collapse
### 3. Karara göre 2 yol
**Eğer ROI ≥ +%2 ve overfit yok:**
- `/sc:design` ile UI/API contract → Sprint 1
- Sprint 1: top-5 skor + evidence panel + "why" cümlesi
- Test edip prod'a aç
**Eğer ROI negatif veya overfit:**
- `analyze_backtest_csv.py` ile loss diagnostic
- Hangi market hâlâ kötü → tighten filter veya mute
- Calibrator recalibrate (özellikle BTTS dışındakiler için yeni sample)
- Tekrar backtest
---
## ⚠️ Bilinen açık problemler / sorular
1. **Coherence filter validate edilmedi production-side** — smoke test 20/20 ama gerçek production data ile karşılaştırma yok
2. **Lineup-overlap last-5 hesabı** — yazılmadı, requirements doc'ta F8 var
3. **Skor top-5 distribution** — Poisson zaten hesaplıyor, surface edilmedi (UI tarafı)
4. **"Why" cümlesi main_pick'te** — boş, doldurulması gerek
5. **Cards/Corners/RED CARD model** — yok, "henüz desteklenmiyor" placeholder ile bırak (kullanıcı onayladı: mevcut market'ler sağlamlaşsın)
6. **Orphan match_id 51 satır**`prediction_runs` içinde, `matches`'ta yok. Sample noise, geçiştirilebilir.
7. **opening_value feeder bug**`odds_movement_*` SQL yazıyor ama tüm değerler 0 (opening == closing). Feeder upstream sorun. Düşük öncelik.
---
## 🚦 Yeni Claude session'ında ilk komut
```
Bu projeye yeni bağlandım. Lütfen aşağıdaki dosyayı oku ve bana proje durumunu özet ver:
C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\mds\SESSION_HANDOFF.md
Sonra validation backtest'in sonucuna bak:
- C:\Users\fahri\OneDrive\المستندات\GitHub\iddaai\iddaai-be\ai-engine\reports\
içindeki en yeni diagnostic_backtest_*.txt dosyasını oku
- compare_backtests.py script'ini koş, verdict göster
- Verdict'e göre sonraki adımı öner
```
Buradan devam eder. Tüm context bu doc'ta + dosyalarda + DB'de.
---
## 🛠️ Requirements spec (sıkıştırılmış)
**Ürün**: UI-tetikli per-match analiz, bahis uzmanı seviyesi
**Trigger**: User tıklar, on-demand
**Output**: main_pick + value_pick + tüm market olasılıkları + tek HT/FT skoru + top-5 skor dağılımı + evidence panel
**Kapsam**: Mevcut market'ler sağlamlaştırılır, yeni market eklenmez (kullanıcı onayı)
**Quality bar**: Calibration sapması ±2-5pp per market, NaN yok, response <3sn
**Validation**: Out-of-sample backtest (1500 maç, May 1-14) — KOŞUYOR
---
**SON NOT**: Backtest'in TAMAMLANMASINI bekle (~22:00). Laptop'u kapatma. Bittiğinde OneDrive senkronize eder, başka makinede otomatik orada olur. Yeni session'da bu dosyayı oku, sonuçlara bak, devam et.
+212
View File
@@ -0,0 +1,212 @@
# Social Poster — Setup & Operations
Otomatik tahmin kartı üretip Twitter / Facebook / Instagram'a postlayan modül.
Cron her **10 dakikada bir** çalışır, **yaklaşan 10-60 dk içindeki maçları**
yakalar, AI Engine'den tahmin alır, 1080×1080 görsel üretir, caption üretir,
3 platforma post eder.
## 1) ENV değişkenleri
`.env`'ye ekle:
```bash
# Master switch — false ise cron çalışmaz, manual endpoint'ler de boş döner.
SOCIAL_POSTER_ENABLED=true
# Hangi sporlar (virgüllü liste). Varsayılan: football,basketball
SOCIAL_POSTER_SPORTS=football,basketball
# Yaklaşan maç penceresi (dakika). Varsayılan 10-60.
SOCIAL_POSTER_WINDOW_MIN=10
SOCIAL_POSTER_WINDOW_MAX=60
# Tek cron koşusunda kaç maç post edilir (rate-limit koruması). Varsayılan 5.
SOCIAL_POSTER_MAX_PER_RUN=5
# Public base URL — Instagram media upload için fotoğrafın HTTPS'ten erişilebilir
# olması ŞART. Localhost ile IG çalışmaz; production domain veya ngrok kullan.
APP_BASE_URL=https://api.iddaai.com
# AI Engine URL (orchestrator)
AI_ENGINE_URL=http://localhost:8000
# ─── Twitter / X ───
TWITTER_API_KEY=...
TWITTER_API_SECRET=...
TWITTER_ACCESS_TOKEN=...
TWITTER_ACCESS_SECRET=...
# ─── Meta (Facebook + Instagram) ───
META_PAGE_ACCESS_TOKEN=... # FB Page'in long-lived access token'ı
META_PAGE_ID=... # FB Page numeric ID
META_IG_USER_ID=... # IG Business account numeric ID
META_GRAPH_API_VERSION=v25.0 # opsiyonel
# ─── Caption AI (opsiyonel — yoksa template caption kullanılır) ───
ENABLE_GEMINI=true
GEMINI_API_KEY=...
GEMINI_MODEL=gemini-1.5-flash
# Veya local Ollama:
OLLAMA_BASE_URL=http://localhost:11434
SOCIAL_POSTER_OLLAMA_MODEL=llama3.1
```
## 2) API anahtarlarını alma
### Twitter / X
1. https://developer.x.com → Project + App oluştur
2. App'ın "Keys and tokens" → "API Key", "API Secret" al
3. User authentication settings → "Read and write and Direct Message"
4. "Access Token and Secret" generate et (bu hesap adına post eder)
5. **Free tier**: 1500 tweet/ay, 50 post/24 saat — 10 dk'lık cron'la günde
~144 koşu × 5 post = 720 potansiyel post → free tier yetmez, **Basic plan
($200/ay)** lazım. Cron interval'i 30 dk'ya alıp 50/gün kalmak istersen
`@Cron("*/30 * * * *")` olarak değiştir.
### Meta (Facebook + Instagram)
1. https://developers.facebook.com → App oluştur (type: Business)
2. Facebook Page bağla (mevcut sayfan yoksa oluştur)
3. Instagram Business hesabını Facebook Page'e bağla
4. Graph API Explorer'dan **page access token** al (User token değil!)
5. Long-lived token'a çevir (60 gün geçerli, refresh edilebilir)
6. **Page ID**: `https://graph.facebook.com/me/accounts?access_token=...`
7. **IG User ID**: `graph.facebook.com/{pageId}?fields=instagram_business_account&access_token=...`
8. Required permissions: `pages_show_list`, `pages_manage_posts`,
`pages_read_engagement`, `instagram_basic`, `instagram_content_publish`
### Gemini (caption AI — opsiyonel)
- https://aistudio.google.com → API key (free tier yeterli, günde ~1500 istek)
- `ENABLE_GEMINI=true` + `GEMINI_API_KEY=...`
- Gemini yoksa template caption kullanılır (yine SEO'lu, sadece daha statik)
## 3) Test komutları
```bash
# Servisi başlat
npm run start:dev
# Health endpoint — auth gerekmez
curl http://localhost:3005/social-poster/health | jq
# Manuel preview (görsel + JSON) — superadmin token gerekir
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:3005/social-poster/preview/<matchId>
# Görseli tarayıcıda direkt göster
open http://localhost:3005/social-poster/preview-png/<matchId>?token=$TOKEN
# Manuel post (tek maç, tüm platformlara) — superadmin token
curl -X POST -H "Authorization: Bearer $TOKEN" \
http://localhost:3005/social-poster/post/<matchId>
# Cron'u beklemeden full sweep koş — superadmin token
curl -X POST -H "Authorization: Bearer $TOKEN" \
http://localhost:3005/social-poster/run-now
```
## 4) SEO özellikleri
### Image dosya adı (SEO)
Eskiden: `prediction_basketball_xyz12345_1716595200000.jpg` (opaque)
Yeni: `sampiyonlar-ligi-unicaja-malaga-vs-aek-20260525.jpg` (Google indexable)
### Yan dosya: metadata sidecar
Her görsel için aynı dizinde `.json`:
- `title`, `description`, `og:*`, `schema.org SportsEvent`, `picks[]`
- Sayfada `<head>` Open Graph + Twitter Cards bu dosyadan beslenir
- Schema.org markup zengin sonuç (Google rich snippet) sağlar
### Caption (SEO + hashtags)
Her post 12'ye kadar küratör hashtag içerir:
- Marka: `#MaçTahmini #İddaa #BugünMaç`
- Spor: `#Futbol #Basketbol #FutbolTahmin`
- Lig: `#SüperLig #PremierLeague #ŞampiyonlarLigi #EuroLeague #NBA`
- Bölge: `#Türkiye #İngiltere #İspanya`
- Takım: `#Galatasaray #Fenerbahçe`
- Gün: `#PazarTahmini #CumartesiTahmini`
- Market: `#AltÜst #KGVar #ÇifteŞans #MaçSonucu #Handikap`
LLM (Gemini) caption üretiyorsa hashtag'leri çıkarır; sistem kendi
hashtag set'ini ekler. Tutarlı index için tek kaynak.
## 5) İzleme
```bash
# Health endpoint — periyodik monitor
curl http://localhost:3005/social-poster/health | jq
# Sample output:
{
"enabled": true,
"sports": ["football", "basketball"],
"window_min_minutes": 10,
"window_max_minutes": 60,
"max_posts_per_run": 5,
"top_leagues_loaded": 42,
"posted_match_count": 137,
"last_run_at": "2026-05-25T03:10:00.123Z",
"last_run_result": { "posted": 4, "skipped": 1, "errors": 0 },
"twitter_available": true,
"meta_facebook_available": true,
"meta_instagram_available": true,
"ai_engine_url": "http://localhost:8000",
"app_base_url": "https://api.iddaai.com"
}
```
`posted_match_count` `storage/social-poster-posted.json`'dan okunur, son 500
match ID hafızada — aynı maçı 2 kere post etmez.
## 6) Rate limit ipuçları
| Platform | Free limit | Tedbir |
|---|---|---|
| Twitter | 50 post/24 saat | `SOCIAL_POSTER_MAX_PER_RUN=2` + cron `*/30` → günde ~96 |
| Facebook | ~200 post/saat (Page) | Default config rahat |
| Instagram | 25 post/24 saat | `MAX_PER_RUN=1` + cron `*/60` → günde 24, sınırın hemen altında |
IG en sıkı sınır — production için **IG ayrı cron'da daha seyrek post**
yapılması önerilir (kod henüz tek cron, ileride ayrılabilir).
## 7) Hangi maçlar seçilir?
`top_leagues.json` dosyasındaki league_id'ler içinden:
- Şu anda 10-60 dakika sonra başlayacak
- Daha önce post edilmemiş
- `sport: football, basketball` filtresi geçen
`top_leagues.json` yoksa **tüm liglerden** maç seçer (hacmi yüksek tutar).
Sadece premium ligler postlamak istersen dosyayı doldur.
## 8) Görsel formatı
- **Boyut**: 1080×1080 (Instagram square — Twitter da kabul ediyor)
- **Format**: JPEG, quality 94
- **Tema**: Sport'a göre değişir — football yeşil, basketball turuncu
- **İçerik**: Lig logosu + ülke bayrağı, takım logoları + adları, HT skor,
FT skor, top 3 tahmin (confidence ile), risk badge
Card layout `image-renderer.service.ts` içinde — value-pick yıldız ile
işaretli, scenario top 3 listelenir, footer alt'ta tarih + brand.
## 9) Sık sorular
**Q: Görseller nereye yazılıyor?**
`public/predictions/` (gitignored). ServeStatic ile `/predictions/<file>.jpg`
URL'inden erişilir.
**Q: Eski görseller temizleniyor mu?**
Hayır — manuel temizlik gerekir. Cron eklemek istersen `LimitResetterTask`
örneği var.
**Q: AI Engine çalışmıyorsa ne olur?**
Cron tahmin alamaz, log'a hata düşer, devam eder. Sonraki koşuda dener.
**Q: Bir maç 2 kere post ediliyor mu?**
Hayır — `postedMatchIds` set'i Match ID bazında dedup yapar, dosyaya yazılır
(restart-safe).
**Q: Caption Gemini olmadan ne kadar iyi?**
Template caption tüm bilgileri + 12 hashtag içerir. SEO açısından yeterli,
sadece anlatım daha statik. Gemini ile her post için özgün metin.
@@ -0,0 +1,72 @@
# Gereksinim Keşfi: Lig Etiketleme + Milli Takım / Dünya Kupası Desteği
> `/sc:brainstorm` çıktısı — REQUIREMENTS ONLY. Tasarım/kod sonraki adım (/sc:design, /sc:workflow).
> Tarih: 2026-06 · Kaynak: 10k backtest + canlı DB/API kanıtı.
## Doğrulanmış Gerçekler (kanıta dayalı, varsayım değil)
### Lig performansı
- 10k backtest 18 ligde BET üretti; ROI dağılımı: ~7 güçlü kârlı (+25%..+102%),
~4 başabaş, ~7 zararlı (12%..62%).
- `live_matches` distinct lig ≠ `qualified_leagues.json` (48 lig). live_matches'te
qualified olmayan ligler var → kullanıcının "gereksiz ligler" sezgisi DOĞRU.
- Lig isimleri backtest CSV'de boş; DB'den `leagues` tablosundan çözülür.
### Milli takım / Dünya Kupası (ÖNEMLİ — ilk hipotez ÇÜRÜDÜ)
- Milli takımlar DB'de VAR: Türkiye(9s2kpeunkes0g17l95r3t91j6, elo 1675),
Almanya(3l2t2db0c5ow2f7s7bhr6mij4, 1689), Kolombiya(1692), Andorra(1243).
ELO + matches_played (30-37) MEVCUT.
- Milli maç hacmi yüksek: DK Elemeler 645, Hazırlık Maçları Ülkeler 522,
Uluslar Ligi 148, Avrupa Şamp. Elemeleri 120 bitmiş maç. Ayrı ligler halinde.
- football_ai_features milli maçlar için %98 üretiliyor (196/200) — kulüpten yüksek.
- **KÖK SEBEP (canlı API ile kanıtlandı):** Sistem milli maçı tahmin EDİYOR
(MS olasılıkları + 9-10 market geliyor, data_quality MEDIUM 0.57-0.74). Sorun:
`betting_brain approved=0` — hiçbir market "oynanabilir" işaretlenmiyor. Ortak
flag `ai_features_inferred_from_history` → data_quality MEDIUM tavanı (0.74) +
lig qualified değil → brain eşikleri geçilemiyor. Yani "yetersiz veri" mesajı
aslında "brain güvenmiyor, BET yok" demek. Model/veri sorunu DEĞİL, gate/tuning sorunu.
## Kullanıcı Kararları (bu oturumda alındı)
- Lig filtresi: **"Hepsi görünsün ama etiketli"** (gizleme yok; güven rozeti: Yüksek/Orta/Düşük).
- Milli takım: başta "ayrı model" istendi; veri görülünce yön = mevcut motoru milli
maçlara uyarlamak (ayrı ML modeli gereksiz — ELO+feature+geçmiş zaten var).
- Öncelik: **önce lig etiketleme** (hazır veri), sonra milli takım.
- Dünya Kupası: hazırlık maçlarında test edilebilmeli (yakın takvim baskısı).
## Fonksiyonel Gereksinimler
### A. Lig Güven Etiketleme
- FR-A1: Her lig için backtest'e dayalı güven seviyesi (Yüksek/Orta/Düşük) hesaplanmalı
(metrik: BET ROI + örneklem sayısı; düşük örneklem = otomatik Düşük/Bilinmiyor).
- FR-A2: live_matches'teki maçlar lig güven rozetiyle gösterilmeli (gizlenmeden).
- FR-A3: Etiket kaynağı tek yerde (config/tablo) tutulmalı, backtest tazelendikçe güncellenebilmeli.
- FR-A4: Forward-test (Model Performansı) verisi biriktikçe etiketler canlı sonuçla doğrulanmalı.
### B. Milli Takım / Dünya Kupası Desteği
- FR-B1: Milli maçlarda da BET önerisi çıkabilmeli (şu an approved=0).
- FR-B2: `ai_features_inferred_from_history` flag'i olan milli maçlar için data_quality
tavanı / brain eşikleri milli-maça uygun kalibre edilmeli (kör gevşetme DEĞİL).
- FR-B3: Milli maç ligleri (DK Elemeler, Hazırlık Maçları Ülkeler, Uluslar Ligi, Avrupa
Şamp., Dünya Kupası) "tanınan" kapsama alınmalı (qualified benzeri).
- FR-B4: Hazırlık maçlarında uçtan uca test edilebilmeli (tahmin + forward-test kaydı).
- FR-B5: Milli maç kalibrasyonu ayrı izlenmeli (kulüple karışmasın) — Model Performansı
sayfasında "milli" kırılımı.
## Fonksiyonel Olmayan Gereksinimler
- NFR-1: Gerçek para — milli maç eşik değişiklikleri backtest/forward-test ile doğrulanmadan
canlı agresifleştirilmemeli.
- NFR-2: Lig etiketleme mevcut hacmi düşürmemeli (gizleme değil işaretleme).
- NFR-3: Değişiklikler additive; mevcut kulüp tahmin akışını bozmamalı.
## Açık Sorular (sonraki tasarım turunda netleşecek)
- OQ-1: Lig güven eşikleri tam olarak ne? (örn. Yüksek = ROI>+10% & N≥30 BET)
- OQ-2: Milli maçlar için brain eşiği nasıl ayarlanacak — ayrı tier mi, data_quality
flag istisnası mı? Önce backtest: milli maçlarda mevcut motor kaç BET/ne ROI verirdi
(eşik gevşetilse)? Bu ölçülmeden tuning yapılmamalı.
- OQ-3: Etiket nerede saklanacak: yeni tablo mı, mevcut league_tiers mı, config mi?
- OQ-4: Dünya Kupası grup maçlarında lineup geç gelir — probable_xi cezası milli maçta
nasıl ele alınacak?
## Sonraki Adım
1. (Önce) Lig güven etiketleme → /sc:design veya doğrudan uygulama (veri hazır).
2. (Sonra) Milli maç backtest'i: eşik gevşetildiğinde milli maçlarda ROI ne? → ona göre tuning.
+70
View File
@@ -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ı).
+417
View File
@@ -27,6 +27,7 @@
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^6.19.3",
"@resvg/resvg-js": "^2.6.2",
"axios": "^1.13.6",
"bcrypt": "^6.0.0",
"bullmq": "^5.66.4",
@@ -49,6 +50,8 @@
"prisma": "^6.19.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"satori": "^0.26.0",
"satori-html": "^0.3.2",
"twitter-api-v2": "^1.29.0",
"zod": "^4.3.5"
},
@@ -3912,12 +3915,243 @@
"@redis/client": "^1.0.0"
}
},
"node_modules/@resvg/resvg-js": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js/-/resvg-js-2.6.2.tgz",
"integrity": "sha512-xBaJish5OeGmniDj9cW5PRa/PtmuVU3ziqrbr5xJj901ZDN4TosrVaNZpEiLZAxdfnhAe7uQ7QFWfjPe9d9K2Q==",
"license": "MPL-2.0",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
"@resvg/resvg-js-android-arm-eabi": "2.6.2",
"@resvg/resvg-js-android-arm64": "2.6.2",
"@resvg/resvg-js-darwin-arm64": "2.6.2",
"@resvg/resvg-js-darwin-x64": "2.6.2",
"@resvg/resvg-js-linux-arm-gnueabihf": "2.6.2",
"@resvg/resvg-js-linux-arm64-gnu": "2.6.2",
"@resvg/resvg-js-linux-arm64-musl": "2.6.2",
"@resvg/resvg-js-linux-x64-gnu": "2.6.2",
"@resvg/resvg-js-linux-x64-musl": "2.6.2",
"@resvg/resvg-js-win32-arm64-msvc": "2.6.2",
"@resvg/resvg-js-win32-ia32-msvc": "2.6.2",
"@resvg/resvg-js-win32-x64-msvc": "2.6.2"
}
},
"node_modules/@resvg/resvg-js-android-arm-eabi": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm-eabi/-/resvg-js-android-arm-eabi-2.6.2.tgz",
"integrity": "sha512-FrJibrAk6v29eabIPgcTUMPXiEz8ssrAk7TXxsiZzww9UTQ1Z5KAbFJs+Z0Ez+VZTYgnE5IQJqBcoSiMebtPHA==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-android-arm64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-android-arm64/-/resvg-js-android-arm64-2.6.2.tgz",
"integrity": "sha512-VcOKezEhm2VqzXpcIJoITuvUS/fcjIw5NA/w3tjzWyzmvoCdd+QXIqy3FBGulWdClvp4g+IfUemigrkLThSjAQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-darwin-arm64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-arm64/-/resvg-js-darwin-arm64-2.6.2.tgz",
"integrity": "sha512-nmok2LnAd6nLUKI16aEB9ydMC6Lidiiq2m1nEBDR1LaaP7FGs4AJ90qDraxX+CWlVuRlvNjyYJTNv8qFjtL9+A==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-darwin-x64": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-darwin-x64/-/resvg-js-darwin-x64-2.6.2.tgz",
"integrity": "sha512-GInyZLjgWDfsVT6+SHxQVRwNzV0AuA1uqGsOAW+0th56J7Nh6bHHKXHBWzUrihxMetcFDmQMAX1tZ1fZDYSRsw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm-gnueabihf": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm-gnueabihf/-/resvg-js-linux-arm-gnueabihf-2.6.2.tgz",
"integrity": "sha512-YIV3u/R9zJbpqTTNwTZM5/ocWetDKGsro0SWp70eGEM9eV2MerWyBRZnQIgzU3YBnSBQ1RcxRZvY/UxwESfZIw==",
"cpu": [
"arm"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm64-gnu": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-gnu/-/resvg-js-linux-arm64-gnu-2.6.2.tgz",
"integrity": "sha512-zc2BlJSim7YR4FZDQ8OUoJg5holYzdiYMeobb9pJuGDidGL9KZUv7SbiD4E8oZogtYY42UZEap7dqkkYuA91pg==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-arm64-musl": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-arm64-musl/-/resvg-js-linux-arm64-musl-2.6.2.tgz",
"integrity": "sha512-3h3dLPWNgSsD4lQBJPb4f+kvdOSJHa5PjTYVsWHxLUzH4IFTJUAnmuWpw4KqyQ3NA5QCyhw4TWgxk3jRkQxEKg==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-x64-gnu": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-gnu/-/resvg-js-linux-x64-gnu-2.6.2.tgz",
"integrity": "sha512-IVUe+ckIerA7xMZ50duAZzwf1U7khQe2E0QpUxu5MBJNao5RqC0zwV/Zm965vw6D3gGFUl7j4m+oJjubBVoftw==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-linux-x64-musl": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-linux-x64-musl/-/resvg-js-linux-x64-musl-2.6.2.tgz",
"integrity": "sha512-UOf83vqTzoYQO9SZ0fPl2ZIFtNIz/Rr/y+7X8XRX1ZnBYsQ/tTb+cj9TE+KHOdmlTFBxhYzVkP2lRByCzqi4jQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-arm64-msvc": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-arm64-msvc/-/resvg-js-win32-arm64-msvc-2.6.2.tgz",
"integrity": "sha512-7C/RSgCa+7vqZ7qAbItfiaAWhyRSoD4l4BQAbVDqRRsRgY+S+hgS3in0Rxr7IorKUpGE69X48q6/nOAuTJQxeQ==",
"cpu": [
"arm64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-ia32-msvc": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-ia32-msvc/-/resvg-js-win32-ia32-msvc-2.6.2.tgz",
"integrity": "sha512-har4aPAlvjnLcil40AC77YDIk6loMawuJwFINEM7n0pZviwMkMvjb2W5ZirsNOZY4aDbo5tLx0wNMREp5Brk+w==",
"cpu": [
"ia32"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@resvg/resvg-js-win32-x64-msvc": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/@resvg/resvg-js-win32-x64-msvc/-/resvg-js-win32-x64-msvc-2.6.2.tgz",
"integrity": "sha512-ZXtYhtUr5SSaBrUDq7DiyjOFJqBVL/dOBN7N/qmi/pO0IgiWW/f/ue3nbvu9joWE5aAKDoIzy/CxsY0suwGosQ==",
"cpu": [
"x64"
],
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true
},
"node_modules/@shuding/opentype.js": {
"version": "1.4.0-beta.0",
"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
"integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
"license": "MIT",
"dependencies": {
"fflate": "^0.7.3",
"string.prototype.codepointat": "^0.2.1"
},
"bin": {
"ot": "bin/ot"
},
"engines": {
"node": ">= 8.0.0"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.34.47",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz",
@@ -6535,6 +6769,15 @@
"node": ">=6"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001762",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz",
@@ -7086,6 +7329,36 @@
"node": ">= 8"
}
},
"node_modules/css-background-parser": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
"integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==",
"license": "MIT"
},
"node_modules/css-box-shadow": {
"version": "1.0.0-3",
"resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
"integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==",
"license": "MIT"
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/css-gradient-parser": {
"version": "0.0.17",
"resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz",
"integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -7102,6 +7375,17 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/css-what": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
@@ -7433,6 +7717,15 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
},
"node_modules/emoji-regex-xs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz",
"integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/empathic": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz",
@@ -8152,6 +8445,12 @@
"node": "^12.20 || >= 14.13"
}
},
"node_modules/fflate": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz",
"integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==",
"license": "MIT"
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -8794,6 +9093,18 @@
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"dev": true
},
"node_modules/hex-rgb": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
"integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/hookified": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.0.tgz",
@@ -10097,6 +10408,25 @@
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.33.tgz",
"integrity": "sha512-r9kw4OA6oDO4dPXkOrXTkArQAafIKAU71hChInV4FxZ69dxCfbwQGDPzqR5/vea94wU705/3AZroEbSoeVWrQw=="
},
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -11030,6 +11360,12 @@
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
},
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -11042,6 +11378,16 @@
"node": ">=6"
}
},
"node_modules/parse-css-color": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
"integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.1.4",
"hex-rgb": "^4.1.0"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
@@ -11438,6 +11784,12 @@
"node": ">=4"
}
},
"node_modules/postcss-value-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -12012,6 +12364,37 @@
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/satori": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/satori/-/satori-0.26.0.tgz",
"integrity": "sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA==",
"license": "MPL-2.0",
"dependencies": {
"@shuding/opentype.js": "1.4.0-beta.0",
"css-background-parser": "^0.1.0",
"css-box-shadow": "1.0.0-3",
"css-gradient-parser": "^0.0.17",
"css-to-react-native": "^3.0.0",
"emoji-regex-xs": "^2.0.1",
"escape-html": "^1.0.3",
"linebreak": "^1.1.0",
"parse-css-color": "^0.2.1",
"postcss-value-parser": "^4.2.0",
"yoga-layout": "^3.2.1"
},
"engines": {
"node": ">=16"
}
},
"node_modules/satori-html": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/satori-html/-/satori-html-0.3.2.tgz",
"integrity": "sha512-wjTh14iqADFKDK80e51/98MplTGfxz2RmIzh0GqShlf4a67+BooLywF17TvJPD6phO0Hxm7Mf1N5LtRYvdkYRA==",
"license": "MIT",
"dependencies": {
"ultrahtml": "^1.2.0"
}
},
"node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
@@ -12488,6 +12871,12 @@
"node": ">=8"
}
},
"node_modules/string.prototype.codepointat": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==",
"license": "MIT"
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -12858,6 +13247,12 @@
"real-require": "^0.2.0"
}
},
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tinyexec": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
@@ -13260,6 +13655,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ultrahtml": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz",
"integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==",
"license": "MIT"
},
"node_modules/undici": {
"version": "7.18.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.18.2.tgz",
@@ -13274,6 +13675,16 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -13858,6 +14269,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yoga-layout": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz",
"integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==",
"license": "MIT"
},
"node_modules/zod": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz",
+4 -1
View File
@@ -60,6 +60,7 @@
"@nestjs/terminus": "^11.0.0",
"@nestjs/throttler": "^6.5.0",
"@prisma/client": "^6.19.3",
"@resvg/resvg-js": "^2.6.2",
"axios": "^1.13.6",
"bcrypt": "^6.0.0",
"bullmq": "^5.66.4",
@@ -82,6 +83,8 @@
"prisma": "^6.19.3",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"satori": "^0.26.0",
"satori-html": "^0.3.2",
"twitter-api-v2": "^1.29.0",
"zod": "^4.3.5"
},
@@ -134,4 +137,4 @@
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
}
-17
View File
@@ -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}")
-153
View File
@@ -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
View File
@@ -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"
]
-74
View File
@@ -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();
-51
View File
@@ -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();
-100
View File
@@ -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();
-63
View File
@@ -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();
-170
View File
@@ -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()
-153
View File
@@ -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);
+20
View File
@@ -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}'
+2
View File
@@ -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,
+35
View File
@@ -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}`;
}
-59
View File
@@ -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;
}
}
}
+198
View File
@@ -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 (0100)
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) {
+16 -11
View File
@@ -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,
})),
+16
View File
@@ -111,6 +111,22 @@ export class MatchesController {
return this.matchesService.getActiveLeagues(sport || Sport.FOOTBALL);
}
/**
* GET /matches/:id/odds-movement
* Openingclosing 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
+51 -10
View File
@@ -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 (openingclosing) 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 {
+14
View File
@@ -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"],
+94 -1
View File
@@ -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,
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><circle fill="#F4900C" cx="18" cy="18" r="18"/><path fill="#231F20" d="M36 17h-8.981c.188-5.506 1.943-9.295 4.784-10.546-.445-.531-.926-1.027-1.428-1.504-2.83 1.578-5.145 5.273-5.354 12.049H19V0h-2v17h-6.021c-.208-6.776-2.523-10.471-5.353-12.049-.502.476-.984.972-1.428 1.503C7.039 7.705 8.793 11.494 8.981 17H0v2h8.981c-.188 5.506-1.942 9.295-4.783 10.546.445.531.926 1.027 1.428 1.504 2.831-1.578 5.145-5.273 5.353-12.05H17v17h2V19h6.021c.209 6.776 2.523 10.471 5.354 12.05.502-.476.984-.973 1.428-1.504-2.841-1.251-4.595-5.04-4.784-10.546H36v-2z"/></svg>

After

Width:  |  Height:  |  Size: 617 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#FFCD05" d="M0 27c0 2.209 1.791 4 4 4h28c2.209 0 4-1.791 4-4v-4H0v4z"/><path fill="#ED1F24" d="M0 14h36v9H0z"/><path fill="#141414" d="M32 5H4C1.791 5 0 6.791 0 9v5h36V9c0-2.209-1.791-4-4-4z"/></svg>

After

Width:  |  Height:  |  Size: 271 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#C60A1D" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V9c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v18z"/><path fill="#FFC400" d="M0 12h36v12H0z"/><path fill="#EA596E" d="M9 17v3c0 1.657 1.343 3 3 3s3-1.343 3-3v-3H9z"/><path fill="#F4A2B2" d="M12 16h3v3h-3z"/><path fill="#DD2E44" d="M9 16h3v3H9z"/><ellipse fill="#EA596E" cx="12" cy="14.5" rx="3" ry="1.5"/><ellipse fill="#FFAC33" cx="12" cy="13.75" rx="3" ry=".75"/><path fill="#99AAB5" d="M7 16h1v7H7zm9 0h1v7h-1z"/><path fill="#66757F" d="M6 22h3v1H6zm9 0h3v1h-3zm-8-7h1v1H7zm9 0h1v1h-1z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#039" d="M32 5H4C1.791 5 0 6.791 0 9v18c0 2.209 1.791 4 4 4h28c2.209 0 4-1.791 4-4V9c0-2.209-1.791-4-4-4z"/><path d="M18.539 9.705l.849-.617h-1.049l-.325-.998-.324.998h-1.049l.849.617-.325.998.849-.617.849.617zm0 17.333l.849-.617h-1.049l-.325-.998-.324.998h-1.049l.849.617-.325.998.849-.617.849.617zm-8.666-8.667l.849-.617h-1.05l-.324-.998-.325.998H7.974l.849.617-.324.998.849-.617.849.617zm1.107-4.285l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.617.849.617zm0 8.619l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.617.849.617zm3.226-11.839l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.617.849.617zm0 15.067l.849-.617h-1.05l-.324-.998-.324.998h-1.05l.849.617-.324.998.849-.616.849.616zm11.921-7.562l-.849-.617h1.05l.324-.998.325.998h1.049l-.849.617.324.998-.849-.617-.849.617zm-1.107-4.285l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.617-.849.617zm0 8.619l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.617-.849.617zm-3.226-11.839l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.617-.849.617zm0 15.067l-.849-.617h1.05l.324-.998.324.998h1.05l-.849.617.324.998-.849-.616-.849.616z" fill="#FC0"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#ED2939" d="M36 27c0 2.209-1.791 4-4 4h-8V5h8c2.209 0 4 1.791 4 4v18z"/><path fill="#002495" d="M4 5C1.791 5 0 6.791 0 9v18c0 2.209 1.791 4 4 4h8V5H4z"/><path fill="#EEE" d="M12 5h12v26H12z"/></svg>

After

Width:  |  Height:  |  Size: 270 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#00247D" d="M0 9.059V13h5.628zM4.664 31H13v-5.837zM23 25.164V31h8.335zM0 23v3.941L5.63 23zM31.337 5H23v5.837zM36 26.942V23h-5.631zM36 13V9.059L30.371 13zM13 5H4.664L13 10.837z"/><path fill="#CF1B2B" d="M25.14 23l9.712 6.801c.471-.479.808-1.082.99-1.749L28.627 23H25.14zM13 23h-2.141l-9.711 6.8c.521.53 1.189.909 1.938 1.085L13 23.943V23zm10-10h2.141l9.711-6.8c-.521-.53-1.188-.909-1.937-1.085L23 12.057V13zm-12.141 0L1.148 6.2C.677 6.68.34 7.282.157 7.949L7.372 13h3.487z"/><path fill="#EEE" d="M36 21H21v10h2v-5.836L31.335 31H32c1.117 0 2.126-.461 2.852-1.199L25.14 23h3.487l7.215 5.052c.093-.337.158-.686.158-1.052v-.058L30.369 23H36v-2zM0 21v2h5.63L0 26.941V27c0 1.091.439 2.078 1.148 2.8l9.711-6.8H13v.943l-9.914 6.941c.294.07.598.116.914.116h.664L13 25.163V31h2V21H0zM36 9c0-1.091-.439-2.078-1.148-2.8L25.141 13H23v-.943l9.915-6.942C32.62 5.046 32.316 5 32 5h-.663L23 10.837V5h-2v10h15v-2h-5.629L36 9.059V9zM13 5v5.837L4.664 5H4c-1.118 0-2.126.461-2.852 1.2l9.711 6.8H7.372L.157 7.949C.065 8.286 0 8.634 0 9v.059L5.628 13H0v2h15V5h-2z"/><path fill="#CF1B2B" d="M21 15V5h-6v10H0v6h15v10h6V21h15v-6z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#CE2B37" d="M36 27c0 2.209-1.791 4-4 4h-8V5h8c2.209 0 4 1.791 4 4v18z"/><path fill="#009246" d="M4 5C1.791 5 0 6.791 0 9v18c0 2.209 1.791 4 4 4h8V5H4z"/><path fill="#EEE" d="M12 5h12v26H12z"/></svg>

After

Width:  |  Height:  |  Size: 270 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36"><path fill="#E30917" d="M36 27c0 2.209-1.791 4-4 4H4c-2.209 0-4-1.791-4-4V9c0-2.209 1.791-4 4-4h28c2.209 0 4 1.791 4 4v18z"/><path fill="#EEE" d="M16 24c-3.314 0-6-2.685-6-6 0-3.314 2.686-6 6-6 1.31 0 2.52.425 3.507 1.138-1.348-1.524-3.312-2.491-5.507-2.491-4.061 0-7.353 3.292-7.353 7.353 0 4.062 3.292 7.354 7.353 7.354 2.195 0 4.16-.967 5.507-2.492C18.521 23.575 17.312 24 16 24zm3.913-5.77l2.44.562.22 2.493 1.288-2.146 2.44.561-1.644-1.888 1.287-2.147-2.303.98-1.644-1.889.22 2.494z"/></svg>

After

Width:  |  Height:  |  Size: 556 B

Some files were not shown because too many files have changed in this diff Show More