@@ -0,0 +1,352 @@
|
||||
"""
|
||||
Tutarsızlık Bazlı Backtest
|
||||
============================
|
||||
Modeller arası tutarsızlığı ölçer, tutarlı maçlarda bahis açılsaydı
|
||||
ROI ne olurdu hesaplar.
|
||||
|
||||
Mantık:
|
||||
- Her maç için market'ler arası çelişkileri tespit et
|
||||
- Tutarsız maçları filtrele
|
||||
- Tutarlı maçlarda hit rate ve ROI hesapla
|
||||
|
||||
Usage:
|
||||
python scripts/backtest_consistency.py
|
||||
"""
|
||||
|
||||
import os, sys, json
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import xgboost as xgb
|
||||
from sklearn.metrics import accuracy_score
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
DATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
'data', 'training_data.csv')
|
||||
MODELS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
'models', 'v25')
|
||||
|
||||
SKIP_COLS = {
|
||||
'match_id','home_team_id','away_team_id','league_id','mst_utc',
|
||||
'score_home','score_away','total_goals','ht_score_home','ht_score_away','ht_total_goals',
|
||||
'label_ms','label_ou05','label_ou15','label_ou25','label_ou35','label_btts',
|
||||
'label_ht_result','label_ht_ou05','label_ht_ou15','label_ht_ft',
|
||||
'label_odd_even','label_yellow_cards','label_cards_ou45','label_handicap_ms',
|
||||
}
|
||||
|
||||
|
||||
def load_model(market: str):
|
||||
path = os.path.join(MODELS_DIR, f'xgb_v25_{market}.json')
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
b = xgb.Booster()
|
||||
b.load_model(path)
|
||||
return b
|
||||
|
||||
|
||||
def predict_proba(model, X: np.ndarray, feature_cols: list, n_class: int):
|
||||
dmat = xgb.DMatrix(pd.DataFrame(X, columns=feature_cols))
|
||||
raw = model.predict(dmat)
|
||||
if n_class > 2:
|
||||
return raw.reshape(-1, n_class)
|
||||
return np.column_stack([1 - raw, raw])
|
||||
|
||||
|
||||
def consistency_score(probs: dict) -> tuple[float, list]:
|
||||
"""
|
||||
Market'ler arası tutarsızlığı hesapla.
|
||||
0 = tamamen tutarlı, 1 = tamamen çelişkili.
|
||||
|
||||
Kontrol edilen çelişkiler:
|
||||
1. OU15 üst yüksek ama OU25 üst de yüksek → ok
|
||||
OU15 üst yüksek ama OU25 alt yüksek → ÇELISKI (1 gol bekleniyor ama 2.5+ da bekleniyor?)
|
||||
|
||||
2. HT_OU05 üst yüksek ama HT sonucu draw yüksek → ÇELISKI
|
||||
|
||||
3. OU35 üst yüksek ama BTTS düşük → şüpheli
|
||||
|
||||
4. MS home yüksek ama HT away yüksek → çelişkili
|
||||
"""
|
||||
conflicts = []
|
||||
total_weight = 0
|
||||
total_conflict = 0
|
||||
|
||||
# OU tutarlılığı: P(OU25>0.5) <= P(OU15>0.5) matematiksel zorunluluk
|
||||
ou15_over = probs.get('ou15_over', 0.5)
|
||||
ou25_over = probs.get('ou25_over', 0.5)
|
||||
ou35_over = probs.get('ou35_over', 0.5)
|
||||
|
||||
# OU hiyerarşisi: ou35 <= ou25 <= ou15 olmalı
|
||||
if ou25_over > ou15_over + 0.05:
|
||||
gap = ou25_over - ou15_over
|
||||
conflicts.append(f'OU25>{ou25_over:.0%} > OU15>{ou15_over:.0%} (imkansız)')
|
||||
total_conflict += gap * 2
|
||||
total_weight += 1
|
||||
|
||||
if ou35_over > ou25_over + 0.05:
|
||||
gap = ou35_over - ou25_over
|
||||
conflicts.append(f'OU35>{ou35_over:.0%} > OU25>{ou25_over:.0%} (imkansız)')
|
||||
total_conflict += gap * 2
|
||||
total_weight += 1
|
||||
|
||||
# HT_OU05 ve HT sonuç tutarlılığı
|
||||
ht_ou05_over = probs.get('ht_ou05_over', 0.5)
|
||||
ht_draw_prob = probs.get('ht_draw', 0.34)
|
||||
|
||||
# İlk yarıda gol bekleniyor ama beraberlik de bekleniyor (0-0 draw?)
|
||||
# HT_OU05 >%70 ama HT draw >%50 → çelişkili (0-0 berabere çok?)
|
||||
if ht_ou05_over > 0.70 and ht_draw_prob > 0.50:
|
||||
conflict = min(ht_ou05_over - 0.5, ht_draw_prob - 0.4)
|
||||
conflicts.append(f'HT_OU05>{ht_ou05_over:.0%} ama HT_Draw>{ht_draw_prob:.0%}')
|
||||
total_conflict += conflict
|
||||
total_weight += 1
|
||||
|
||||
# HT_OU05 ve HT_OU15 tutarlılığı
|
||||
ht_ou15_over = probs.get('ht_ou15_over', 0.3)
|
||||
if ht_ou15_over > ht_ou05_over + 0.05:
|
||||
gap = ht_ou15_over - ht_ou05_over
|
||||
conflicts.append(f'HT_OU15>{ht_ou15_over:.0%} > HT_OU05>{ht_ou05_over:.0%} (imkansız)')
|
||||
total_conflict += gap * 2
|
||||
total_weight += 1
|
||||
|
||||
# MS ve OU tutarlılığı
|
||||
ms_home = probs.get('ms_home', 0.33)
|
||||
ms_away = probs.get('ms_away', 0.33)
|
||||
btts_yes = probs.get('btts_yes', 0.5)
|
||||
|
||||
# Tek takım galibiyeti kuvvetli ama BTTS yüksek → şüpheli
|
||||
dominant = max(ms_home, ms_away)
|
||||
if dominant > 0.65 and btts_yes > 0.65:
|
||||
conflict = (dominant - 0.5) * (btts_yes - 0.5)
|
||||
conflicts.append(f'MS dominant>{dominant:.0%} ama BTTS_Yes>{btts_yes:.0%}')
|
||||
total_conflict += conflict * 0.5
|
||||
total_weight += 1
|
||||
|
||||
# OU25 ve BTTS tutarlılığı
|
||||
# BTTS yüksekse en az 2 gol → OU25 üst de yüksek olmalı
|
||||
if btts_yes > 0.65 and ou25_over < 0.45:
|
||||
conflict = btts_yes - ou25_over
|
||||
conflicts.append(f'BTTS_Yes>{btts_yes:.0%} ama OU25>{ou25_over:.0%} düşük')
|
||||
total_conflict += conflict
|
||||
total_weight += 1
|
||||
|
||||
# OU35 üst yüksek ama BTTS düşük → şüpheli (3+ gol ama tek takım mı?)
|
||||
if ou35_over > 0.45 and btts_yes < 0.40:
|
||||
conflict = (ou35_over - 0.35) * (0.5 - btts_yes)
|
||||
conflicts.append(f'OU35>{ou35_over:.0%} ama BTTS_Yes<{btts_yes:.0%}')
|
||||
total_conflict += conflict
|
||||
total_weight += 1
|
||||
|
||||
score = min(1.0, total_conflict / max(total_weight * 0.3, 0.1))
|
||||
return score, conflicts
|
||||
|
||||
|
||||
def main():
|
||||
print('Loading data...')
|
||||
df = pd.read_csv(DATA_PATH, low_memory=False)
|
||||
|
||||
# Son %20 = test seti (kronolojik)
|
||||
df = df.sort_values('mst_utc')
|
||||
n_test = int(len(df) * 0.20)
|
||||
df_test = df.tail(n_test).copy()
|
||||
print(f'Test seti: {len(df_test):,} maç')
|
||||
|
||||
feature_cols = [c for c in df.columns if c not in SKIP_COLS]
|
||||
|
||||
# Modelleri yükle
|
||||
print('Modeller yükleniyor...')
|
||||
models = {
|
||||
'ms': (load_model('ms'), 3),
|
||||
'ou15': (load_model('ou15'), 2),
|
||||
'ou25': (load_model('ou25'), 2),
|
||||
'ou35': (load_model('ou35'), 2),
|
||||
'btts': (load_model('btts'), 2),
|
||||
'ht_result':(load_model('ht_result'), 3),
|
||||
'ht_ou05': (load_model('ht_ou05'), 2),
|
||||
'ht_ou15': (load_model('ht_ou15'), 2),
|
||||
}
|
||||
models = {k: v for k, v in models.items() if v[0] is not None}
|
||||
print(f'Yüklenen model: {list(models.keys())}')
|
||||
|
||||
X = df_test[feature_cols].fillna(0).values
|
||||
|
||||
# Tüm tahminleri al
|
||||
print('Tahminler yapılıyor...')
|
||||
preds = {}
|
||||
for mkey, (model, n_class) in models.items():
|
||||
p = predict_proba(model, X, feature_cols, n_class)
|
||||
preds[mkey] = p
|
||||
|
||||
# Her maç için tutarsızlık skoru ve tahmin kararı
|
||||
results = []
|
||||
for i in range(len(df_test)):
|
||||
row = df_test.iloc[i]
|
||||
|
||||
# Olasılıkları topla
|
||||
probs = {}
|
||||
if 'ms' in preds:
|
||||
probs['ms_home'] = preds['ms'][i][0]
|
||||
probs['ms_draw'] = preds['ms'][i][1]
|
||||
probs['ms_away'] = preds['ms'][i][2]
|
||||
if 'ou15' in preds:
|
||||
probs['ou15_over'] = preds['ou15'][i][1]
|
||||
if 'ou25' in preds:
|
||||
probs['ou25_over'] = preds['ou25'][i][1]
|
||||
if 'ou35' in preds:
|
||||
probs['ou35_over'] = preds['ou35'][i][1]
|
||||
if 'btts' in preds:
|
||||
probs['btts_yes'] = preds['btts'][i][1]
|
||||
if 'ht_result' in preds:
|
||||
probs['ht_home'] = preds['ht_result'][i][0]
|
||||
probs['ht_draw'] = preds['ht_result'][i][1]
|
||||
probs['ht_away'] = preds['ht_result'][i][2]
|
||||
if 'ht_ou05' in preds:
|
||||
probs['ht_ou05_over'] = preds['ht_ou05'][i][1]
|
||||
if 'ht_ou15' in preds:
|
||||
probs['ht_ou15_over'] = preds['ht_ou15'][i][1]
|
||||
|
||||
c_score, conflicts = consistency_score(probs)
|
||||
|
||||
# Gerçek sonuçlar
|
||||
actual = {
|
||||
'ms': int(row.get('label_ms', -1)),
|
||||
'ou15': int(row.get('label_ou15', -1)),
|
||||
'ou25': int(row.get('label_ou25', -1)),
|
||||
'ou35': int(row.get('label_ou35', -1)),
|
||||
'btts': int(row.get('label_btts', -1)),
|
||||
}
|
||||
|
||||
# Her market için tahmin ve doğruluk
|
||||
market_results = {}
|
||||
for mkt, label_key in [('ms','ms'),('ou15','ou15'),('ou25','ou25'),
|
||||
('ou35','ou35'),('btts','btts')]:
|
||||
if mkt not in preds or actual[label_key] < 0:
|
||||
continue
|
||||
pred_class = int(np.argmax(preds[mkt][i]))
|
||||
correct = int(pred_class == actual[label_key])
|
||||
|
||||
# Odds (implied prob → odds = 1/prob)
|
||||
pred_prob = float(preds[mkt][i][pred_class])
|
||||
implied_odds = 1 / pred_prob if pred_prob > 0.01 else 10.0
|
||||
# ROI hesabı: 1 birim bahis, kazanırsa (odds-1) kazanç, kaybederse -1
|
||||
roi = (implied_odds - 1) * correct - (1 - correct)
|
||||
|
||||
market_results[mkt] = {
|
||||
'pred': pred_class,
|
||||
'actual': actual[label_key],
|
||||
'correct': correct,
|
||||
'prob': pred_prob,
|
||||
'roi': roi,
|
||||
}
|
||||
|
||||
results.append({
|
||||
'idx': i,
|
||||
'consistency_score': c_score,
|
||||
'conflicts': conflicts,
|
||||
'probs': probs,
|
||||
'market_results': market_results,
|
||||
})
|
||||
|
||||
df_results = pd.DataFrame([{
|
||||
'consistency_score': r['consistency_score'],
|
||||
'n_conflicts': len(r['conflicts']),
|
||||
**{f'{m}_correct': r['market_results'].get(m, {}).get('correct', None)
|
||||
for m in ['ms','ou15','ou25','ou35','btts']},
|
||||
**{f'{m}_roi': r['market_results'].get(m, {}).get('roi', None)
|
||||
for m in ['ms','ou15','ou25','ou35','btts']},
|
||||
} for r in results])
|
||||
|
||||
# ── Analiz ──────────────────────────────────────────────────────────
|
||||
print(f'\n{"="*70}')
|
||||
print('TUTARSIZLIK ANALİZİ')
|
||||
print(f'{"="*70}')
|
||||
|
||||
thresholds = [0.0, 0.1, 0.2, 0.3, 0.5]
|
||||
markets = ['ms', 'ou15', 'ou25', 'ou35', 'btts']
|
||||
|
||||
for t in thresholds:
|
||||
mask = df_results['consistency_score'] <= t
|
||||
n = mask.sum()
|
||||
if n < 50:
|
||||
continue
|
||||
|
||||
print(f'\n[Tutarsızlık <= {t:.1f}] → {n:,} maç ({n/len(df_results)*100:.0f}%)')
|
||||
print(f' {"Market":<8} {"HitRate":>8} {"ROI/bahis":>10} {"Toplam ROI":>12}')
|
||||
print(f' {"-"*42}')
|
||||
for m in markets:
|
||||
col_c = f'{m}_correct'
|
||||
col_r = f'{m}_roi'
|
||||
if col_c not in df_results.columns:
|
||||
continue
|
||||
sub = df_results[mask][col_c].dropna()
|
||||
roi_sub = df_results[mask][col_r].dropna()
|
||||
if len(sub) < 20:
|
||||
continue
|
||||
hit = sub.mean()
|
||||
avg_roi = roi_sub.mean()
|
||||
total_roi = roi_sub.sum()
|
||||
print(f' {m:<8} {hit:>7.1%} {avg_roi:>+9.3f} {total_roi:>+11.1f}')
|
||||
|
||||
# Çelişki türlerine göre breakdown
|
||||
print(f'\n{"="*70}')
|
||||
print('EN SIK ÇELIŞKILER')
|
||||
print(f'{"="*70}')
|
||||
all_conflicts = [c for r in results for c in r['conflicts']]
|
||||
from collections import Counter
|
||||
for conflict, cnt in Counter(all_conflicts).most_common(10):
|
||||
print(f' {cnt:>5}x {conflict}')
|
||||
|
||||
# Tutarsızlık dağılımı
|
||||
print(f'\n{"="*70}')
|
||||
print('TUTARSIZLIK DAĞILIMI')
|
||||
print(f'{"="*70}')
|
||||
for label, lo, hi in [
|
||||
('Tamamen tutarlı', 0.0, 0.05),
|
||||
('Çok tutarlı', 0.05, 0.15),
|
||||
('Orta', 0.15, 0.30),
|
||||
('Tutarsız', 0.30, 0.50),
|
||||
('Çok tutarsız', 0.50, 1.01),
|
||||
]:
|
||||
mask = (df_results['consistency_score'] >= lo) & (df_results['consistency_score'] < hi)
|
||||
n = mask.sum()
|
||||
ou25_hit = df_results[mask]['ou25_correct'].mean()
|
||||
ms_hit = df_results[mask]['ms_correct'].mean()
|
||||
print(f' {label:<20} {n:>6,} maç ({n/len(df_results)*100:>4.0f}%) | '
|
||||
f'MS={ms_hit:.0%} OU25={ou25_hit:.0%}')
|
||||
|
||||
# Raporu kaydet
|
||||
report = {
|
||||
'total_test': len(df_results),
|
||||
'thresholds': {},
|
||||
}
|
||||
for t in thresholds:
|
||||
mask = df_results['consistency_score'] <= t
|
||||
n = mask.sum()
|
||||
report['thresholds'][str(t)] = {
|
||||
'n_matches': int(n),
|
||||
'pct': round(n/len(df_results)*100, 1),
|
||||
'markets': {},
|
||||
}
|
||||
for m in markets:
|
||||
col_c = f'{m}_correct'
|
||||
col_r = f'{m}_roi'
|
||||
if col_c not in df_results.columns:
|
||||
continue
|
||||
sub_c = df_results[mask][col_c].dropna()
|
||||
sub_r = df_results[mask][col_r].dropna()
|
||||
if len(sub_c) > 0:
|
||||
report['thresholds'][str(t)]['markets'][m] = {
|
||||
'hit_rate': round(float(sub_c.mean()), 4),
|
||||
'avg_roi': round(float(sub_r.mean()), 4),
|
||||
'total_roi': round(float(sub_r.sum()), 2),
|
||||
}
|
||||
|
||||
out_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
'reports', 'backtest_consistency.json')
|
||||
with open(out_path, 'w') as f:
|
||||
json.dump(report, f, indent=2)
|
||||
print(f'\nRapor: {out_path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user