gg
This commit is contained in:
@@ -1,206 +0,0 @@
|
||||
"""
|
||||
Backtest for September 13th (Top Leagues Only)
|
||||
==============================================
|
||||
Simulates the NEW 'Skip Logic' on matches from Sept 13, 2025.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
# Load .env manually to ensure correct DB connection
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
sys.path.insert(0, project_root) # Add root to path if needed
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
# ─── Configuration ─────────
|
||||
MIN_CONF_THRESHOLDS = {
|
||||
"MS": 45.0, "DC": 40.0, "OU15": 50.0, "OU25": 45.0,
|
||||
"OU35": 45.0, "BTTS": 45.0, "HT": 40.0,
|
||||
}
|
||||
|
||||
def run_backtest():
|
||||
print("🚀 Backtest: 13 Eylül 2024 - Top Leagues")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load Top Leagues
|
||||
leagues_path = os.path.join(project_root, "top_leagues.json")
|
||||
try:
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
# Ensure they are strings for SQL IN clause
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading top_leagues.json: {e}")
|
||||
return
|
||||
|
||||
# 2. Define Date Range (Sept 13, 2024 UTC)
|
||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
||||
start_ts = int(start_dt.timestamp() * 1000)
|
||||
end_ts = int(end_dt.timestamp() * 1000)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 3. Fetch Matches & Predictions
|
||||
# We need matches that are FT and have a prediction
|
||||
query = """
|
||||
SELECT p.match_id, p.prediction_json,
|
||||
m.score_home, m.score_away, m.status, m.league_id
|
||||
FROM predictions p
|
||||
JOIN matches m ON p.match_id = m.id
|
||||
WHERE m.mst_utc BETWEEN %s AND %s
|
||||
AND m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND p.prediction_json IS NOT NULL
|
||||
"""
|
||||
|
||||
try:
|
||||
cur.execute(query, (start_ts, end_ts, league_ids))
|
||||
rows = cur.fetchall()
|
||||
except Exception as e:
|
||||
print(f"❌ DB Error: {e}")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
print(f"📊 Found {len(rows)} matches with predictions on Sept 13, 2024.")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No predictions found for this date. The AI Engine might not have processed these historical matches yet.")
|
||||
print("💡 Tip: Run the feeder or AI engine on this date range to generate predictions first.")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
total_bets = 0
|
||||
winning_bets = 0
|
||||
skipped_bets = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for row in rows:
|
||||
data = row['prediction_json']
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
|
||||
home_score = row['score_home'] or 0
|
||||
away_score = row['score_away'] or 0
|
||||
total_goals = home_score + away_score
|
||||
|
||||
# Extract Main Pick
|
||||
main_pick = None
|
||||
main_pick_conf = 0.0
|
||||
main_pick_odds = 0.0
|
||||
|
||||
if "main_pick" in data and isinstance(data["main_pick"], dict):
|
||||
mp = data["main_pick"]
|
||||
main_pick = mp.get("pick")
|
||||
main_pick_conf = mp.get("confidence", 0.0)
|
||||
main_pick_odds = mp.get("odds", 0.0)
|
||||
|
||||
if not main_pick or not main_pick_conf:
|
||||
continue
|
||||
|
||||
# Determine Market Type
|
||||
pick_str = str(main_pick).upper()
|
||||
market_type = "MS"
|
||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
||||
if "1.5" in pick_str: market_type = "OU15"
|
||||
elif "3.5" in pick_str: market_type = "OU35"
|
||||
else: market_type = "OU25"
|
||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
||||
|
||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
||||
|
||||
# --- SKIP LOGIC ---
|
||||
# 1. Confidence Gate
|
||||
if main_pick_conf < threshold:
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# 2. Value Gate
|
||||
if main_pick_odds > 0:
|
||||
implied_prob = 1.0 / main_pick_odds
|
||||
my_prob = main_pick_conf / 100.0
|
||||
edge = my_prob - implied_prob
|
||||
if edge < -0.03:
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# --- BET PLAYED ---
|
||||
total_bets += 1
|
||||
is_won = False
|
||||
|
||||
# Resolve Result
|
||||
if market_type == "MS":
|
||||
if (main_pick == "1" or main_pick == "MS 1") and home_score > away_score: is_won = True
|
||||
elif (main_pick == "X" or main_pick == "MS X") and home_score == away_score: is_won = True
|
||||
elif (main_pick == "2" or main_pick == "MS 2") and away_score > home_score: is_won = True
|
||||
|
||||
elif market_type.startswith("OU"):
|
||||
line = 2.5
|
||||
if "1.5" in pick_str: line = 1.5
|
||||
elif "3.5" in pick_str: line = 3.5
|
||||
is_over = total_goals > line
|
||||
is_under = total_goals < line
|
||||
if ("ÜST" in pick_str or "OVER" in pick_str) and is_over: is_won = True
|
||||
elif ("ALT" in pick_str or "UNDER" in pick_str) and is_under: is_won = True
|
||||
|
||||
elif market_type == "BTTS":
|
||||
if home_score > 0 and away_score > 0:
|
||||
if "VAR" in pick_str: is_won = True
|
||||
else:
|
||||
if "YOK" in pick_str: is_won = True
|
||||
|
||||
elif market_type == "DC":
|
||||
if "1X" in pick_str and home_score >= away_score: is_won = True
|
||||
elif "X2" in pick_str and away_score >= home_score: is_won = True
|
||||
elif "12" in pick_str and home_score != away_score: is_won = True
|
||||
|
||||
if is_won:
|
||||
winning_bets += 1
|
||||
profit = main_pick_odds - 1.0
|
||||
total_profit += profit
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
|
||||
# Report
|
||||
print("\n" + "="*60)
|
||||
print("📈 BACKTEST RESULTS: 13 EYLÜL 2025 (TOP LEAGUES)")
|
||||
print("="*60)
|
||||
print(f"Total Matches Analyzed: {len(rows)}")
|
||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
||||
print(f"✅ Bets PLAYED: {total_bets}")
|
||||
|
||||
if total_bets > 0:
|
||||
win_rate = (winning_bets / total_bets) * 100
|
||||
roi = (total_profit / total_bets) * 100
|
||||
|
||||
print(f"🏆 Winning Bets: {winning_bets}")
|
||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
||||
print("-" * 40)
|
||||
print(f" Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING")
|
||||
else:
|
||||
print("⚠️ No bets were played. Thresholds might be too high or no suitable matches found.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_backtest()
|
||||
@@ -1,240 +0,0 @@
|
||||
"""
|
||||
Detailed Backtest with 50 Top League Matches
|
||||
============================================
|
||||
Runs AI Engine predictions on 50 real historical matches and shows
|
||||
exactly which predictions were correct and which were skipped.
|
||||
|
||||
Usage:
|
||||
python ai-engine/scripts/backtest_50_detailed.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
# Add paths
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
# 50 Match IDs from the query
|
||||
MATCH_IDS = [
|
||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
||||
]
|
||||
|
||||
def run_detailed_backtest():
|
||||
print("🚀 DETAILED BACKTEST: 50 Top League Matches")
|
||||
print("🧠 Engine: V30 Ensemble (V20+V25) + Skip Logic")
|
||||
print("="*80)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Fetch match details with odds
|
||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
||||
cur.execute(f"""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.id IN ({placeholders})
|
||||
AND m.status = 'FT'
|
||||
ORDER BY m.mst_utc DESC
|
||||
""", MATCH_IDS)
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Found {len(rows)} matches. Starting AI Analysis...")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No matches found.")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Initialize AI Engine
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
print("✅ AI Engine Loaded.\n")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load AI Engine: {e}")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# ─── Backtest Loop ───
|
||||
results = []
|
||||
total_skipped = 0
|
||||
total_played = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
MIN_CONF = 45.0
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home_team = row['home_team'] or "Unknown"
|
||||
away_team = row['away_team'] or "Unknown"
|
||||
league = row['league_name'] or "Unknown"
|
||||
home_score = row['score_home'] or 0
|
||||
away_score = row['score_away'] or 0
|
||||
total_goals = home_score + away_score
|
||||
|
||||
print(f"[{i+1}/{len(rows)}] {home_team} vs {away_team} ({league}) ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
prediction = orchestrator.analyze_match(match_id)
|
||||
|
||||
if not prediction:
|
||||
print("⚠️ No prediction")
|
||||
continue
|
||||
|
||||
# Extract Main Pick
|
||||
main_pick = prediction.get("main_pick") or {}
|
||||
pick_name = main_pick.get("pick", "")
|
||||
confidence = main_pick.get("confidence", 0)
|
||||
odds = main_pick.get("odds", 0)
|
||||
|
||||
# Apply Skip Logic
|
||||
if confidence < MIN_CONF:
|
||||
print(f"🚫 SKIP (Conf {confidence:.0f}%)")
|
||||
total_skipped += 1
|
||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
||||
continue
|
||||
|
||||
if odds > 0:
|
||||
implied_prob = 1.0 / odds
|
||||
my_prob = confidence / 100.0
|
||||
if my_prob - implied_prob < -0.03:
|
||||
print(f"🚫 SKIP (Bad Value)")
|
||||
total_skipped += 1
|
||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
||||
"conf": confidence, "odds": odds, "result": "SKIPPED", "profit": 0})
|
||||
continue
|
||||
|
||||
# Bet Played
|
||||
total_played += 1
|
||||
won = False
|
||||
|
||||
# Resolve
|
||||
pick_clean = str(pick_name).upper()
|
||||
if pick_clean in ["1", "MS 1", "İY 1"] and home_score > away_score: won = True
|
||||
elif pick_clean in ["X", "MS X", "İY X"] and home_score == away_score: won = True
|
||||
elif pick_clean in ["2", "MS 2", "İY 2"] and away_score > home_score: won = True
|
||||
elif pick_clean in ["1X", "X2"] or ("1X" in pick_clean or "X2" in pick_clean):
|
||||
if "1X" in pick_clean and home_score >= away_score: won = True
|
||||
elif "X2" in pick_clean and away_score >= home_score: won = True
|
||||
elif pick_clean in ["12"] and home_score != away_score: won = True
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if total_goals > line: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if total_goals < line: won = True
|
||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
||||
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
print(f"✅ WON ({pick_name} @ {odds:.2f}, +{profit:.2f})")
|
||||
else:
|
||||
profit = -1.0
|
||||
print(f"❌ LOST ({pick_name} @ {odds:.2f})")
|
||||
|
||||
total_profit += profit
|
||||
results.append({"match": f"{home_team} vs {away_team}", "pick": pick_name,
|
||||
"conf": confidence, "odds": odds,
|
||||
"result": "WON" if won else "LOST", "profit": profit,
|
||||
"score": f"{home_score}-{away_score}"})
|
||||
|
||||
except Exception as e:
|
||||
print(f"💥 Error: {e}")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# ─── DETAILED REPORT ───
|
||||
print("\n" + "="*80)
|
||||
print("📈 DETAILED BACKTEST RESULTS")
|
||||
print(f"⏱️ Time: {elapsed:.1f}s")
|
||||
print("="*80)
|
||||
print(f"📊 Total Matches: {len(rows)}")
|
||||
print(f"🚫 Skipped: {total_skipped}")
|
||||
print(f"🎲 Played: {total_played}")
|
||||
print(f"✅ Won: {total_won}")
|
||||
print(f"💀 Lost: {total_played - total_won}")
|
||||
print(f"💰 Profit: {total_profit:+.2f} units")
|
||||
|
||||
if total_played > 0:
|
||||
win_rate = (total_won / total_played) * 100
|
||||
roi = (total_profit / total_played) * 100
|
||||
print(f"📊 Win Rate: {win_rate:.1f}%")
|
||||
print(f"📊 ROI: {roi:.1f}%")
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING")
|
||||
|
||||
# ─── TABLE OF ALL RESULTS ───
|
||||
print("\n" + "="*80)
|
||||
print("📋 DETAILED MATCH RESULTS")
|
||||
print("="*80)
|
||||
print(f"{'Match':<40} {'Pick':<15} {'Conf':<6} {'Odds':<6} {'Result':<8} {'Score':<6}")
|
||||
print("-"*80)
|
||||
for r in results:
|
||||
match_str = r['match'][:38]
|
||||
pick_str = str(r['pick'])[:13]
|
||||
conf_str = f"{r['conf']:.0f}%"
|
||||
odds_str = f"{r['odds']:.2f}" if r['odds'] > 0 else "N/A"
|
||||
res_str = r['result']
|
||||
score_str = r.get('score', '')
|
||||
|
||||
# Color coding
|
||||
if res_str == "WON": res_display = f"✅ {res_str}"
|
||||
elif res_str == "LOST": res_display = f"❌ {res_str}"
|
||||
else: res_display = f"🚫 {res_str}"
|
||||
|
||||
print(f"{match_str:<40} {pick_str:<15} {conf_str:<6} {odds_str:<6} {res_display:<12} {score_str:<6}")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_detailed_backtest()
|
||||
@@ -1,191 +0,0 @@
|
||||
"""
|
||||
Adaptive 500 Match Backtest
|
||||
=============================
|
||||
Skips NO match unless NO odds exist.
|
||||
Evaluates ALL available markets (MS, OU, BTTS) and picks the BEST value bet.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_adaptive_backtest():
|
||||
print("🔄 ADAPTIVE 500 MATCH BACKTEST")
|
||||
print("="*60)
|
||||
|
||||
# 1. Load Top Leagues
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 2. Fetch 500 Finished Matches with Odds
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Found {len(rows)} matches. Analyzing...\n")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No matches found.")
|
||||
return
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Error: {e}")
|
||||
return
|
||||
|
||||
# Stats
|
||||
total_evaluated = 0
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
skipped_count = 0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
total_evaluated += 1
|
||||
# print(f"[{i+1}] {home} vs {away} ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred:
|
||||
# print("⚠️ No Data")
|
||||
continue
|
||||
|
||||
# ─── ADAPTIVE PICKING ───
|
||||
# Check ALL recommendations (Expert or Standard) to find the BEST option
|
||||
candidates = []
|
||||
|
||||
# Add main picks
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("safe_alternative"): candidates.append(rec["safe_alternative"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
best_bet = None
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
conf = c.get("confidence", 0)
|
||||
odds = c.get("odds", 0)
|
||||
pick = c.get("pick")
|
||||
|
||||
# Flexible Criteria:
|
||||
# 1. Confidence > 60%
|
||||
# 2. Odds > 1.10 (Not "free" odds like 1.00)
|
||||
# 3. Edge > -2% (Slightly tolerant)
|
||||
if conf >= 60 and odds > 1.10:
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
|
||||
# Prioritize positive edge, but accept small negative if confidence is high
|
||||
if edge > -2.0:
|
||||
if best_bet is None or (conf > best_bet.get("confidence", 0)):
|
||||
best_bet = c
|
||||
|
||||
if best_bet:
|
||||
pick = str(best_bet.get("pick")).upper()
|
||||
conf = best_bet.get("confidence")
|
||||
odds = best_bet.get("odds")
|
||||
|
||||
# Resolution Logic
|
||||
won = False
|
||||
if pick in ["1", "MS 1", "İY 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X", "İY X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2", "İY 2"] and a_score > h_score: won = True
|
||||
elif pick in ["1X", "X2"]:
|
||||
if "1X" in pick and h_score >= a_score: won = True
|
||||
elif "X2" in pick and a_score >= h_score: won = True
|
||||
elif pick == "12" and h_score != a_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
total_bet += 1
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
# print(f"✅ WON (+{profit:.2f}) | {pick}")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
# print(f"❌ LOST ({pick} @ {odds:.2f})")
|
||||
else:
|
||||
skipped_count += 1
|
||||
# print(f"🚫 SKIP (No Value)")
|
||||
|
||||
except Exception as e:
|
||||
# print(f"💥 Error: {e}")
|
||||
pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🔄 ADAPTIVE BACKTEST RESULTS (500 Matches)")
|
||||
print("="*60)
|
||||
print(f"📊 Evaluated: {total_evaluated}")
|
||||
print(f"🎲 Played: {total_bet}")
|
||||
print(f"🚫 Skipped: {skipped_count}")
|
||||
print(f"✅ Won: {total_won}")
|
||||
|
||||
if total_bet > 0:
|
||||
win_rate = (total_won / total_bet) * 100
|
||||
roi = (total_profit / total_bet) * 100
|
||||
print(f"📈 Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit: {total_profit:.2f} Units")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
if total_profit > 0: print("🟢 KARLI STRATEJİ")
|
||||
else: print("🔴 ZARARDA")
|
||||
else:
|
||||
print("⚠️ Hiç bahis oynanmadı. Veri kalitesi çok düşük.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_adaptive_backtest()
|
||||
@@ -1,145 +0,0 @@
|
||||
"""
|
||||
Diagnostic Backtest - Hangi Pazar Kanıyor?
|
||||
===========================================
|
||||
Analyses the 500 matches to see WHICH markets are losing money.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from collections import defaultdict
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_diagnostic():
|
||||
print("🔍 TANI BACKTESTİ: NEREDE KAYBETTİK?")
|
||||
print("="*60)
|
||||
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away, m.league_id,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
# Market Stats: { "MS": {"won": 10, "lost": 20, "profit": -5.0}, ... }
|
||||
market_stats = defaultdict(lambda: {"won": 0, "lost": 0, "profit": 0.0, "total": 0})
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
candidates = []
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
played_this = False
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
conf = c.get("confidence", 0)
|
||||
odds = c.get("odds", 0)
|
||||
pick = str(c.get("pick")).upper()
|
||||
market_type = c.get("market_type", "Unknown")
|
||||
|
||||
# Criteria
|
||||
if conf >= 60 and odds > 1.10:
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
if edge > -2.0:
|
||||
# Resolve
|
||||
won = False
|
||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif pick in ["1X", "X2"]:
|
||||
if "1X" in pick and h_score >= a_score: won = True
|
||||
elif "X2" in pick and a_score >= h_score: won = True
|
||||
elif pick == "12" and h_score != a_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
market_stats[market_type]["total"] += 1
|
||||
if won:
|
||||
market_stats[market_type]["won"] += 1
|
||||
market_stats[market_type]["profit"] += (odds - 1.0)
|
||||
else:
|
||||
market_stats[market_type]["lost"] += 1
|
||||
market_stats[market_type]["profit"] -= 1.0
|
||||
|
||||
played_this = True
|
||||
break # Only one bet per match
|
||||
|
||||
except: pass
|
||||
|
||||
# Print Results
|
||||
print("\n" + "="*60)
|
||||
print("📊 PAZAR BAZLI KAR/ZARAR TABLOSU")
|
||||
print("="*60)
|
||||
print(f"{'Market':<15} {'Oynanan':<10} {'Kazanılan':<10} {'Win%':<8} {'Kâr':<10}")
|
||||
print("-" * 60)
|
||||
|
||||
for mkt, stats in sorted(market_stats.items(), key=lambda x: x[1]["profit"], reverse=True):
|
||||
wr = (stats["won"] / stats["total"] * 100) if stats["total"] > 0 else 0
|
||||
print(f"{mkt:<15} {stats['total']:<10} {stats['won']:<10} {wr:.1f}% {stats['profit']:+.2f} Units")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_diagnostic()
|
||||
@@ -1,215 +0,0 @@
|
||||
"""
|
||||
V27 FINAL BACKTEST — Conservative Flat Bet
|
||||
Only the strongest validated edges. No Kelly compounding.
|
||||
"""
|
||||
import pandas as pd, numpy as np
|
||||
|
||||
df = pd.read_csv('data/training_data_v27.csv', low_memory=False)
|
||||
for c in df.columns:
|
||||
if c not in ['match_id','league_name','home_team','away_team']:
|
||||
df[c] = pd.to_numeric(df[c], errors='coerce')
|
||||
df = df.dropna(subset=['odds_ms_h','odds_ms_d','odds_ms_a'])
|
||||
df = df[(df.odds_ms_h>1.01)&(df.odds_ms_d>1.01)&(df.odds_ms_a>1.01)]
|
||||
|
||||
n = len(df)
|
||||
# 5-fold walk-forward: train on 60%, validate patterns, test on remaining
|
||||
folds = 5
|
||||
fold_size = n // folds
|
||||
all_results = []
|
||||
|
||||
print("="*65)
|
||||
print(" V27 WALK-FORWARD FLAT-BET BACKTEST")
|
||||
print("="*65)
|
||||
|
||||
for fold in range(2, folds): # start from fold 2 so we have enough training data
|
||||
train_end = fold * fold_size
|
||||
test_start = train_end
|
||||
test_end = (fold+1)*fold_size if fold < folds-1 else n
|
||||
|
||||
train_df = df.iloc[:train_end]
|
||||
test_df = df.iloc[test_start:test_end]
|
||||
|
||||
print(f"\n --- Fold {fold}: train={len(train_df)}, test={len(test_df)} ---")
|
||||
|
||||
# Discover REST edges from training data
|
||||
strategies = []
|
||||
|
||||
for hr in [5, 7, 10, 14]:
|
||||
for ar in [3, 4, 5]:
|
||||
for cls, col in [(0,'odds_ms_h'), (2,'odds_ms_a')]:
|
||||
idx = (train_df.home_days_rest > hr) & (train_df.away_days_rest < ar)
|
||||
sub = train_df[idx]
|
||||
if len(sub) < 50:
|
||||
continue
|
||||
rate = (sub.label_ms == cls).mean()
|
||||
avg_odds = sub[col].mean()
|
||||
ev = rate * avg_odds
|
||||
if ev > 1.02: # only strong edges (>2% edge)
|
||||
strategies.append((hr, ar, cls, rate, avg_odds, ev, len(sub)))
|
||||
|
||||
if not strategies:
|
||||
print(" No strong edges found in training data")
|
||||
continue
|
||||
|
||||
# Apply best strategies to test
|
||||
strategies.sort(key=lambda x: x[5], reverse=True)
|
||||
best = strategies[:3] # top 3 only
|
||||
|
||||
fold_bets = 0
|
||||
fold_wins = 0
|
||||
fold_pnl = 0
|
||||
stake = 10 # flat 10 units
|
||||
|
||||
for _, row in test_df.iterrows():
|
||||
for hr, ar, cls, est_p, _, _, _ in best:
|
||||
if pd.isna(row.home_days_rest) or pd.isna(row.away_days_rest):
|
||||
continue
|
||||
if row.home_days_rest <= hr or row.away_days_rest >= ar:
|
||||
continue
|
||||
odds_col = ['odds_ms_h','odds_ms_d','odds_ms_a'][cls]
|
||||
odds_val = row[odds_col]
|
||||
if pd.isna(odds_val) or odds_val < 1.50 or odds_val > 5.0:
|
||||
continue
|
||||
# Additional filter: only bet when odds give reasonable EV
|
||||
if est_p * odds_val < 1.0:
|
||||
continue
|
||||
|
||||
won = (row.label_ms == cls)
|
||||
pnl = stake * (odds_val - 1) if won else -stake
|
||||
fold_bets += 1
|
||||
if won:
|
||||
fold_wins += 1
|
||||
fold_pnl += pnl
|
||||
all_results.append({'fold': fold, 'won': won, 'pnl': pnl,
|
||||
'odds': odds_val, 'stake': stake,
|
||||
'cls': ['H','D','A'][cls]})
|
||||
|
||||
if fold_bets > 0:
|
||||
roi = fold_pnl / (fold_bets * stake) * 100
|
||||
print(f" Best strategies: {[(h,a,['H','D','A'][c],f'EV={e:.3f}') for h,a,c,_,_,e,_ in best]}")
|
||||
print(f" Bets: {fold_bets}, Wins: {fold_wins} ({fold_wins/fold_bets*100:.1f}%), "
|
||||
f"ROI: {roi:+.1f}%, PnL: {fold_pnl:+.0f}")
|
||||
|
||||
# Overall
|
||||
print("\n" + "="*65)
|
||||
print(" OVERALL RESULTS")
|
||||
print("="*65)
|
||||
if all_results:
|
||||
total = len(all_results)
|
||||
wins = sum(1 for r in all_results if r['won'])
|
||||
total_pnl = sum(r['pnl'] for r in all_results)
|
||||
total_staked = sum(r['stake'] for r in all_results)
|
||||
roi = total_pnl / total_staked * 100
|
||||
|
||||
print(f" Total bets: {total}")
|
||||
print(f" Wins: {wins} ({wins/total*100:.1f}%)")
|
||||
print(f" Total staked: {total_staked:.0f}")
|
||||
print(f" PnL: {total_pnl:+.0f}")
|
||||
print(f" ROI: {roi:+.1f}%")
|
||||
print(f" Avg odds: {np.mean([r['odds'] for r in all_results]):.2f}")
|
||||
|
||||
# By class
|
||||
print("\n --- By Bet Type ---")
|
||||
for cls in ['H','A']:
|
||||
cb = [r for r in all_results if r['cls'] == cls]
|
||||
if cb:
|
||||
cw = sum(1 for r in cb if r['won'])
|
||||
cp = sum(r['pnl'] for r in cb)
|
||||
cs = sum(r['stake'] for r in cb)
|
||||
print(f" {cls}: {len(cb)} bets, hit={cw/len(cb)*100:.1f}%, ROI={cp/cs*100:+.1f}%")
|
||||
|
||||
# Cumulative PnL curve
|
||||
print("\n --- Cumulative PnL ---")
|
||||
cum = 0
|
||||
step = max(1, total // 15)
|
||||
for j in range(0, total, step):
|
||||
cum = sum(r['pnl'] for r in all_results[:j+1])
|
||||
print(f" After bet {j+1:4d}: PnL={cum:+.0f}")
|
||||
cum = sum(r['pnl'] for r in all_results)
|
||||
print(f" After bet {total:4d}: PnL={cum:+.0f} (FINAL)")
|
||||
else:
|
||||
print(" No bets placed!")
|
||||
|
||||
# ── Now combine with MODEL for smarter filtering ──
|
||||
print("\n" + "="*65)
|
||||
print(" COMBINED: Rest Rules + Fundamentals Model")
|
||||
print("="*65)
|
||||
|
||||
import pickle, json
|
||||
from pathlib import Path
|
||||
MODELS_DIR = Path("models/v27")
|
||||
|
||||
feat_cols = json.load(open(MODELS_DIR / "v27_feature_cols.json"))
|
||||
ms_models = {}
|
||||
for name in ['xgb','lgb','cb']:
|
||||
p = MODELS_DIR / f"v27_ms_{name}.pkl"
|
||||
if p.exists():
|
||||
with open(p,'rb') as f:
|
||||
ms_models[name] = pickle.load(f)
|
||||
|
||||
if ms_models:
|
||||
test_df = df.iloc[int(n*0.8):].copy()
|
||||
X_test = test_df[feat_cols].values
|
||||
|
||||
# Get model predictions
|
||||
preds = []
|
||||
for name, m in ms_models.items():
|
||||
if name == 'xgb':
|
||||
import xgboost as xgb
|
||||
dm = xgb.DMatrix(X_test, feature_names=feat_cols)
|
||||
preds.append(m.predict(dm))
|
||||
elif name == 'lgb':
|
||||
preds.append(m.predict(X_test))
|
||||
elif name == 'cb':
|
||||
preds.append(m.predict_proba(X_test))
|
||||
model_probs = np.mean(preds, axis=0) # (n, 3)
|
||||
|
||||
# Now apply rest rules + model agreement
|
||||
margin = 1/test_df.odds_ms_h.values + 1/test_df.odds_ms_d.values + 1/test_df.odds_ms_a.values
|
||||
impl = np.column_stack([
|
||||
(1/test_df.odds_ms_h.values)/margin,
|
||||
(1/test_df.odds_ms_d.values)/margin,
|
||||
(1/test_df.odds_ms_a.values)/margin,
|
||||
])
|
||||
|
||||
combo_bets = 0
|
||||
combo_wins = 0
|
||||
combo_pnl = 0
|
||||
|
||||
for j in range(len(test_df)):
|
||||
row = test_df.iloc[j]
|
||||
for hr, ar in [(14,5),(10,5),(7,5),(5,5)]:
|
||||
if pd.isna(row.home_days_rest) or pd.isna(row.away_days_rest):
|
||||
continue
|
||||
if row.home_days_rest <= hr or row.away_days_rest >= ar:
|
||||
continue
|
||||
for cls in [0, 2]:
|
||||
odds_val = [row.odds_ms_h, row.odds_ms_d, row.odds_ms_a][cls]
|
||||
if pd.isna(odds_val) or odds_val < 1.50 or odds_val > 5.0:
|
||||
continue
|
||||
|
||||
model_p = model_probs[j, cls]
|
||||
impl_p = impl[j, cls]
|
||||
|
||||
# DOUBLE FILTER: rest rule + model agrees (model_prob > implied)
|
||||
if model_p <= impl_p:
|
||||
continue # model disagrees, skip
|
||||
edge = model_p - impl_p
|
||||
if edge < 0.03:
|
||||
continue # too small
|
||||
|
||||
won = (row.label_ms == cls)
|
||||
pnl = 10 * (odds_val - 1) if won else -10
|
||||
combo_bets += 1
|
||||
if won:
|
||||
combo_wins += 1
|
||||
combo_pnl += pnl
|
||||
|
||||
if combo_bets > 0:
|
||||
roi = combo_pnl / (combo_bets * 10) * 100
|
||||
print(f" Bets: {combo_bets}")
|
||||
print(f" Wins: {combo_wins} ({combo_wins/combo_bets*100:.1f}%)")
|
||||
print(f" PnL: {combo_pnl:+.0f}")
|
||||
print(f" ROI: {roi:+.1f}%")
|
||||
else:
|
||||
print(" No combined bets triggered")
|
||||
@@ -1,223 +0,0 @@
|
||||
"""
|
||||
Real AI Engine Backtest Script
|
||||
==============================
|
||||
Uses the ACTUAL models (V20/V25 Ensemble) to predict historical matches.
|
||||
|
||||
Usage:
|
||||
python ai-engine/scripts/backtest_real.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
# Add paths
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
|
||||
# Fix for Windows path issues in scripts
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR) # One level up if inside scripts folder
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator, MatchData
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_backtest():
|
||||
print("🚀 REAL AI BACKTEST: Sept 13, 2024 - Top Leagues")
|
||||
print("🧠 Engine: V30 Ensemble (V20+V25)")
|
||||
print("="*60)
|
||||
|
||||
# Load Top Leagues
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
try:
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
print(f"📋 Loaded {len(top_leagues)} top leagues.")
|
||||
except Exception as e:
|
||||
print(f"❌ Error loading top_leagues.json: {e}")
|
||||
return
|
||||
|
||||
# Date Range (Sept 13, 2024)
|
||||
start_dt = datetime(2024, 9, 13, 0, 0, 0)
|
||||
end_dt = datetime(2024, 9, 13, 23, 59, 59)
|
||||
start_ts = int(start_dt.timestamp() * 1000)
|
||||
end_ts = int(end_dt.timestamp() * 1000)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# Fetch Matches
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.mst_utc, m.league_id, m.status, m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.mst_utc BETWEEN %s AND %s
|
||||
AND m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
ORDER BY m.mst_utc ASC
|
||||
LIMIT 20 -- Limit to 20 matches to avoid running for hours on a single backtest
|
||||
""", (start_ts, end_ts, league_ids))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Found {len(rows)} finished matches. Starting AI Analysis...")
|
||||
|
||||
if not rows:
|
||||
print("⚠️ No matches found for this date.")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# Initialize AI Engine
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
print("✅ AI Engine (SingleMatchOrchestrator) Loaded.")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to load AI Engine: {e}")
|
||||
print("💡 Make sure models are trained/present in ai-engine/models/")
|
||||
cur.close()
|
||||
conn.close()
|
||||
return
|
||||
|
||||
# ─── Backtest Loop ───
|
||||
total_matches_analyzed = 0
|
||||
bets_skipped = 0
|
||||
bets_played = 0
|
||||
bets_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
# Thresholds matching the NEW Skip Logic
|
||||
MIN_CONF = 45.0
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home_team = row['home_team']
|
||||
away_team = row['away_team']
|
||||
home_score = row['score_home']
|
||||
away_score = row['score_away']
|
||||
|
||||
print(f"\n[{i+1}/{len(rows)}] Analyzing: {home_team} vs {away_team} ...")
|
||||
|
||||
try:
|
||||
# 1. AI PREDICTION (Actual Model Call)
|
||||
prediction = orchestrator.analyze_match(match_id)
|
||||
|
||||
if not prediction:
|
||||
print(f" ⚠️ AI returned no prediction.")
|
||||
continue
|
||||
|
||||
total_matches_analyzed += 1
|
||||
|
||||
# 2. Extract Main Pick
|
||||
main_pick = prediction.get("main_pick") or {}
|
||||
pick_name = main_pick.get("pick")
|
||||
confidence = main_pick.get("confidence", 0)
|
||||
odds = main_pick.get("odds", 0)
|
||||
|
||||
if not pick_name or not confidence:
|
||||
print(f" ⚠️ No main pick found in prediction.")
|
||||
continue
|
||||
|
||||
print(f" 🤖 Pick: {pick_name} | Conf: {confidence}% | Odds: {odds}")
|
||||
|
||||
# 3. Apply Skip Logic (New Backtest Logic)
|
||||
if confidence < MIN_CONF:
|
||||
print(f" 🚫 SKIPPED (Confidence {confidence}% < {MIN_CONF}%)")
|
||||
bets_skipped += 1
|
||||
continue
|
||||
|
||||
if odds > 0:
|
||||
implied_prob = 1.0 / odds
|
||||
my_prob = confidence / 100.0
|
||||
if my_prob - implied_prob < -0.03: # Negative edge
|
||||
print(f" 🚫 SKIPPED (Negative Edge)")
|
||||
bets_skipped += 1
|
||||
continue
|
||||
|
||||
# 4. Bet Played
|
||||
bets_played += 1
|
||||
print(f" 🎲 BET PLAYED: {pick_name} @ {odds}")
|
||||
|
||||
# 5. Resolve Bet
|
||||
won = False
|
||||
# Basic resolution logic (Need to parse pick_name like "1", "X", "2", "2.5 Üst", etc.)
|
||||
pick_clean = str(pick_name).upper()
|
||||
|
||||
# MS
|
||||
if pick_clean in ["1", "MS 1"] and home_score > away_score: won = True
|
||||
elif pick_clean in ["X", "MS X"] and home_score == away_score: won = True
|
||||
elif pick_clean in ["2", "MS 2"] and away_score > home_score: won = True
|
||||
|
||||
# OU25
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
if (home_score + away_score) > 2.5: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
if (home_score + away_score) < 2.5: won = True
|
||||
|
||||
# BTTS
|
||||
elif "VAR" in pick_clean and home_score > 0 and away_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (home_score == 0 or away_score == 0): won = True
|
||||
|
||||
if won:
|
||||
bets_won += 1
|
||||
profit = odds - 1.0
|
||||
print(f" ✅ WON! (+{profit:.2f} units)")
|
||||
else:
|
||||
profit = -1.0
|
||||
print(f" ❌ LOST! (-1.00 units)")
|
||||
|
||||
total_profit += profit
|
||||
|
||||
except Exception as e:
|
||||
print(f" 💥 Error during analysis: {e}")
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# ─── FINAL REPORT ───
|
||||
print("\n" + "="*60)
|
||||
print("📈 REAL AI BACKTEST RESULTS")
|
||||
print(f"🕒 Time taken: {elapsed:.1f} seconds")
|
||||
print("="*60)
|
||||
print(f"📊 Matches Analyzed: {total_matches_analyzed}")
|
||||
print(f"🚫 Bets SKIPPED: {bets_skipped}")
|
||||
print(f"✅ Bets PLAYED: {bets_played}")
|
||||
|
||||
if bets_played > 0:
|
||||
win_rate = (bets_won / bets_played) * 100
|
||||
roi = (total_profit / bets_played) * 100
|
||||
yield_val = total_profit # Net Units
|
||||
|
||||
print(f"🏆 Bets Won: {bets_won}")
|
||||
print(f"💀 Bets Lost: {bets_played - bets_won}")
|
||||
print("-" * 40)
|
||||
print(f" Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING")
|
||||
else:
|
||||
print("⚠️ No bets were played. All were skipped or failed.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_backtest()
|
||||
@@ -1,231 +0,0 @@
|
||||
"""
|
||||
Backtest ROI Engine
|
||||
===================
|
||||
Simulates the NEW "Skip Logic" on historical predictions.
|
||||
Answers: "What if we only played the bets the model was confident about?"
|
||||
|
||||
Usage:
|
||||
python ai-engine/scripts/backtest_roi.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from typing import Dict, List, Any
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Load .env from project root (2 levels up from this script)
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
load_dotenv(os.path.join(project_root, ".env"))
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
"""Return a psycopg2-compatible DSN from DATABASE_URL."""
|
||||
# HARDCODED FOR BACKTEST (Bypassing dotenv issues)
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
# ─── Configuration (Matching the NEW BetRecommender Logic) ─────────
|
||||
# Minimum confidence to even consider a bet (Hard Gate)
|
||||
MIN_CONF_THRESHOLDS = {
|
||||
"MS": 45.0,
|
||||
"DC": 40.0,
|
||||
"OU15": 50.0,
|
||||
"OU25": 45.0,
|
||||
"OU35": 45.0,
|
||||
"BTTS": 45.0,
|
||||
"HT": 40.0,
|
||||
}
|
||||
|
||||
def get_market_type_from_key(key: str) -> str:
|
||||
"""Map prediction keys to market types for thresholding."""
|
||||
if key.startswith("ms_") or key in ["1", "X", "2"]: return "MS"
|
||||
if key.startswith("dc_") or key in ["1X", "X2", "12"]: return "DC"
|
||||
if key.startswith("ou15_") or key.startswith("1.5"): return "OU15"
|
||||
if key.startswith("ou25_") or key.startswith("2.5"): return "OU25"
|
||||
if key.startswith("ou35_") or key.startswith("3.5"): return "OU35"
|
||||
if key.startswith("btts_") or key in ["Var", "Yok"]: return "BTTS"
|
||||
if key.startswith("ht_") or key.startswith("İY"): return "HT"
|
||||
return "MS"
|
||||
|
||||
def simulate_backtest():
|
||||
print("🚀 Starting Backtest with NEW 'Skip Logic'...")
|
||||
print("="*60)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
# 1. Fetch PREDICTIONS that have a confidence score
|
||||
# We limit to last 1000 finished matches to keep it fast but representative
|
||||
cur.execute("""
|
||||
SELECT p.match_id, p.prediction_json,
|
||||
m.score_home, m.score_away, m.status
|
||||
FROM predictions p
|
||||
JOIN matches m ON p.match_id = m.id
|
||||
WHERE m.status = 'FT'
|
||||
AND p.prediction_json IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 2000
|
||||
""")
|
||||
predictions = cur.fetchall()
|
||||
|
||||
print(f"📊 Loaded {len(predictions)} historical predictions.")
|
||||
|
||||
total_bets = 0
|
||||
winning_bets = 0
|
||||
skipped_bets = 0
|
||||
total_profit = 0.0 # Assuming unit stake of 1.0
|
||||
|
||||
# 2. Process each prediction
|
||||
for pred_row in predictions:
|
||||
match_id = pred_row['match_id']
|
||||
data = pred_row['prediction_json']
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
|
||||
# Real result
|
||||
home_score = pred_row['score_home'] or 0
|
||||
away_score = pred_row['score_away'] or 0
|
||||
total_goals = home_score + away_score
|
||||
|
||||
# Extract prediction details from the JSON structure
|
||||
# The structure varies, but usually contains 'main_pick', 'bet_summary', or 'market_board'
|
||||
|
||||
# Try to get the main pick recommendation
|
||||
main_pick = None
|
||||
main_pick_conf = 0.0
|
||||
main_pick_odds = 0.0
|
||||
|
||||
# Navigate the V20+ JSON structure
|
||||
market_board = data.get("market_board", {})
|
||||
|
||||
# Check Main Pick
|
||||
if "main_pick" in data:
|
||||
mp = data["main_pick"]
|
||||
if isinstance(mp, dict):
|
||||
main_pick = mp.get("pick")
|
||||
main_pick_conf = mp.get("confidence", 0.0)
|
||||
main_pick_odds = mp.get("odds", 0.0)
|
||||
|
||||
# If no main pick, try bet_summary
|
||||
if not main_pick and "bet_summary" in data:
|
||||
summary = data["bet_summary"]
|
||||
if isinstance(summary, list) and len(summary) > 0:
|
||||
# Take the highest confidence one
|
||||
best = max(summary, key=lambda x: x.get("confidence", 0))
|
||||
main_pick = best.get("pick")
|
||||
main_pick_conf = best.get("confidence", 0.0)
|
||||
main_pick_odds = best.get("odds", 0.0)
|
||||
|
||||
if not main_pick or not main_pick_conf:
|
||||
continue
|
||||
|
||||
# ─── NEW LOGIC: APPLY FILTERS ───
|
||||
# 1. Determine Market Type
|
||||
# Simple heuristic based on pick string
|
||||
pick_str = str(main_pick).upper()
|
||||
market_type = "MS"
|
||||
if "1X" in pick_str or "X2" in pick_str or "12" in pick_str: market_type = "DC"
|
||||
elif "ÜST" in pick_str or "ALT" in pick_str or "OVER" in pick_str or "UNDER" in pick_str:
|
||||
if "1.5" in pick_str: market_type = "OU15"
|
||||
elif "3.5" in pick_str: market_type = "OU35"
|
||||
else: market_type = "OU25"
|
||||
elif "VAR" in pick_str or "YOK" in pick_str or "BTTS" in pick_str: market_type = "BTTS"
|
||||
|
||||
threshold = MIN_CONF_THRESHOLDS.get(market_type, 45.0)
|
||||
|
||||
# 2. Check Confidence Gate
|
||||
if main_pick_conf < threshold:
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# 3. Check Value Gate (Edge)
|
||||
if main_pick_odds > 0:
|
||||
implied_prob = 1.0 / main_pick_odds
|
||||
my_prob = main_pick_conf / 100.0
|
||||
edge = my_prob - implied_prob
|
||||
if edge < -0.03: # Negative value
|
||||
skipped_bets += 1
|
||||
continue
|
||||
|
||||
# ─── BET IS PLAYED ───
|
||||
total_bets += 1
|
||||
|
||||
# Determine if WON
|
||||
is_won = False
|
||||
|
||||
# Resolve MS (1, X, 2)
|
||||
if market_type == "MS":
|
||||
if main_pick == "1" and home_score > away_score: is_won = True
|
||||
elif main_pick == "X" and home_score == away_score: is_won = True
|
||||
elif main_pick == "2" and away_score > home_score: is_won = True
|
||||
elif main_pick == "MS 1" and home_score > away_score: is_won = True
|
||||
elif main_pick == "MS X" and home_score == away_score: is_won = True
|
||||
elif main_pick == "MS 2" and away_score > home_score: is_won = True
|
||||
|
||||
# Resolve OU (Over/Under)
|
||||
elif market_type.startswith("OU"):
|
||||
line = 2.5
|
||||
if "1.5" in pick_str: line = 1.5
|
||||
elif "3.5" in pick_str: line = 3.5
|
||||
|
||||
is_over = total_goals > line
|
||||
is_under = total_goals < line # Simplification (usually line is X.5 so no draw)
|
||||
|
||||
if "ÜST" in pick_str or "OVER" in pick_str:
|
||||
if is_over: is_won = True
|
||||
elif "ALT" in pick_str or "UNDER" in pick_str:
|
||||
if is_under: is_won = True
|
||||
|
||||
# Resolve BTTS
|
||||
elif market_type == "BTTS":
|
||||
if home_score > 0 and away_score > 0:
|
||||
if "VAR" in pick_str: is_won = True
|
||||
else:
|
||||
if "YOK" in pick_str: is_won = True
|
||||
|
||||
# Resolve DC (Double Chance) - Simplified
|
||||
elif market_type == "DC":
|
||||
if "1X" in pick_str and (home_score >= away_score): is_won = True
|
||||
elif "X2" in pick_str and (away_score >= home_score): is_won = True
|
||||
elif "12" in pick_str and (home_score != away_score): is_won = True
|
||||
|
||||
if is_won:
|
||||
winning_bets += 1
|
||||
profit = main_pick_odds - 1.0
|
||||
total_profit += profit
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
|
||||
# ─── REPORT ───
|
||||
print("\n" + "="*60)
|
||||
print("📈 BACKTEST RESULTS (With NEW Skip Logic)")
|
||||
print("="*60)
|
||||
print(f"Total Historical Matches Analyzed: {len(predictions)}")
|
||||
print(f"🚫 Bets SKIPPED (Low Conf/Bad Value): {skipped_bets}")
|
||||
print(f"✅ Bets PLAYED: {total_bets}")
|
||||
|
||||
if total_bets > 0:
|
||||
win_rate = (winning_bets / total_bets) * 100
|
||||
roi = (total_profit / total_bets) * 100
|
||||
|
||||
print(f"🏆 Winning Bets: {winning_bets}")
|
||||
print(f"💀 Losing Bets: {total_bets - winning_bets}")
|
||||
print("-" * 40)
|
||||
print(f" Win Rate: {win_rate:.2f}%")
|
||||
print(f"💰 Total Profit (Units): {total_profit:.2f}")
|
||||
print(f"📊 ROI: {roi:.2f}%")
|
||||
|
||||
if roi > 0:
|
||||
print("🟢 STRATEGY IS PROFITABLE!")
|
||||
else:
|
||||
print("🔴 STRATEGY IS LOSING (Adjust thresholds!)")
|
||||
else:
|
||||
print("⚠️ No bets were played. Thresholds might be too high.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
simulate_backtest()
|
||||
@@ -1,164 +0,0 @@
|
||||
"""
|
||||
SNIPER Backtest
|
||||
===============
|
||||
Sadece en yüksek güvenilirlik ve değere sahip bahisleri oynar.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from datetime import datetime
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
MATCH_IDS = [
|
||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
||||
]
|
||||
|
||||
def run_sniper_backtest():
|
||||
print("🎯 SNIPER BACKTEST: SADECE NET OLANLAR")
|
||||
print("="*60)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
||||
cur.execute(f"""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
l.name as league_name
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN leagues l ON m.league_id = l.id
|
||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
||||
""", MATCH_IDS)
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 Analiz edilecek {len(rows)} maç var.\n")
|
||||
|
||||
try:
|
||||
orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
print(f"[{i+1}/{len(rows)}] {home} vs {away} ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred:
|
||||
print("⚠️ Veri Yok")
|
||||
continue
|
||||
|
||||
pick_data = pred.get("expert_recommendation", {}).get("main_pick") or pred.get("main_pick", {})
|
||||
pick = pick_data.get("pick") or pick_data.get("market_type")
|
||||
conf = pick_data.get("confidence", 0)
|
||||
odds = pick_data.get("odds", 0)
|
||||
|
||||
# SNIPER FİLTRELERİ
|
||||
if conf < 75:
|
||||
print(f"🚫 PASS (Conf: {conf:.0f}%)")
|
||||
continue
|
||||
if odds < 1.35:
|
||||
print(f"🚫 PASS (Odds: {odds:.2f} çok düşük)")
|
||||
continue
|
||||
|
||||
# Value Control
|
||||
implied = 1.0 / odds
|
||||
if (conf/100) < implied:
|
||||
print(f"🚫 PASS (Negatif Value)")
|
||||
continue
|
||||
|
||||
# OYNA
|
||||
total_bet += 1
|
||||
won = False
|
||||
pick_clean = str(pick).upper()
|
||||
|
||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
elif "3.5" in pick_clean: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"✅ WON! (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"❌ LOST! ({pick} @ {odds:.2f})")
|
||||
|
||||
except Exception as e:
|
||||
print(f"💥 Hata: {e}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🎯 SNIPER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Oynanan: {total_bet}")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
print(f"Kazanma Oranı: %{(total_won/total_bet)*100:.1f}" if total_bet > 0 else "Kazanma Oranı: N/A")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
|
||||
if total_profit > 0:
|
||||
print("🟢 PARA KAZANDIK!")
|
||||
else:
|
||||
print("🔴 PARA KAYBETTİK!")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_sniper_backtest()
|
||||
@@ -1,162 +0,0 @@
|
||||
"""
|
||||
Strict Sniper Backtest (Calibrated)
|
||||
===================================
|
||||
Sadece Güven > %75 ve Oran > 1.30 olan bahisleri oynar.
|
||||
Modelin şişirilmiş özgüvenini elemek için yapıldı.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_strict_backtest():
|
||||
print("🎯 STRICT SNIPER BACKTEST (Conf > 75%)")
|
||||
print("="*60)
|
||||
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç taranıyor. Sadece NET OLANLAR oynanacak...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
# Check all picks for a HIGH CONFIDENCE bet
|
||||
candidates = []
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
best_bet = None
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
# Access attributes safely (Dict or Object)
|
||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
||||
pick = c.get("pick", "") if isinstance(c, dict) else getattr(c, 'pick', "")
|
||||
|
||||
# STRICT CRITERIA
|
||||
if conf >= 75.0 and odds >= 1.30:
|
||||
# Check Value (Edge)
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
if edge > -5.0: # Tolerant edge
|
||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
||||
best_bet = c
|
||||
|
||||
if best_bet:
|
||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
||||
|
||||
# Resolution
|
||||
won = False
|
||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif pick in ["1X", "X2"]:
|
||||
if "1X" in pick and h_score >= a_score: won = True
|
||||
elif "X2" in pick and a_score >= h_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
total_bet += 1
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({conf:.0f}%) -> WON (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({conf:.0f}%) -> LOST")
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("🎯 STRICT SNIPER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Oynanan Bahis: {total_bet}")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
|
||||
if total_bet > 0:
|
||||
win_rate = (total_won / total_bet) * 100
|
||||
roi = (total_profit / total_bet) * 100
|
||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 PARA KAYBETTİK!")
|
||||
else:
|
||||
print("⚠️ Yeteri kadar NET maç bulunamadı.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_strict_backtest()
|
||||
@@ -1,94 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
||||
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
|
||||
|
||||
def _resolve_dsn() -> str:
|
||||
env_path = AI_ENGINE_DIR / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("DATABASE_URL="):
|
||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
||||
|
||||
|
||||
def _fetch_matches(dsn: str, limit: int = 60) -> list[str]:
|
||||
query = """
|
||||
SELECT m.id
|
||||
FROM matches m
|
||||
WHERE m.status = 'FT'
|
||||
AND m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(query, (limit,))
|
||||
return [str(row["id"]) for row in cur.fetchall()]
|
||||
|
||||
|
||||
def _score_prediction(package: dict) -> dict[str, float]:
|
||||
rows = package.get("bet_summary", []) or []
|
||||
playable = [row for row in rows if row.get("playable")]
|
||||
return {
|
||||
"playable_count": float(len(playable)),
|
||||
"avg_edge": round(
|
||||
sum(float(row.get("ev_edge", 0.0)) for row in playable) / len(playable),
|
||||
4,
|
||||
)
|
||||
if playable
|
||||
else 0.0,
|
||||
"avg_confidence": round(
|
||||
sum(float(row.get("calibrated_confidence", 0.0)) for row in playable)
|
||||
/ len(playable),
|
||||
2,
|
||||
)
|
||||
if playable
|
||||
else 0.0,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dsn = _resolve_dsn()
|
||||
match_ids = _fetch_matches(dsn)
|
||||
orchestrator = SingleMatchOrchestrator()
|
||||
|
||||
results: list[dict[str, object]] = []
|
||||
for match_id in match_ids:
|
||||
orchestrator.engine_mode = "v25"
|
||||
v25 = orchestrator.analyze_match(match_id)
|
||||
orchestrator.engine_mode = "v26"
|
||||
v26 = orchestrator.analyze_match(match_id)
|
||||
if not v25 or not v26:
|
||||
continue
|
||||
results.append(
|
||||
{
|
||||
"match_id": match_id,
|
||||
"v25": _score_prediction(v25),
|
||||
"v26": _score_prediction(v26),
|
||||
"v25_main": (v25.get("main_pick") or {}).get("pick"),
|
||||
"v26_main": (v26.get("main_pick") or {}).get("pick"),
|
||||
}
|
||||
)
|
||||
|
||||
out_path = AI_ENGINE_DIR / "reports" / "backtest_v26_shadow.json"
|
||||
out_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
||||
print(f"[OK] Shadow backtest summary written to {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,505 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
||||
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
|
||||
|
||||
STRATEGIES = ("v25_aggressive", "v26_surprise", "v26_aggressive", "v26_main_htft")
|
||||
REVERSAL_LABELS = ("1/2", "2/1", "X/1", "X/2")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchContext:
|
||||
match_id: str
|
||||
match_date_ms: int
|
||||
league: str
|
||||
home_team: str
|
||||
away_team: str
|
||||
final_home: int
|
||||
final_away: int
|
||||
ht_home: Optional[int]
|
||||
ht_away: Optional[int]
|
||||
|
||||
@property
|
||||
def match_name(self) -> str:
|
||||
return f"{self.home_team} vs {self.away_team}"
|
||||
|
||||
@property
|
||||
def final_score(self) -> str:
|
||||
return f"{self.final_home}-{self.final_away}"
|
||||
|
||||
@property
|
||||
def ht_score(self) -> str:
|
||||
if self.ht_home is None or self.ht_away is None:
|
||||
return "-"
|
||||
return f"{self.ht_home}-{self.ht_away}"
|
||||
|
||||
|
||||
def _resolve_dsn() -> str:
|
||||
env_path = AI_ENGINE_DIR / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("DATABASE_URL="):
|
||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
||||
|
||||
|
||||
def _fetch_matches(dsn: str, limit: int) -> list[MatchContext]:
|
||||
query = """
|
||||
SELECT
|
||||
m.id,
|
||||
m.mst_utc,
|
||||
COALESCE(l.name, 'Unknown League') AS league,
|
||||
COALESCE(ht.name, 'Home') AS home_team,
|
||||
COALESCE(at.name, 'Away') AS away_team,
|
||||
COALESCE(m.score_home, 0) AS score_home,
|
||||
COALESCE(m.score_away, 0) AS score_away,
|
||||
m.ht_score_home,
|
||||
m.ht_score_away
|
||||
FROM matches m
|
||||
LEFT JOIN leagues l ON l.id = m.league_id
|
||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
||||
WHERE m.status = 'FT'
|
||||
AND m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND m.ht_score_home IS NOT NULL
|
||||
AND m.ht_score_away IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(query, (limit,))
|
||||
rows = cur.fetchall()
|
||||
return [
|
||||
MatchContext(
|
||||
match_id=str(row["id"]),
|
||||
match_date_ms=int(row["mst_utc"] or 0),
|
||||
league=str(row["league"] or "Unknown League"),
|
||||
home_team=str(row["home_team"] or "Home"),
|
||||
away_team=str(row["away_team"] or "Away"),
|
||||
final_home=int(row["score_home"] or 0),
|
||||
final_away=int(row["score_away"] or 0),
|
||||
ht_home=int(row["ht_score_home"]) if row.get("ht_score_home") is not None else None,
|
||||
ht_away=int(row["ht_score_away"]) if row.get("ht_score_away") is not None else None,
|
||||
)
|
||||
for row in rows
|
||||
]
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _outcome_symbol(home: int, away: int) -> str:
|
||||
if home > away:
|
||||
return "1"
|
||||
if home < away:
|
||||
return "2"
|
||||
return "X"
|
||||
|
||||
|
||||
def _resolve_htft(pick: str, context: MatchContext) -> Dict[str, Any]:
|
||||
if not pick or "/" not in str(pick):
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "htft_pick_invalid"}
|
||||
actual = f"{_outcome_symbol(context.ht_home or 0, context.ht_away or 0)}/{_outcome_symbol(context.final_home, context.final_away)}"
|
||||
won = str(pick).strip().upper() == actual
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
|
||||
def _market_odds(odds: Dict[str, Any], market: str, pick: str) -> float:
|
||||
mapping = {
|
||||
"HTFT": {
|
||||
"1/1": "htft_11",
|
||||
"1/X": "htft_1x",
|
||||
"1/2": "htft_12",
|
||||
"X/1": "htft_x1",
|
||||
"X/X": "htft_xx",
|
||||
"X/2": "htft_x2",
|
||||
"2/1": "htft_21",
|
||||
"2/X": "htft_2x",
|
||||
"2/2": "htft_22",
|
||||
},
|
||||
"MS": {"1": "ms_h", "X": "ms_d", "2": "ms_a"},
|
||||
}
|
||||
key = mapping.get(market, {}).get(str(pick))
|
||||
if not key:
|
||||
return 0.0
|
||||
value = _safe_float((odds or {}).get(key))
|
||||
return value if value > 1.0 else 0.0
|
||||
|
||||
|
||||
def _evaluate_pick(
|
||||
*,
|
||||
strategy: str,
|
||||
market: str,
|
||||
pick: str,
|
||||
odds: Any,
|
||||
playable: bool,
|
||||
confidence: Any,
|
||||
extra: Optional[Dict[str, Any]],
|
||||
context: MatchContext,
|
||||
) -> Dict[str, Any]:
|
||||
odds_value = _safe_float(odds)
|
||||
if market == "HT/FT":
|
||||
market = "HTFT"
|
||||
resolution = _resolve_htft(pick, context) if market == "HTFT" else {
|
||||
"result": "UNRESOLVED",
|
||||
"won": None,
|
||||
"note": "non_htft_market",
|
||||
}
|
||||
counted = bool(playable and market == "HTFT" and odds_value > 1.01 and resolution["result"] in {"WON", "LOST"})
|
||||
profit = 0.0
|
||||
if counted:
|
||||
profit = (odds_value - 1.0) if resolution["result"] == "WON" else -1.0
|
||||
row = {
|
||||
"strategy": strategy,
|
||||
"market": market,
|
||||
"pick": pick,
|
||||
"odds": round(odds_value, 2),
|
||||
"playable": playable,
|
||||
"confidence": round(_safe_float(confidence), 1),
|
||||
"result": resolution["result"],
|
||||
"counted_in_roi": counted,
|
||||
"profit_flat": round(profit, 4),
|
||||
"resolution_note": resolution["note"],
|
||||
}
|
||||
if extra:
|
||||
row.update(extra)
|
||||
return row
|
||||
|
||||
|
||||
def _extract_strategy_rows(
|
||||
*,
|
||||
context: MatchContext,
|
||||
odds_data: Dict[str, Any],
|
||||
v25: Dict[str, Any],
|
||||
v26: Dict[str, Any],
|
||||
) -> Dict[str, Optional[Dict[str, Any]]]:
|
||||
strategies: Dict[str, Optional[Dict[str, Any]]] = {name: None for name in STRATEGIES}
|
||||
|
||||
v25_aggressive = v25.get("aggressive_pick") or {}
|
||||
if v25_aggressive.get("pick"):
|
||||
pick = str(v25_aggressive.get("pick"))
|
||||
strategies["v25_aggressive"] = _evaluate_pick(
|
||||
strategy="v25_aggressive",
|
||||
market=str(v25_aggressive.get("market") or "HTFT"),
|
||||
pick=pick,
|
||||
odds=_market_odds(odds_data, "HTFT", pick),
|
||||
playable=True,
|
||||
confidence=v25_aggressive.get("confidence"),
|
||||
extra={
|
||||
"source": "v25.aggressive_pick",
|
||||
"reversal_pick": pick,
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
v26_surprise = v26.get("surprise_pick") or {}
|
||||
v26_hunter = v26.get("surprise_hunter") or {}
|
||||
if v26_surprise.get("pick"):
|
||||
pick = str(v26_surprise.get("raw_pick") or v26_surprise.get("pick"))
|
||||
strategies["v26_surprise"] = _evaluate_pick(
|
||||
strategy="v26_surprise",
|
||||
market=str(v26_surprise.get("market") or "HTFT"),
|
||||
pick=pick,
|
||||
odds=v26_surprise.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
||||
playable=bool(v26_surprise.get("playable")),
|
||||
confidence=v26_surprise.get("calibrated_confidence", v26_surprise.get("confidence")),
|
||||
extra={
|
||||
"source": "v26.surprise_pick",
|
||||
"surprise_score": round(_safe_float(v26_surprise.get("surprise_score")), 1),
|
||||
"support_score": round(_safe_float(v26_surprise.get("support_score")), 1),
|
||||
"reversal_pick": v26_hunter.get("reversal_pick"),
|
||||
"reversal_prob": round(_safe_float(v26_hunter.get("reversal_prob")), 4),
|
||||
"favorite_gap": round(_safe_float(v26_hunter.get("favorite_gap")), 3),
|
||||
"favorite_odd": round(_safe_float(v26_hunter.get("favorite_odd")), 2),
|
||||
"odds_band_score": round(_safe_float(v26_hunter.get("odds_band_score")), 3),
|
||||
"odds_band_label": str(v26_hunter.get("odds_band_label") or ""),
|
||||
"league_reversal_rate": round(_safe_float(v26_hunter.get("league_reversal_rate")), 4),
|
||||
"league_strict_rev_rate": round(_safe_float(v26_hunter.get("league_strict_rev_rate")), 4),
|
||||
"referee_strict_rev_rate": round(_safe_float(v26_hunter.get("referee_strict_rev_rate")), 4),
|
||||
"reason_codes": ",".join(v26_hunter.get("reason_codes", [])),
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
v26_aggressive = v26.get("aggressive_pick") or {}
|
||||
if v26_aggressive.get("pick"):
|
||||
pick = str(v26_aggressive.get("pick"))
|
||||
strategies["v26_aggressive"] = _evaluate_pick(
|
||||
strategy="v26_aggressive",
|
||||
market=str(v26_aggressive.get("market") or "HTFT"),
|
||||
pick=pick,
|
||||
odds=v26_aggressive.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
||||
playable=True,
|
||||
confidence=v26_aggressive.get("confidence"),
|
||||
extra={
|
||||
"source": "v26.aggressive_pick",
|
||||
"reversal_pick": pick,
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
v26_main = v26.get("main_pick") or {}
|
||||
if str(v26_main.get("market") or "") == "HTFT" and v26_main.get("pick"):
|
||||
pick = str(v26_main.get("raw_pick") or v26_main.get("pick"))
|
||||
strategies["v26_main_htft"] = _evaluate_pick(
|
||||
strategy="v26_main_htft",
|
||||
market="HTFT",
|
||||
pick=pick,
|
||||
odds=v26_main.get("odds") or _market_odds(odds_data, "HTFT", pick),
|
||||
playable=bool(v26_main.get("playable")),
|
||||
confidence=v26_main.get("calibrated_confidence", v26_main.get("confidence")),
|
||||
extra={
|
||||
"source": "v26.main_pick",
|
||||
"pick_reason": v26_main.get("pick_reason"),
|
||||
"surprise_score": round(_safe_float(v26_main.get("surprise_score")), 1),
|
||||
},
|
||||
context=context,
|
||||
)
|
||||
|
||||
return strategies
|
||||
|
||||
|
||||
def _summarize_bucket(bucket: Dict[str, float]) -> Dict[str, Any]:
|
||||
played = int(bucket["played"])
|
||||
won = int(bucket["won"])
|
||||
lost = int(bucket["lost"])
|
||||
candidate = int(bucket["candidate"])
|
||||
profit = round(bucket["profit"], 4)
|
||||
roi = round((profit / played) * 100.0, 2) if played else 0.0
|
||||
hit = round((won / played) * 100.0, 2) if played else 0.0
|
||||
return {
|
||||
"candidates": candidate,
|
||||
"played": played,
|
||||
"won": won,
|
||||
"lost": lost,
|
||||
"profit_flat": profit,
|
||||
"roi_flat_pct": roi,
|
||||
"hit_rate_pct": hit,
|
||||
}
|
||||
|
||||
|
||||
def _format_date(ms: int) -> str:
|
||||
return datetime.fromtimestamp(ms / 1000, tz=timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _build_markdown(report: Dict[str, Any]) -> str:
|
||||
lines: list[str] = []
|
||||
lines.append("# HT/FT + Upset Backtest")
|
||||
lines.append("")
|
||||
lines.append(f"- Sample: last {report['sample_size']} finished football matches")
|
||||
lines.append("- Scope: only HT/FT reversal and upset-oriented picks")
|
||||
lines.append("- ROI: flat `1 unit` per played pick")
|
||||
lines.append(f"- Generated at: {report['generated_at']}")
|
||||
lines.append("")
|
||||
lines.append("## Strategy Summary")
|
||||
lines.append("")
|
||||
lines.append("| Strategy | Candidates | Played | Won | Lost | Hit Rate | Profit | ROI |")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|---:|")
|
||||
for strategy in STRATEGIES:
|
||||
payload = report["summary"]["strategies"][strategy]
|
||||
lines.append(
|
||||
f"| {strategy} | {payload['candidates']} | {payload['played']} | {payload['won']} | "
|
||||
f"{payload['lost']} | {payload['hit_rate_pct']}% | {payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## v26 Surprise By Reversal Type")
|
||||
lines.append("")
|
||||
lines.append("| Reversal | Candidates | Played | Won | Lost | Profit | ROI |")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|")
|
||||
for reversal, payload in report["summary"]["v26_surprise_by_pick"].items():
|
||||
lines.append(
|
||||
f"| {reversal} | {payload['candidates']} | {payload['played']} | {payload['won']} | "
|
||||
f"{payload['lost']} | {payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## Match Detail")
|
||||
lines.append("")
|
||||
lines.append("| Date | Match | HT | FT | v25 aggressive | v26 surprise | v26 aggressive | v26 main HTFT |")
|
||||
lines.append("|---|---|---|---|---|---|---|---|")
|
||||
for match in report["matches"]:
|
||||
lines.append(
|
||||
f"| {_format_date(match['match_date_ms'])} | {match['match_name']} | {match['ht_score']} | {match['final_score']} | "
|
||||
f"{match['v25_aggressive']} | {match['v26_surprise']} | {match['v26_aggressive']} | {match['v26_main_htft']} |"
|
||||
)
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="HT/FT + upset focused backtest.")
|
||||
parser.add_argument("--limit", type=int, default=120, help="Number of finished matches to analyze.")
|
||||
args = parser.parse_args()
|
||||
|
||||
dsn = _resolve_dsn()
|
||||
orchestrator = SingleMatchOrchestrator()
|
||||
matches = _fetch_matches(dsn, max(1, args.limit))
|
||||
|
||||
strategy_buckets: Dict[str, Dict[str, float]] = {name: defaultdict(float) for name in STRATEGIES}
|
||||
v26_reversal_buckets: Dict[str, Dict[str, float]] = {label: defaultdict(float) for label in REVERSAL_LABELS}
|
||||
report_matches: list[Dict[str, Any]] = []
|
||||
csv_rows: list[Dict[str, Any]] = []
|
||||
|
||||
for context in matches:
|
||||
data = orchestrator._load_match_data(context.match_id) # noqa: SLF001
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
orchestrator.engine_mode = "v25"
|
||||
v25 = orchestrator.analyze_match(context.match_id) or {}
|
||||
orchestrator.engine_mode = "v26"
|
||||
v26 = orchestrator.analyze_match(context.match_id) or {}
|
||||
|
||||
extracted = _extract_strategy_rows(
|
||||
context=context,
|
||||
odds_data=data.odds_data or {},
|
||||
v25=v25,
|
||||
v26=v26,
|
||||
)
|
||||
|
||||
match_row: Dict[str, Any] = {
|
||||
"match_id": context.match_id,
|
||||
"match_name": context.match_name,
|
||||
"league": context.league,
|
||||
"match_date_ms": context.match_date_ms,
|
||||
"ht_score": context.ht_score,
|
||||
"final_score": context.final_score,
|
||||
}
|
||||
|
||||
for strategy, payload in extracted.items():
|
||||
if payload:
|
||||
strategy_buckets[strategy]["candidate"] += 1
|
||||
if payload["counted_in_roi"]:
|
||||
strategy_buckets[strategy]["played"] += 1
|
||||
if payload["result"] == "WON":
|
||||
strategy_buckets[strategy]["won"] += 1
|
||||
else:
|
||||
strategy_buckets[strategy]["lost"] += 1
|
||||
strategy_buckets[strategy]["profit"] += payload["profit_flat"]
|
||||
|
||||
if strategy == "v26_surprise":
|
||||
reversal_label = str(payload.get("reversal_pick") or "")
|
||||
if reversal_label in v26_reversal_buckets:
|
||||
v26_reversal_buckets[reversal_label]["candidate"] += 1
|
||||
if payload["counted_in_roi"]:
|
||||
v26_reversal_buckets[reversal_label]["played"] += 1
|
||||
if payload["result"] == "WON":
|
||||
v26_reversal_buckets[reversal_label]["won"] += 1
|
||||
else:
|
||||
v26_reversal_buckets[reversal_label]["lost"] += 1
|
||||
v26_reversal_buckets[reversal_label]["profit"] += payload["profit_flat"]
|
||||
|
||||
summary = (
|
||||
f"{payload['pick']} ({payload['result']}, {'played' if payload['counted_in_roi'] else 'not played'}, {payload['profit_flat']:+.2f})"
|
||||
)
|
||||
match_row[strategy] = summary
|
||||
|
||||
csv_rows.append(
|
||||
{
|
||||
"match_id": context.match_id,
|
||||
"date": _format_date(context.match_date_ms),
|
||||
"league": context.league,
|
||||
"match": context.match_name,
|
||||
"ht_score": context.ht_score,
|
||||
"final_score": context.final_score,
|
||||
**payload,
|
||||
}
|
||||
)
|
||||
else:
|
||||
match_row[strategy] = "-"
|
||||
|
||||
report_matches.append(match_row)
|
||||
|
||||
report = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"sample_size": len(report_matches),
|
||||
"summary": {
|
||||
"strategies": {
|
||||
strategy: _summarize_bucket(bucket)
|
||||
for strategy, bucket in strategy_buckets.items()
|
||||
},
|
||||
"v26_surprise_by_pick": {
|
||||
label: _summarize_bucket(bucket)
|
||||
for label, bucket in v26_reversal_buckets.items()
|
||||
},
|
||||
},
|
||||
"matches": report_matches,
|
||||
}
|
||||
|
||||
report_dir = AI_ENGINE_DIR / "reports"
|
||||
json_path = report_dir / "backtest_v26_shadow_htft_upset.json"
|
||||
csv_path = report_dir / "backtest_v26_shadow_htft_upset.csv"
|
||||
md_path = report_dir / "backtest_v26_shadow_htft_upset.md"
|
||||
|
||||
json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(
|
||||
handle,
|
||||
fieldnames=[
|
||||
"match_id",
|
||||
"date",
|
||||
"league",
|
||||
"match",
|
||||
"ht_score",
|
||||
"final_score",
|
||||
"strategy",
|
||||
"market",
|
||||
"pick",
|
||||
"odds",
|
||||
"playable",
|
||||
"confidence",
|
||||
"result",
|
||||
"counted_in_roi",
|
||||
"profit_flat",
|
||||
"resolution_note",
|
||||
"source",
|
||||
"reversal_pick",
|
||||
"reversal_prob",
|
||||
"favorite_gap",
|
||||
"favorite_odd",
|
||||
"support_score",
|
||||
"odds_band_score",
|
||||
"odds_band_label",
|
||||
"league_reversal_rate",
|
||||
"league_strict_rev_rate",
|
||||
"referee_strict_rev_rate",
|
||||
"surprise_score",
|
||||
"reason_codes",
|
||||
"pick_reason",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(csv_rows)
|
||||
md_path.write_text(_build_markdown(report), encoding="utf-8")
|
||||
|
||||
print(f"[OK] JSON report written to {json_path}")
|
||||
print(f"[OK] CSV report written to {csv_path}")
|
||||
print(f"[OK] Markdown report written to {md_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,810 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, Optional
|
||||
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
|
||||
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(AI_ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(AI_ENGINE_DIR))
|
||||
|
||||
from services.single_match_orchestrator import SingleMatchOrchestrator
|
||||
from utils.top_leagues import load_top_league_ids
|
||||
|
||||
|
||||
MARKET_ORDER = [
|
||||
"MS",
|
||||
"DC",
|
||||
"OU15",
|
||||
"OU25",
|
||||
"OU35",
|
||||
"BTTS",
|
||||
"HT",
|
||||
"HT_OU05",
|
||||
"HT_OU15",
|
||||
"HTFT",
|
||||
"OE",
|
||||
"CARDS",
|
||||
"HCAP",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class MatchContext:
|
||||
match_id: str
|
||||
match_date_ms: int
|
||||
league_id: Optional[str]
|
||||
league: str
|
||||
home_team: str
|
||||
away_team: str
|
||||
final_home: int
|
||||
final_away: int
|
||||
ht_home: Optional[int]
|
||||
ht_away: Optional[int]
|
||||
total_cards: Optional[float]
|
||||
|
||||
@property
|
||||
def match_name(self) -> str:
|
||||
return f"{self.home_team} vs {self.away_team}"
|
||||
|
||||
@property
|
||||
def final_score(self) -> str:
|
||||
return f"{self.final_home}-{self.final_away}"
|
||||
|
||||
@property
|
||||
def ht_score(self) -> Optional[str]:
|
||||
if self.ht_home is None or self.ht_away is None:
|
||||
return None
|
||||
return f"{self.ht_home}-{self.ht_away}"
|
||||
|
||||
@property
|
||||
def total_goals(self) -> int:
|
||||
return self.final_home + self.final_away
|
||||
|
||||
@property
|
||||
def total_ht_goals(self) -> Optional[int]:
|
||||
if self.ht_home is None or self.ht_away is None:
|
||||
return None
|
||||
return self.ht_home + self.ht_away
|
||||
|
||||
|
||||
def _resolve_dsn() -> str:
|
||||
env_path = AI_ENGINE_DIR / ".env"
|
||||
if env_path.exists():
|
||||
for line in env_path.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("DATABASE_URL="):
|
||||
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
||||
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
||||
|
||||
|
||||
def _fetch_matches(
|
||||
dsn: str,
|
||||
limit: int,
|
||||
top_league_ids: Optional[list[str]] = None,
|
||||
) -> list[MatchContext]:
|
||||
query = """
|
||||
SELECT
|
||||
m.id,
|
||||
m.mst_utc,
|
||||
m.league_id,
|
||||
COALESCE(l.name, 'Unknown League') AS league,
|
||||
COALESCE(ht.name, 'Home') AS home_team,
|
||||
COALESCE(at.name, 'Away') AS away_team,
|
||||
COALESCE(m.score_home, 0) AS score_home,
|
||||
COALESCE(m.score_away, 0) AS score_away,
|
||||
m.ht_score_home,
|
||||
m.ht_score_away,
|
||||
cards.total_cards
|
||||
FROM matches m
|
||||
LEFT JOIN leagues l ON l.id = m.league_id
|
||||
LEFT JOIN teams ht ON ht.id = m.home_team_id
|
||||
LEFT JOIN teams at ON at.id = m.away_team_id
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
mpe.match_id,
|
||||
SUM(
|
||||
CASE
|
||||
WHEN mpe.event_type::text LIKE '%%yellow_card%%' THEN 1
|
||||
WHEN mpe.event_type::text LIKE '%%red_card%%' THEN 2
|
||||
ELSE 1
|
||||
END
|
||||
)::float AS total_cards
|
||||
FROM match_player_events mpe
|
||||
WHERE mpe.event_type::text LIKE '%%card%%'
|
||||
GROUP BY mpe.match_id
|
||||
) cards ON cards.match_id = m.id
|
||||
WHERE m.status = 'FT'
|
||||
AND m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
"""
|
||||
params: list[Any] = []
|
||||
if top_league_ids:
|
||||
query += " AND m.league_id = ANY(%s)"
|
||||
params.append(top_league_ids)
|
||||
query += """
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
params.append(limit)
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
||||
cur.execute(query, params)
|
||||
rows = cur.fetchall()
|
||||
|
||||
results: list[MatchContext] = []
|
||||
for row in rows:
|
||||
results.append(
|
||||
MatchContext(
|
||||
match_id=str(row["id"]),
|
||||
match_date_ms=int(row["mst_utc"] or 0),
|
||||
league_id=str(row["league_id"]) if row.get("league_id") else None,
|
||||
league=str(row["league"] or "Unknown League"),
|
||||
home_team=str(row["home_team"] or "Home"),
|
||||
away_team=str(row["away_team"] or "Away"),
|
||||
final_home=int(row["score_home"] or 0),
|
||||
final_away=int(row["score_away"] or 0),
|
||||
ht_home=(
|
||||
int(row["ht_score_home"])
|
||||
if row.get("ht_score_home") is not None
|
||||
else None
|
||||
),
|
||||
ht_away=(
|
||||
int(row["ht_score_away"])
|
||||
if row.get("ht_score_away") is not None
|
||||
else None
|
||||
),
|
||||
total_cards=(
|
||||
float(row["total_cards"])
|
||||
if row.get("total_cards") is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _odds_band(odds: float) -> str:
|
||||
if odds < 1.5:
|
||||
return "<1.50"
|
||||
if odds < 1.8:
|
||||
return "1.50-1.79"
|
||||
if odds < 2.1:
|
||||
return "1.80-2.09"
|
||||
if odds < 2.5:
|
||||
return "2.10-2.49"
|
||||
return "2.50+"
|
||||
|
||||
|
||||
def _confidence_band(confidence: float) -> str:
|
||||
if confidence < 55.0:
|
||||
return "<55"
|
||||
if confidence < 65.0:
|
||||
return "55-64.9"
|
||||
if confidence < 75.0:
|
||||
return "65-74.9"
|
||||
return "75+"
|
||||
|
||||
|
||||
def _edge_band(edge: float) -> str:
|
||||
if edge < 0.03:
|
||||
return "<0.03"
|
||||
if edge < 0.06:
|
||||
return "0.03-0.059"
|
||||
if edge < 0.10:
|
||||
return "0.06-0.099"
|
||||
return "0.10+"
|
||||
|
||||
|
||||
def _top_n_buckets(rows: Iterable[tuple[str, float]], limit: int = 10) -> list[dict[str, Any]]:
|
||||
ranked = sorted(rows, key=lambda item: (-item[1], item[0]))
|
||||
return [
|
||||
{"label": label, "count": int(count)}
|
||||
for label, count in ranked[:limit]
|
||||
]
|
||||
|
||||
|
||||
def _summarize_v26_losses(csv_rows: list[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
losses = [
|
||||
row for row in csv_rows
|
||||
if row.get("model") == "v26.shadow"
|
||||
and bool(row.get("counted_in_roi"))
|
||||
and row.get("result") == "LOST"
|
||||
]
|
||||
by_market: Dict[str, float] = defaultdict(float)
|
||||
by_league: Dict[str, float] = defaultdict(float)
|
||||
by_pick: Dict[str, float] = defaultdict(float)
|
||||
by_odds_band: Dict[str, float] = defaultdict(float)
|
||||
by_conf_band: Dict[str, float] = defaultdict(float)
|
||||
by_edge_band: Dict[str, float] = defaultdict(float)
|
||||
|
||||
for row in losses:
|
||||
market = str(row.get("market") or "UNKNOWN")
|
||||
league = str(row.get("league") or "Unknown League")
|
||||
pick = str(row.get("pick") or "")
|
||||
odds = _safe_float(row.get("odds"))
|
||||
confidence = _safe_float(row.get("confidence"))
|
||||
edge = _safe_float(row.get("edge"))
|
||||
|
||||
by_market[market] += 1
|
||||
by_league[league] += 1
|
||||
by_pick[f"{market} {pick}".strip()] += 1
|
||||
by_odds_band[_odds_band(odds)] += 1
|
||||
by_conf_band[_confidence_band(confidence)] += 1
|
||||
by_edge_band[_edge_band(edge)] += 1
|
||||
|
||||
return {
|
||||
"lost_bets": len(losses),
|
||||
"by_market": _top_n_buckets(by_market.items(), limit=20),
|
||||
"by_league": _top_n_buckets(by_league.items(), limit=15),
|
||||
"by_pick": _top_n_buckets(by_pick.items(), limit=15),
|
||||
"by_odds_band": _top_n_buckets(by_odds_band.items(), limit=10),
|
||||
"by_confidence_band": _top_n_buckets(by_conf_band.items(), limit=10),
|
||||
"by_edge_band": _top_n_buckets(by_edge_band.items(), limit=10),
|
||||
}
|
||||
|
||||
|
||||
def _safe_float(value: Any) -> float:
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def _normalize_text(value: Any) -> str:
|
||||
text = str(value or "").strip().upper()
|
||||
return (
|
||||
text.replace("İ", "I")
|
||||
.replace("İ", "I")
|
||||
.replace("Ş", "S")
|
||||
.replace("Ğ", "G")
|
||||
.replace("Ü", "U")
|
||||
.replace("Ö", "O")
|
||||
.replace("Ç", "C")
|
||||
)
|
||||
|
||||
|
||||
def _outcome_symbol(home: int, away: int) -> str:
|
||||
if home > away:
|
||||
return "1"
|
||||
if home < away:
|
||||
return "2"
|
||||
return "X"
|
||||
|
||||
|
||||
def _resolve_pick(
|
||||
market: str,
|
||||
pick: str,
|
||||
context: MatchContext,
|
||||
) -> Dict[str, Any]:
|
||||
market_code = _normalize_text(market).replace("/", "")
|
||||
pick_text = str(pick or "").strip()
|
||||
pick_norm = _normalize_text(pick_text)
|
||||
|
||||
if not market_code or not pick_norm:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "pick_missing"}
|
||||
|
||||
if market_code == "HTFT":
|
||||
market_code = "HTFT"
|
||||
if market_code == "HTFT" or market_code == "HTFT":
|
||||
if context.ht_home is None or context.ht_away is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
||||
if "/" not in pick_text:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "htft_pick_invalid"}
|
||||
ht_pick, ft_pick = pick_text.split("/", 1)
|
||||
actual = f"{_outcome_symbol(context.ht_home, context.ht_away)}/{_outcome_symbol(context.final_home, context.final_away)}"
|
||||
won = f"{_normalize_text(ht_pick)}/{_normalize_text(ft_pick)}" == actual
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "MS":
|
||||
actual = _outcome_symbol(context.final_home, context.final_away)
|
||||
won = pick_norm in {actual, f"MS {actual}"}
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "DC":
|
||||
actual = _outcome_symbol(context.final_home, context.final_away)
|
||||
winning = {
|
||||
"1X": {"1", "X"},
|
||||
"X2": {"X", "2"},
|
||||
"12": {"1", "2"},
|
||||
}
|
||||
won = actual in winning.get(pick_norm, set())
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code in {"OU15", "OU25", "OU35", "HTOU05", "HTOU15", "HT_OU05", "HT_OU15"}:
|
||||
if market_code in {"HTOU05", "HTOU15", "HT_OU05", "HT_OU15"}:
|
||||
if context.total_ht_goals is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
||||
total = context.total_ht_goals
|
||||
line = 0.5 if "05" in market_code else 1.5
|
||||
else:
|
||||
total = context.total_goals
|
||||
line = {"OU15": 1.5, "OU25": 2.5, "OU35": 3.5}[market_code]
|
||||
|
||||
if "UST" in pick_norm or "OVER" in pick_norm:
|
||||
won = total > line
|
||||
side = "OVER"
|
||||
elif "ALT" in pick_norm or "UNDER" in pick_norm:
|
||||
won = total < line
|
||||
side = "UNDER"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ou_side_unknown"}
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual_total={total} side={side} line={line}",
|
||||
}
|
||||
|
||||
if market_code == "BTTS":
|
||||
both_scored = context.final_home > 0 and context.final_away > 0
|
||||
if "VAR" in pick_norm or "YES" in pick_norm:
|
||||
won = both_scored
|
||||
side = "YES"
|
||||
elif "YOK" in pick_norm or pick_norm.endswith("NO") or pick_norm == "NO":
|
||||
won = not both_scored
|
||||
side = "NO"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "btts_side_unknown"}
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual_btts={'YES' if both_scored else 'NO'} side={side}",
|
||||
}
|
||||
|
||||
if market_code == "HT":
|
||||
if context.ht_home is None or context.ht_away is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "ht_score_missing"}
|
||||
actual = _outcome_symbol(context.ht_home, context.ht_away)
|
||||
won = pick_norm == actual
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "OE":
|
||||
actual = "EVEN" if context.total_goals % 2 == 0 else "ODD"
|
||||
if pick_norm in {"CIFT", "EVEN"}:
|
||||
wanted = "EVEN"
|
||||
elif pick_norm in {"TEK", "ODD"}:
|
||||
wanted = "ODD"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "oe_pick_unknown"}
|
||||
won = actual == wanted
|
||||
return {"result": "WON" if won else "LOST", "won": won, "note": f"actual={actual}"}
|
||||
|
||||
if market_code == "CARDS":
|
||||
if context.total_cards is None:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "cards_missing"}
|
||||
if "UST" in pick_norm or "OVER" in pick_norm:
|
||||
won = context.total_cards > 4.5
|
||||
side = "OVER"
|
||||
elif "ALT" in pick_norm or "UNDER" in pick_norm:
|
||||
won = context.total_cards < 4.5
|
||||
side = "UNDER"
|
||||
else:
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "cards_side_unknown"}
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual_cards={context.total_cards:.1f} side={side} line=4.5",
|
||||
}
|
||||
|
||||
if market_code == "HCAP":
|
||||
adjusted_home = context.final_home - 1.0
|
||||
adjusted_away = float(context.final_away)
|
||||
if adjusted_home > adjusted_away:
|
||||
actual = "1"
|
||||
elif adjusted_home < adjusted_away:
|
||||
actual = "2"
|
||||
else:
|
||||
actual = "X"
|
||||
won = pick_norm == actual
|
||||
return {
|
||||
"result": "WON" if won else "LOST",
|
||||
"won": won,
|
||||
"note": f"actual={actual} line_home=-1.0",
|
||||
}
|
||||
|
||||
return {"result": "UNRESOLVED", "won": None, "note": "market_not_supported"}
|
||||
|
||||
|
||||
def _evaluate_row(
|
||||
market: str,
|
||||
pick: str,
|
||||
odds: Any,
|
||||
playable: bool,
|
||||
stake_units: Any,
|
||||
context: MatchContext,
|
||||
) -> Dict[str, Any]:
|
||||
resolution = _resolve_pick(market, pick, context)
|
||||
odds_value = _safe_float(odds)
|
||||
stake_value = _safe_float(stake_units)
|
||||
counted = bool(playable and odds_value > 1.01 and resolution["result"] in {"WON", "LOST"})
|
||||
|
||||
flat_profit = 0.0
|
||||
stake_profit = 0.0
|
||||
if counted:
|
||||
flat_profit = (odds_value - 1.0) if resolution["result"] == "WON" else -1.0
|
||||
stake_profit = flat_profit * (stake_value if stake_value > 0 else 1.0)
|
||||
|
||||
return {
|
||||
"result": resolution["result"],
|
||||
"won": resolution["won"],
|
||||
"resolution_note": resolution["note"],
|
||||
"counted_in_roi": counted,
|
||||
"profit_flat": round(flat_profit, 4),
|
||||
"profit_stake": round(stake_profit, 4),
|
||||
}
|
||||
|
||||
|
||||
def _summarize_bucket(bucket: Dict[str, float]) -> Dict[str, Any]:
|
||||
played = int(bucket["played"])
|
||||
won = int(bucket["won"])
|
||||
lost = int(bucket["lost"])
|
||||
unresolved = int(bucket["unresolved"])
|
||||
profit = round(bucket["profit"], 4)
|
||||
roi = round((profit / played) * 100.0, 2) if played else 0.0
|
||||
win_rate = round((won / played) * 100.0, 2) if played else 0.0
|
||||
return {
|
||||
"played": played,
|
||||
"won": won,
|
||||
"lost": lost,
|
||||
"unresolved": unresolved,
|
||||
"profit_flat": profit,
|
||||
"roi_flat_pct": roi,
|
||||
"win_rate_pct": win_rate,
|
||||
}
|
||||
|
||||
|
||||
def _format_date(ms: int) -> str:
|
||||
if ms <= 0:
|
||||
return "-"
|
||||
dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
|
||||
return dt.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def _build_markdown_report(report: Dict[str, Any]) -> str:
|
||||
lines: list[str] = []
|
||||
lines.append("# v25 vs v26.shadow ROI Report")
|
||||
lines.append("")
|
||||
lines.append(f"- Sample: last {report['sample_size']} finished football matches")
|
||||
if report.get("top_leagues_only"):
|
||||
lines.append("- Filter: top leagues only")
|
||||
lines.append("- ROI calculation: flat `1 unit` per playable and resolvable bet")
|
||||
lines.append(f"- Generated at: {report['generated_at']}")
|
||||
lines.append("")
|
||||
lines.append("## Overall Summary")
|
||||
lines.append("")
|
||||
lines.append("| Model | Played | Won | Lost | Win Rate | Profit | ROI | Main Pick ROI | Main Pick W/L |")
|
||||
lines.append("|---|---:|---:|---:|---:|---:|---:|---:|---|")
|
||||
for model_name, payload in report["summary"]["models"].items():
|
||||
main = payload["main_pick"]
|
||||
lines.append(
|
||||
f"| {model_name} | {payload['all_playable']['played']} | {payload['all_playable']['won']} | "
|
||||
f"{payload['all_playable']['lost']} | {payload['all_playable']['win_rate_pct']}% | "
|
||||
f"{payload['all_playable']['profit_flat']:+.2f} | {payload['all_playable']['roi_flat_pct']:+.2f}% | "
|
||||
f"{main['roi_flat_pct']:+.2f}% | {main['won']}/{main['played']} |"
|
||||
)
|
||||
lines.append("")
|
||||
lines.append("## Market Summary")
|
||||
lines.append("")
|
||||
lines.append("| Model | Market | Played | Won | Lost | Profit | ROI |")
|
||||
lines.append("|---|---|---:|---:|---:|---:|---:|")
|
||||
for model_name, markets in report["summary"]["markets"].items():
|
||||
for market_name in MARKET_ORDER:
|
||||
payload = markets.get(market_name)
|
||||
if not payload or payload["played"] == 0:
|
||||
continue
|
||||
lines.append(
|
||||
f"| {model_name} | {market_name} | {payload['played']} | {payload['won']} | {payload['lost']} | "
|
||||
f"{payload['profit_flat']:+.2f} | {payload['roi_flat_pct']:+.2f}% |"
|
||||
)
|
||||
lines.append("")
|
||||
loss_summary = report["summary"].get("v26_loss_analysis", {})
|
||||
if loss_summary:
|
||||
lines.append("## v26 Loss Analysis")
|
||||
lines.append("")
|
||||
lines.append(f"- Lost bets: {loss_summary.get('lost_bets', 0)}")
|
||||
lines.append("")
|
||||
lines.append("| Bucket | Top Items |")
|
||||
lines.append("|---|---|")
|
||||
for label, key in (
|
||||
("By market", "by_market"),
|
||||
("By league", "by_league"),
|
||||
("By pick", "by_pick"),
|
||||
("By odds band", "by_odds_band"),
|
||||
("By confidence band", "by_confidence_band"),
|
||||
("By edge band", "by_edge_band"),
|
||||
):
|
||||
items = loss_summary.get(key) or []
|
||||
rendered = ", ".join(f"{item['label']} ({item['count']})" for item in items[:6]) or "-"
|
||||
lines.append(f"| {label} | {rendered} |")
|
||||
lines.append("")
|
||||
lines.append("## Match By Match")
|
||||
lines.append("")
|
||||
lines.append("| Date | Match | Score | v25 Main | v25 Played Picks | v25 Profit | v26 Main | v26 Played Picks | v26 Profit |")
|
||||
lines.append("|---|---|---|---|---|---:|---|---|---:|")
|
||||
for match in report["matches"]:
|
||||
v25 = match["models"]["v25"]
|
||||
v26 = match["models"]["v26.shadow"]
|
||||
lines.append(
|
||||
f"| {_format_date(match['match_date_ms'])} | {match['match_name']} | {match['final_score']} | "
|
||||
f"{v25['main_pick']['summary']} | {v25['played_picks_summary']} | {v25['profit_flat']:+.2f} | "
|
||||
f"{v26['main_pick']['summary']} | {v26['played_picks_summary']} | {v26['profit_flat']:+.2f} |"
|
||||
)
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Detailed ROI backtest for v25 vs v26.shadow.",
|
||||
)
|
||||
parser.add_argument("--limit", type=int, default=60, help="Number of finished matches to analyze.")
|
||||
parser.add_argument(
|
||||
"--top-leagues-only",
|
||||
action="store_true",
|
||||
help="Only analyze matches whose league_id exists in top_leagues.json.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
dsn = _resolve_dsn()
|
||||
top_league_ids = sorted(load_top_league_ids()) if args.top_leagues_only else None
|
||||
matches = _fetch_matches(dsn, max(1, args.limit), top_league_ids=top_league_ids)
|
||||
orchestrator = SingleMatchOrchestrator()
|
||||
|
||||
report_matches: list[Dict[str, Any]] = []
|
||||
model_aggregate: Dict[str, Dict[str, float]] = {
|
||||
"v25": defaultdict(float),
|
||||
"v26.shadow": defaultdict(float),
|
||||
}
|
||||
main_pick_aggregate: Dict[str, Dict[str, float]] = {
|
||||
"v25": defaultdict(float),
|
||||
"v26.shadow": defaultdict(float),
|
||||
}
|
||||
market_aggregate: Dict[str, Dict[str, Dict[str, float]]] = {
|
||||
"v25": defaultdict(lambda: defaultdict(float)),
|
||||
"v26.shadow": defaultdict(lambda: defaultdict(float)),
|
||||
}
|
||||
csv_rows: list[Dict[str, Any]] = []
|
||||
|
||||
for context in matches:
|
||||
match_payload = {
|
||||
"match_id": context.match_id,
|
||||
"match_name": context.match_name,
|
||||
"league": context.league,
|
||||
"match_date_ms": context.match_date_ms,
|
||||
"final_score": context.final_score,
|
||||
"ht_score": context.ht_score,
|
||||
"total_cards": context.total_cards,
|
||||
"models": {},
|
||||
}
|
||||
|
||||
for model_name, mode in (("v25", "v25"), ("v26.shadow", "v26")):
|
||||
orchestrator.engine_mode = mode
|
||||
package = orchestrator.analyze_match(context.match_id) or {}
|
||||
rows = package.get("bet_summary") or []
|
||||
evaluated_rows: list[Dict[str, Any]] = []
|
||||
match_profit = 0.0
|
||||
|
||||
for row in rows:
|
||||
market = str(row.get("market") or "")
|
||||
pick = str(row.get("pick") or "")
|
||||
evaluation = _evaluate_row(
|
||||
market=market,
|
||||
pick=pick,
|
||||
odds=row.get("odds"),
|
||||
playable=bool(row.get("playable")),
|
||||
stake_units=row.get("stake_units"),
|
||||
context=context,
|
||||
)
|
||||
combined = {
|
||||
"market": market,
|
||||
"pick": pick,
|
||||
"playable": bool(row.get("playable")),
|
||||
"bet_grade": row.get("bet_grade"),
|
||||
"odds": round(_safe_float(row.get("odds")), 2),
|
||||
"calibrated_confidence": round(_safe_float(row.get("calibrated_confidence")), 1),
|
||||
"edge": round(_safe_float(row.get("ev_edge", row.get("edge"))), 4),
|
||||
"stake_units": round(_safe_float(row.get("stake_units")), 2),
|
||||
**evaluation,
|
||||
}
|
||||
evaluated_rows.append(combined)
|
||||
|
||||
if combined["counted_in_roi"]:
|
||||
bucket = market_aggregate[model_name][market]
|
||||
bucket["played"] += 1
|
||||
if combined["result"] == "WON":
|
||||
bucket["won"] += 1
|
||||
else:
|
||||
bucket["lost"] += 1
|
||||
bucket["profit"] += combined["profit_flat"]
|
||||
|
||||
model_bucket = model_aggregate[model_name]
|
||||
model_bucket["played"] += 1
|
||||
if combined["result"] == "WON":
|
||||
model_bucket["won"] += 1
|
||||
else:
|
||||
model_bucket["lost"] += 1
|
||||
model_bucket["profit"] += combined["profit_flat"]
|
||||
match_profit += combined["profit_flat"]
|
||||
elif combined["playable"]:
|
||||
model_aggregate[model_name]["unresolved"] += 1
|
||||
market_aggregate[model_name][market]["unresolved"] += 1
|
||||
|
||||
csv_rows.append(
|
||||
{
|
||||
"match_id": context.match_id,
|
||||
"date": _format_date(context.match_date_ms),
|
||||
"league": context.league,
|
||||
"match": context.match_name,
|
||||
"final_score": context.final_score,
|
||||
"ht_score": context.ht_score or "",
|
||||
"model": model_name,
|
||||
"market": market,
|
||||
"pick": pick,
|
||||
"playable": combined["playable"],
|
||||
"bet_grade": combined["bet_grade"],
|
||||
"odds": combined["odds"],
|
||||
"confidence": combined["calibrated_confidence"],
|
||||
"edge": combined["edge"],
|
||||
"result": combined["result"],
|
||||
"counted_in_roi": combined["counted_in_roi"],
|
||||
"profit_flat": combined["profit_flat"],
|
||||
"resolution_note": combined["resolution_note"],
|
||||
}
|
||||
)
|
||||
|
||||
main_pick = package.get("main_pick") or {}
|
||||
main_eval = _evaluate_row(
|
||||
market=str(main_pick.get("market") or ""),
|
||||
pick=str(main_pick.get("pick") or ""),
|
||||
odds=main_pick.get("odds"),
|
||||
playable=bool(main_pick.get("playable")),
|
||||
stake_units=main_pick.get("stake_units"),
|
||||
context=context,
|
||||
)
|
||||
main_pick_summary = {
|
||||
"market": main_pick.get("market"),
|
||||
"pick": main_pick.get("pick"),
|
||||
"playable": bool(main_pick.get("playable")),
|
||||
"odds": round(_safe_float(main_pick.get("odds")), 2),
|
||||
"confidence": round(
|
||||
_safe_float(
|
||||
main_pick.get("calibrated_confidence", main_pick.get("confidence"))
|
||||
),
|
||||
1,
|
||||
),
|
||||
"edge": round(_safe_float(main_pick.get("ev_edge", main_pick.get("edge"))), 4),
|
||||
**main_eval,
|
||||
}
|
||||
|
||||
if main_pick_summary["counted_in_roi"]:
|
||||
summary_suffix = (
|
||||
f"{main_pick_summary['result']}, played, {main_pick_summary['profit_flat']:+.2f}"
|
||||
)
|
||||
elif main_pick_summary.get("market") and main_pick_summary.get("pick"):
|
||||
summary_suffix = f"{main_pick_summary['result']}, not played"
|
||||
else:
|
||||
summary_suffix = ""
|
||||
|
||||
if main_pick_summary["counted_in_roi"]:
|
||||
bucket = main_pick_aggregate[model_name]
|
||||
bucket["played"] += 1
|
||||
if main_pick_summary["result"] == "WON":
|
||||
bucket["won"] += 1
|
||||
else:
|
||||
bucket["lost"] += 1
|
||||
bucket["profit"] += main_pick_summary["profit_flat"]
|
||||
elif main_pick_summary["playable"]:
|
||||
main_pick_aggregate[model_name]["unresolved"] += 1
|
||||
|
||||
main_pick_summary["summary"] = (
|
||||
f"{main_pick_summary['market']} {main_pick_summary['pick']} "
|
||||
f"({summary_suffix})"
|
||||
if main_pick_summary.get("market") and main_pick_summary.get("pick")
|
||||
else "No main pick"
|
||||
)
|
||||
|
||||
played_rows = [row for row in evaluated_rows if row["counted_in_roi"]]
|
||||
played_picks_summary = (
|
||||
"; ".join(
|
||||
f"{row['market']} {row['pick']}={row['result']} ({row['profit_flat']:+.2f})"
|
||||
for row in played_rows
|
||||
)
|
||||
if played_rows
|
||||
else "-"
|
||||
)
|
||||
|
||||
match_payload["models"][model_name] = {
|
||||
"main_pick": main_pick_summary,
|
||||
"profit_flat": round(match_profit, 4),
|
||||
"played_picks_summary": played_picks_summary,
|
||||
"played_picks": played_rows,
|
||||
"all_picks": evaluated_rows,
|
||||
}
|
||||
|
||||
report_matches.append(match_payload)
|
||||
|
||||
summary = {
|
||||
"models": {
|
||||
model_name: {
|
||||
"all_playable": _summarize_bucket(model_aggregate[model_name]),
|
||||
"main_pick": _summarize_bucket(main_pick_aggregate[model_name]),
|
||||
}
|
||||
for model_name in ("v25", "v26.shadow")
|
||||
},
|
||||
"markets": {
|
||||
model_name: {
|
||||
market_name: _summarize_bucket(bucket)
|
||||
for market_name, bucket in sorted(
|
||||
market_aggregate[model_name].items(),
|
||||
key=lambda item: (
|
||||
MARKET_ORDER.index(item[0]) if item[0] in MARKET_ORDER else 999,
|
||||
item[0],
|
||||
),
|
||||
)
|
||||
}
|
||||
for model_name in ("v25", "v26.shadow")
|
||||
},
|
||||
"v26_loss_analysis": _summarize_v26_losses(csv_rows),
|
||||
}
|
||||
|
||||
report = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"sample_size": len(report_matches),
|
||||
"top_leagues_only": bool(args.top_leagues_only),
|
||||
"summary": summary,
|
||||
"matches": report_matches,
|
||||
}
|
||||
|
||||
report_dir = AI_ENGINE_DIR / "reports"
|
||||
json_path = report_dir / "backtest_v26_shadow_roi_detail.json"
|
||||
csv_path = report_dir / "backtest_v26_shadow_roi_picks.csv"
|
||||
md_path = report_dir / "backtest_v26_shadow_roi_report.md"
|
||||
|
||||
json_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||
|
||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(
|
||||
handle,
|
||||
fieldnames=[
|
||||
"match_id",
|
||||
"date",
|
||||
"league",
|
||||
"match",
|
||||
"final_score",
|
||||
"ht_score",
|
||||
"model",
|
||||
"market",
|
||||
"pick",
|
||||
"playable",
|
||||
"bet_grade",
|
||||
"odds",
|
||||
"confidence",
|
||||
"edge",
|
||||
"result",
|
||||
"counted_in_roi",
|
||||
"profit_flat",
|
||||
"resolution_note",
|
||||
],
|
||||
)
|
||||
writer.writeheader()
|
||||
writer.writerows(csv_rows)
|
||||
|
||||
md_path.write_text(_build_markdown_report(report), encoding="utf-8")
|
||||
|
||||
print(f"[OK] JSON report written to {json_path}")
|
||||
print(f"[OK] CSV report written to {csv_path}")
|
||||
print(f"[OK] Markdown report written to {md_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,230 +0,0 @@
|
||||
"""
|
||||
Backtest the live V2 predictor stack against recent finished football matches.
|
||||
|
||||
This script uses the same path as production:
|
||||
database -> feature extractor -> betting predictor -> quant ranking.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
ROOT_DIR = Path(__file__).resolve().parents[1]
|
||||
if str(ROOT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT_DIR))
|
||||
|
||||
from core.quant import MarketPick, analyze_market
|
||||
from data.database import dispose_engine, get_session
|
||||
from features.extractor import extract_features
|
||||
from models.betting_engine import get_predictor
|
||||
|
||||
|
||||
@dataclass
|
||||
class BacktestStats:
|
||||
sampled_matches: int = 0
|
||||
analyzed_matches: int = 0
|
||||
skipped_matches: int = 0
|
||||
ms_correct: int = 0
|
||||
ou25_correct: int = 0
|
||||
btts_correct: int = 0
|
||||
main_pick_count: int = 0
|
||||
main_pick_correct: int = 0
|
||||
playable_pick_count: int = 0
|
||||
playable_pick_correct: int = 0
|
||||
playable_units_staked: float = 0.0
|
||||
playable_units_profit: float = 0.0
|
||||
|
||||
|
||||
def _parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--limit", type=int, default=50)
|
||||
parser.add_argument("--days", type=int, default=45)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def _actual_ms(score_home: int, score_away: int) -> str:
|
||||
if score_home > score_away:
|
||||
return "1"
|
||||
if score_home < score_away:
|
||||
return "2"
|
||||
return "X"
|
||||
|
||||
|
||||
def _actual_ou25(score_home: int, score_away: int) -> str:
|
||||
return "Over" if (score_home + score_away) > 2 else "Under"
|
||||
|
||||
|
||||
def _actual_btts(score_home: int, score_away: int) -> str:
|
||||
return "Yes" if score_home > 0 and score_away > 0 else "No"
|
||||
|
||||
|
||||
def _odds_map_from_features(feats) -> dict[str, dict[str, float]]:
|
||||
return {
|
||||
"MS": {"1": feats.odds_home, "X": feats.odds_draw, "2": feats.odds_away},
|
||||
"OU25": {"Under": feats.odds_under25, "Over": feats.odds_over25},
|
||||
"BTTS": {"No": feats.odds_btts_no, "Yes": feats.odds_btts_yes},
|
||||
}
|
||||
|
||||
|
||||
def _best_pick(feats, all_probs: dict[str, dict[str, float]]) -> MarketPick | None:
|
||||
odds_map = _odds_map_from_features(feats)
|
||||
picks = [
|
||||
analyze_market("MS", all_probs["MS"], odds_map["MS"], feats.data_quality_score),
|
||||
analyze_market("OU25", all_probs["OU25"], odds_map["OU25"], feats.data_quality_score),
|
||||
analyze_market("BTTS", all_probs["BTTS"], odds_map["BTTS"], feats.data_quality_score),
|
||||
]
|
||||
ranked = sorted(
|
||||
[pick for pick in picks if pick.pick],
|
||||
key=lambda pick: pick.play_score,
|
||||
reverse=True,
|
||||
)
|
||||
return ranked[0] if ranked else None
|
||||
|
||||
|
||||
def _pick_won(pick: MarketPick, actuals: dict[str, str]) -> bool:
|
||||
return actuals.get(pick.market) == pick.pick
|
||||
|
||||
|
||||
async def _load_match_rows(limit: int, days: int) -> list[dict[str, object]]:
|
||||
min_mst_utc = days * 86400000
|
||||
query = text("""
|
||||
SELECT
|
||||
m.id,
|
||||
m.match_name,
|
||||
m.score_home,
|
||||
m.score_away,
|
||||
m.mst_utc
|
||||
FROM matches m
|
||||
WHERE m.sport = 'football'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND m.score_away IS NOT NULL
|
||||
AND m.mst_utc >= (
|
||||
EXTRACT(EPOCH FROM NOW()) * 1000 - :min_mst_utc
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM odd_categories oc
|
||||
WHERE oc.match_id = m.id
|
||||
AND oc.name IN ('Maç Sonucu', '2,5 Alt/Üst', 'Karşılıklı Gol')
|
||||
)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT :limit
|
||||
""")
|
||||
async with get_session() as session:
|
||||
result = await session.execute(
|
||||
query,
|
||||
{"limit": limit, "min_mst_utc": min_mst_utc},
|
||||
)
|
||||
rows = result.mappings().all()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
async def _run(limit: int, days: int) -> BacktestStats:
|
||||
stats = BacktestStats()
|
||||
predictor = get_predictor()
|
||||
rows = await _load_match_rows(limit, days)
|
||||
stats.sampled_matches = len(rows)
|
||||
|
||||
async with get_session() as session:
|
||||
for row in rows:
|
||||
match_id = str(row["id"])
|
||||
score_home = int(row["score_home"])
|
||||
score_away = int(row["score_away"])
|
||||
feats = await extract_features(session, match_id)
|
||||
|
||||
if feats is None:
|
||||
stats.skipped_matches += 1
|
||||
continue
|
||||
|
||||
if feats.data_quality_score <= 0.0:
|
||||
stats.skipped_matches += 1
|
||||
continue
|
||||
|
||||
all_probs = predictor.predict_all(feats.to_model_array(), feats)
|
||||
stats.analyzed_matches += 1
|
||||
|
||||
actuals = {
|
||||
"MS": _actual_ms(score_home, score_away),
|
||||
"OU25": _actual_ou25(score_home, score_away),
|
||||
"BTTS": _actual_btts(score_home, score_away),
|
||||
}
|
||||
|
||||
if max(all_probs["MS"], key=all_probs["MS"].get) == actuals["MS"]:
|
||||
stats.ms_correct += 1
|
||||
if max(all_probs["OU25"], key=all_probs["OU25"].get) == actuals["OU25"]:
|
||||
stats.ou25_correct += 1
|
||||
if max(all_probs["BTTS"], key=all_probs["BTTS"].get) == actuals["BTTS"]:
|
||||
stats.btts_correct += 1
|
||||
|
||||
best_pick = _best_pick(feats, all_probs)
|
||||
if best_pick is None:
|
||||
continue
|
||||
|
||||
stats.main_pick_count += 1
|
||||
if _pick_won(best_pick, actuals):
|
||||
stats.main_pick_correct += 1
|
||||
|
||||
if best_pick.playable:
|
||||
stats.playable_pick_count += 1
|
||||
stats.playable_units_staked += best_pick.stake_units
|
||||
if _pick_won(best_pick, actuals):
|
||||
stats.playable_pick_correct += 1
|
||||
stats.playable_units_profit += best_pick.stake_units * (best_pick.odds - 1.0)
|
||||
else:
|
||||
stats.playable_units_profit -= best_pick.stake_units
|
||||
|
||||
return stats
|
||||
|
||||
|
||||
def _pct(numerator: int, denominator: int) -> float:
|
||||
if denominator <= 0:
|
||||
return 0.0
|
||||
return round((numerator / denominator) * 100.0, 2)
|
||||
|
||||
|
||||
def _roi(profit: float, staked: float) -> float:
|
||||
if staked <= 0:
|
||||
return 0.0
|
||||
return round((profit / staked) * 100.0, 2)
|
||||
|
||||
|
||||
def _print_summary(stats: BacktestStats) -> None:
|
||||
print("=== V2 Runtime Backtest ===")
|
||||
print(f"Sampled matches : {stats.sampled_matches}")
|
||||
print(f"Analyzed matches : {stats.analyzed_matches}")
|
||||
print(f"Skipped matches : {stats.skipped_matches}")
|
||||
print(f"MS accuracy : {_pct(stats.ms_correct, stats.analyzed_matches)}%")
|
||||
print(f"OU2.5 accuracy : {_pct(stats.ou25_correct, stats.analyzed_matches)}%")
|
||||
print(f"BTTS accuracy : {_pct(stats.btts_correct, stats.analyzed_matches)}%")
|
||||
print(
|
||||
"Main pick accuracy : "
|
||||
f"{_pct(stats.main_pick_correct, stats.main_pick_count)}% "
|
||||
f"({stats.main_pick_correct}/{stats.main_pick_count})"
|
||||
)
|
||||
print(
|
||||
"Playable accuracy : "
|
||||
f"{_pct(stats.playable_pick_correct, stats.playable_pick_count)}% "
|
||||
f"({stats.playable_pick_correct}/{stats.playable_pick_count})"
|
||||
)
|
||||
print(f"Units staked : {stats.playable_units_staked:.2f}")
|
||||
print(f"Units profit : {stats.playable_units_profit:.2f}")
|
||||
print(f"ROI : {_roi(stats.playable_units_profit, stats.playable_units_staked)}%")
|
||||
|
||||
|
||||
async def _main() -> None:
|
||||
args = _parse_args()
|
||||
try:
|
||||
stats = await _run(args.limit, args.days)
|
||||
_print_summary(stats)
|
||||
finally:
|
||||
await dispose_engine()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(_main())
|
||||
@@ -1,147 +0,0 @@
|
||||
"""
|
||||
Value Hunter Backtest
|
||||
=====================
|
||||
Sadece modelin büroyu yendiği (Pozitif Edge) maçları oynar.
|
||||
"""
|
||||
|
||||
import os, sys, json, time, psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR): ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
MATCH_IDS = [
|
||||
"v2ljcst50nk37x04xwimpi50", "7gz0bhb5yvdssazl3y5946kno", "7ftj7kbu4rzpewxravf3luuc4",
|
||||
"7f1z4e8ch1dm5q677644cky6s", "7ffq3aq3so22iymfdzch63nys", "rrkmeuymz7gzvoz8mplikzdg",
|
||||
"7hegc9covicy699bxsi81xkb8", "7gl7rpr1hjayk3e5ut0gr613o", "7g7d86i3738287xfvyfeffcwk",
|
||||
"7hs4boe4hv80muawocevvx2j8", "7ijhsloieg4t9yp5cxp0duln8", "7ixaiiptli5ek32kuybuni4gk",
|
||||
"7i5sfh41cjpwg4l972dm487x0", "eo7g4wunxxxr8uv45q8p5x638", "7dinds2937w4645wva2rddlas",
|
||||
"7b5ukdhvqh62wtndeqfg01ixg", "7bjptsj24gndoydn7n0202g44", "7cqxf3vo58ewrwmoom5xiyexg",
|
||||
"7bxjl9h2hnf165rlp3o1vfztg", "7eo8zrez08c342rqsezpvq39w", "7as1muhs98vdarlhsean4bspg",
|
||||
"7dwhj8cfxv6v6bzxpu5e3h05w", "7d4vq4417ps84yjzh95bnvvv8", "7ea9z501jgp9kxw3gay4myrkk",
|
||||
"7cd3401itlty6ded7c1wct0yc", "ebgpz9mcije2snv986n6587pw", "i7ar1dkhvcwpxmkyks65ib6c",
|
||||
"lyek7tyy6qk2xjs9vblucnx0", "hdn9qtyn3ysjwbc3i2trantg", "3y2bnssfqlajosiz2gpkn6xhw",
|
||||
"40pehd14s9djjtycujavbex3o", "3xnbfjznzmnwml20akbgnis5w", "2eovi2rcc2l4ha7fpb2w7e1hw",
|
||||
"2bwuikdjyyuithhru8ka8o00k", "2d3pcd76ya9ihi9yotxc553is", "1e9it04z4epy2etdxsffe7m6s",
|
||||
"7af49jgo4iulv1k8cplj9smj8", "5k3vrz619hdu9nx4rnx6uim1g", "amjppgpetnyr0iisi241kgkyc",
|
||||
"coqrhq09kxd16iejvgtzj3mz8", "d8ysan1qdctmkvjaz2adw7aqc", "9ttciz0gtb0z09ev1q5fe0ro4",
|
||||
"9u720o37yaddqu1w6hlszpnh0", "7ijezdjp8t0rjti91ac63hyxg", "72gvdvztbb3dn79jidzzxzcb8",
|
||||
"6uof1v2s6vrpieeml2bwo9tlg", "91dd8ia3m0bxoqzjgyo3ptsk", "3tj1nt3udsbvb9soqn2cs6gpg",
|
||||
"1br5g88o5idtjxka1fr6zg4k4", "akuesquthbmxlzckvnqmgles4"
|
||||
]
|
||||
|
||||
def run_value_hunter():
|
||||
print("💎 VALUE HUNTER: SADECE HATALI ORANLARI YAKALA")
|
||||
print("="*60)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
placeholders = ','.join(['%s'] * len(MATCH_IDS))
|
||||
cur.execute(f"""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.id IN ({placeholders}) AND m.status = 'FT'
|
||||
""", MATCH_IDS)
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
total_edge_found = 0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
# Tüm önerileri kontrol et
|
||||
picks = pred.get("expert_recommendation", {}).get("value_picks", [])
|
||||
if not picks: picks = [pred.get("expert_recommendation", {}).get("main_pick")]
|
||||
|
||||
played_this_match = False
|
||||
|
||||
for pick_data in picks:
|
||||
if not pick_data: continue
|
||||
pick = pick_data.get("pick")
|
||||
conf = pick_data.get("confidence", 0)
|
||||
odds = pick_data.get("odds", 0)
|
||||
edge = pick_data.get("edge", 0)
|
||||
|
||||
# VALUE KURALI: Model bürodan en az %10 daha iyi olmalı
|
||||
if edge < 10: continue
|
||||
if odds < 1.20: continue
|
||||
|
||||
total_bet += 1
|
||||
total_edge_found += edge
|
||||
won = False
|
||||
pick_clean = str(pick).upper()
|
||||
|
||||
if pick_clean in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick_clean in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick_clean in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif "ÜST" in pick_clean or "OVER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick_clean or "UNDER" in pick_clean:
|
||||
line = 2.5
|
||||
if "1.5" in pick_clean: line = 1.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick_clean and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick_clean and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> WON! (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({edge:.0f}% Edge) -> LOST")
|
||||
|
||||
played_this_match = True
|
||||
break # Maç başına tek bahis
|
||||
|
||||
except Exception: pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("💎 VALUE HUNTER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Toplam Value Bulunan Bahis: {total_bet}")
|
||||
print(f"Ortalama Edge: {total_edge_found/total_bet:.1f}%" if total_bet > 0 else "N/A")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 PARA KAYBETTİK!")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_value_hunter()
|
||||
@@ -1,153 +0,0 @@
|
||||
"""
|
||||
Value Sniper Backtest (High Odds)
|
||||
=================================
|
||||
Sadece Oran > 1.50 ve Güven > %70 olan bahisleri oynar.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
sys.path.insert(0, ROOT_DIR)
|
||||
if "scripts" in os.path.basename(AI_DIR):
|
||||
ROOT_DIR = os.path.dirname(ROOT_DIR)
|
||||
|
||||
from services.single_match_orchestrator import get_single_match_orchestrator
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_value_sniper():
|
||||
print("💰 VALUE SNIPER BACKTEST (Odds > 1.50)")
|
||||
print("="*60)
|
||||
|
||||
leagues_path = os.path.join(ROOT_DIR, "top_leagues.json")
|
||||
with open(leagues_path, 'r') as f:
|
||||
top_leagues = json.load(f)
|
||||
league_ids = tuple(str(lid) for lid in top_leagues)
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.match_name, m.home_team_id, m.away_team_id,
|
||||
m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s
|
||||
AND m.status = 'FT'
|
||||
AND m.score_home IS NOT NULL
|
||||
AND EXISTS (SELECT 1 FROM odd_categories oc WHERE oc.match_id = m.id)
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 500
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç taranıyor...\n")
|
||||
|
||||
try: orchestrator = get_single_match_orchestrator()
|
||||
except Exception as e:
|
||||
print(f"❌ AI Hatası: {e}")
|
||||
return
|
||||
|
||||
total_bet = 0
|
||||
total_won = 0
|
||||
total_profit = 0.0
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
match_id = str(row['id'])
|
||||
home = row['home_team'] or "?"
|
||||
away = row['away_team'] or "?"
|
||||
h_score = row['score_home'] or 0
|
||||
a_score = row['score_away'] or 0
|
||||
|
||||
try:
|
||||
pred = orchestrator.analyze_match(match_id)
|
||||
if not pred: continue
|
||||
|
||||
candidates = []
|
||||
if pred.get("expert_recommendation"):
|
||||
rec = pred["expert_recommendation"]
|
||||
if rec.get("main_pick"): candidates.append(rec["main_pick"])
|
||||
if rec.get("value_picks"): candidates.extend(rec["value_picks"])
|
||||
elif pred.get("main_pick"):
|
||||
candidates.append(pred["main_pick"])
|
||||
|
||||
best_bet = None
|
||||
for c in candidates:
|
||||
if not c: continue
|
||||
conf = c.get("confidence", 0) if isinstance(c, dict) else getattr(c, 'confidence', 0)
|
||||
odds = c.get("odds", 0) if isinstance(c, dict) else getattr(c, 'odds', 0)
|
||||
|
||||
# VALUE CRITERIA: Odds > 1.50 AND Conf > 70%
|
||||
if conf >= 70.0 and odds >= 1.50:
|
||||
# Check Edge
|
||||
implied = 1.0 / odds
|
||||
edge = ((conf/100) - implied) * 100
|
||||
if edge > 0: # Must be positive value
|
||||
if best_bet is None or (conf > (best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0))):
|
||||
best_bet = c
|
||||
|
||||
if best_bet:
|
||||
pick = str(best_bet.get("pick") if isinstance(best_bet, dict) else getattr(best_bet, 'pick', "")).upper()
|
||||
conf = best_bet.get("confidence", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'confidence', 0)
|
||||
odds = best_bet.get("odds", 0) if isinstance(best_bet, dict) else getattr(best_bet, 'odds', 0)
|
||||
|
||||
won = False
|
||||
if pick in ["1", "MS 1"] and h_score > a_score: won = True
|
||||
elif pick in ["X", "MS X"] and h_score == a_score: won = True
|
||||
elif pick in ["2", "MS 2"] and a_score > h_score: won = True
|
||||
elif "ÜST" in pick or "OVER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) > line: won = True
|
||||
elif "ALT" in pick or "UNDER" in pick:
|
||||
line = 2.5
|
||||
if "1.5" in pick: line = 1.5
|
||||
elif "3.5" in pick: line = 3.5
|
||||
if (h_score + a_score) < line: won = True
|
||||
elif "VAR" in pick and h_score > 0 and a_score > 0: won = True
|
||||
elif "YOK" in pick and (h_score == 0 or a_score == 0): won = True
|
||||
|
||||
total_bet += 1
|
||||
if won:
|
||||
total_won += 1
|
||||
profit = odds - 1.0
|
||||
total_profit += profit
|
||||
print(f"[{i+1}] ✅ {home} vs {away} | {pick} ({odds:.2f}) -> WON (+{profit:.2f})")
|
||||
else:
|
||||
total_profit -= 1.0
|
||||
print(f"[{i+1}] ❌ {home} vs {away} | {pick} ({odds:.2f}) -> LOST")
|
||||
|
||||
except: pass
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("💰 VALUE SNIPER SONUÇLARI")
|
||||
print("="*60)
|
||||
print(f"Oynanan Bahis: {total_bet}")
|
||||
print(f"Kazanılan: {total_won}")
|
||||
|
||||
if total_bet > 0:
|
||||
win_rate = (total_won / total_bet) * 100
|
||||
roi = (total_profit / total_bet) * 100
|
||||
print(f"Kazanma Oranı: %{win_rate:.2f}")
|
||||
print(f"Toplam Kâr: {total_profit:.2f} Units")
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 PARA KAYBETTİK!")
|
||||
else:
|
||||
print("⚠️ Yeterli VALUE bulunamadı.")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_value_sniper()
|
||||
@@ -1,136 +0,0 @@
|
||||
"""
|
||||
VQWEN Full Backtest
|
||||
===================
|
||||
Tests all 3 VQWEN models (MS, OU25, BTTS) on 1000 historical matches.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_vqwen_backtest():
|
||||
print("🧠 VQWEN FULL BACKTEST")
|
||||
print("="*60)
|
||||
|
||||
# Load Models
|
||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
||||
try:
|
||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
||||
print("✅ VQWEN MS, OU25, BTTS modelleri yüklendi.")
|
||||
except Exception as e:
|
||||
print(f"❌ Model hatası: {e}")
|
||||
return
|
||||
|
||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1000
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
||||
|
||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
||||
|
||||
for row in rows:
|
||||
oh, od, oa = float(row['oh'] or 0), float(row['od'] or 0), float(row['oa'] or 0)
|
||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
||||
|
||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
||||
|
||||
margin = (1/oh) + (1/od) + (1/oa)
|
||||
|
||||
# MS Prediction
|
||||
f_ms = pd.DataFrame([{'h_form': float(row['h_form']), 'a_form': float(row['a_form']), 'h_xg': h_xg, 'a_xg': a_xg,
|
||||
'pow_diff': h_p - a_p, 'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
||||
'h_sot': 4.0, 'a_sot': 3.0}])
|
||||
ms_probs = model_ms.predict(f_ms)[0]
|
||||
|
||||
# MS Value Bet
|
||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
||||
if odd <= 1.0: continue
|
||||
edge = prob - (1/odd)
|
||||
if edge > 0.05 and prob > 0.50: # Value ve Güven
|
||||
results['ms']['bet'] += 1
|
||||
h, a = row['score_home'], row['score_away']
|
||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
||||
else: results['ms']['profit'] -= 1.0
|
||||
break
|
||||
|
||||
# OU2.5 Prediction
|
||||
f_ou = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'total_xg': h_xg+a_xg, 'h_sot': 4.0, 'a_sot': 3.0}])
|
||||
p_over = model_ou.predict(f_ou)[0]
|
||||
|
||||
# OU2.5 Value Bet
|
||||
if p_over > 0.55 and oh > 1.0: # Sadece örnek olarak over > %55 ise
|
||||
results['ou25']['bet'] += 1
|
||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85 # Ortalama oran
|
||||
else: results['ou25']['profit'] -= 1.0
|
||||
|
||||
# BTTS Prediction
|
||||
f_btts = pd.DataFrame([{'h_xg': h_xg, 'a_xg': a_xg, 'h_sc': float(row['h_sc']), 'a_sc': float(row['a_sc'])}])
|
||||
p_btts = model_btts.predict(f_btts)[0]
|
||||
|
||||
# BTTS Value Bet
|
||||
if p_btts > 0.55:
|
||||
results['btts']['bet'] += 1
|
||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
||||
else: results['btts']['profit'] -= 1.0
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 VQWEN PAZAR BAZLI SONUÇLAR")
|
||||
print("="*60)
|
||||
for mkt in ['ms', 'ou25', 'btts']:
|
||||
r = results[mkt]
|
||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
||||
print(f"{mkt.upper():<10} Oynanan: {r['bet']:<5} Kazanılan: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f} Units")
|
||||
|
||||
total_profit = sum(r['profit'] for r in results.values())
|
||||
print(f"\n💰 TOPLAM KÂR: {total_profit:+.2f} Units")
|
||||
if total_profit > 0: print("🟢 PARA KAZANDIK!")
|
||||
else: print("🔴 ZARARDA")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_vqwen_backtest()
|
||||
@@ -1,141 +0,0 @@
|
||||
"""
|
||||
VQWEN Deep Backtest
|
||||
===================
|
||||
Tests the NEW Deep model with player & card data.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_vqwen_deep_backtest():
|
||||
print("🧠 VQWEN DEEP BACKTEST")
|
||||
print("="*60)
|
||||
|
||||
# Load Models
|
||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
||||
try:
|
||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
||||
print("✅ VQWEN Deep modelleri yüklendi.")
|
||||
except Exception as e:
|
||||
print(f"❌ Model hatası: {e}")
|
||||
return
|
||||
|
||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.home_team_id = m.home_team_id AND m2.score_home > m2.score_away THEN 3 WHEN m2.home_team_id = m.home_team_id AND m2.score_home = m2.score_away THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as h_form,
|
||||
COALESCE((SELECT AVG(CASE WHEN m2.away_team_id = m.away_team_id AND m2.score_away > m2.score_home THEN 3 WHEN m2.away_team_id = m.away_team_id AND m2.score_away = m2.score_home THEN 1 ELSE 0 END) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc LIMIT 5), 0) as a_form,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_sc,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.home_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as h_co,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_sc,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.away_team_id AND m2.status = 'FT' LIMIT 10), 1.2) as a_co,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 0) as h_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 0) as a_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 0) as cards
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1000
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
||||
|
||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
||||
|
||||
for row in rows:
|
||||
oh = float(row['oh'] or 0)
|
||||
od = float(row['od'] or 0)
|
||||
oa = float(row['oa'] or 0)
|
||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
||||
|
||||
h_xg = (float(row['h_sc'] or 1.2) + float(row['a_co'] or 1.2)) / 2
|
||||
a_xg = (float(row['a_sc'] or 1.2) + float(row['h_co'] or 1.2)) / 2
|
||||
h_p = (float(row['h_form'] or 0)*10) + (float(row['h_sc'] or 1.2)*5) - (float(row['h_co'] or 1.2)*5)
|
||||
a_p = (float(row['a_form'] or 0)*10) + (float(row['a_sc'] or 1.2)*5) - (float(row['a_co'] or 1.2)*5)
|
||||
|
||||
margin = (1/oh) + (1/od) + (1/oa)
|
||||
h_sot, a_sot = 4.0, 3.0
|
||||
|
||||
# Features
|
||||
f = pd.DataFrame([{
|
||||
'h_form': float(row['h_form']), 'a_form': float(row['a_form']),
|
||||
'h_xg': h_xg, 'a_xg': a_xg, 'pow_diff': h_p - a_p,
|
||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
||||
'h_sot': h_sot, 'a_sot': a_sot,
|
||||
'h_xi': float(row['h_xi']), 'a_xi': float(row['a_xi']),
|
||||
'xi_diff': float(row['h_xi'] - row['a_xi']),
|
||||
'cards': float(row['cards'])
|
||||
}])
|
||||
|
||||
# MS
|
||||
ms_probs = model_ms.predict(f)[0]
|
||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
||||
if odd <= 1.0: continue
|
||||
edge = prob - (1/odd)
|
||||
if edge > 0.05 and prob > 0.50:
|
||||
results['ms']['bet'] += 1
|
||||
h, a = row['score_home'], row['score_away']
|
||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
||||
else: results['ms']['profit'] -= 1.0
|
||||
break
|
||||
|
||||
# OU2.5
|
||||
p_over = float(model_ou.predict(f)[0])
|
||||
if p_over > 0.55:
|
||||
results['ou25']['bet'] += 1
|
||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
||||
else: results['ou25']['profit'] -= 1.0
|
||||
|
||||
# BTTS
|
||||
p_btts = float(model_btts.predict(f)[0])
|
||||
if p_btts > 0.55:
|
||||
results['btts']['bet'] += 1
|
||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
||||
else: results['btts']['profit'] -= 1.0
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 VQWEN DEEP SONUÇLAR")
|
||||
print("="*60)
|
||||
for mkt in ['ms', 'ou25', 'btts']:
|
||||
r = results[mkt]
|
||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
||||
|
||||
total = sum(r['profit'] for r in results.values())
|
||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_vqwen_deep_backtest()
|
||||
@@ -1,159 +0,0 @@
|
||||
"""
|
||||
VQWEN Final Backtest
|
||||
====================
|
||||
Tests the Final Model (ELO + Rest + Context).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import pickle
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
|
||||
AI_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT_DIR = os.path.dirname(AI_DIR)
|
||||
PROJECT_ROOT = os.path.dirname(ROOT_DIR)
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
return "postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db"
|
||||
|
||||
def run_final_backtest():
|
||||
print("🧠 VQWEN FINAL BACKTEST (ELO + REST)")
|
||||
print("="*60)
|
||||
|
||||
# Load Models
|
||||
mdir = os.path.join(ROOT_DIR, 'models', 'vqwen')
|
||||
try:
|
||||
with open(os.path.join(mdir, 'vqwen_ms.pkl'), 'rb') as f: model_ms = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_ou25.pkl'), 'rb') as f: model_ou = pickle.load(f)
|
||||
with open(os.path.join(mdir, 'vqwen_btts.pkl'), 'rb') as f: model_btts = pickle.load(f)
|
||||
print("✅ VQWEN Final modelleri yüklendi.")
|
||||
except Exception as e:
|
||||
print(f"❌ Model hatası: {e}")
|
||||
return
|
||||
|
||||
with open(os.path.join(PROJECT_ROOT, "top_leagues.json"), 'r') as f:
|
||||
league_ids = tuple(str(lid) for lid in json.load(f))
|
||||
|
||||
dsn = get_clean_dsn()
|
||||
conn = psycopg2.connect(dsn)
|
||||
cur = conn.cursor(cursor_factory=RealDictCursor)
|
||||
|
||||
cur.execute("""
|
||||
SELECT m.id, m.home_team_id, m.away_team_id, m.score_home, m.score_away,
|
||||
m.mst_utc,
|
||||
t1.name as home_team, t2.name as away_team,
|
||||
maf.home_elo, maf.away_elo,
|
||||
COALESCE((SELECT AVG(m2.score_home) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as h_home_goals,
|
||||
COALESCE((SELECT AVG(m2.score_away) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc), 1.2) as a_away_goals,
|
||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.home_team_id = m.home_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as h_rest,
|
||||
COALESCE(EXTRACT(EPOCH FROM (to_timestamp(m.mst_utc/1000) - (SELECT MAX(to_timestamp(m2.mst_utc/1000)) FROM matches m2 WHERE m2.away_team_id = m.away_team_id AND m2.status = 'FT' AND m2.mst_utc < m.mst_utc)) / 86400), 7) as a_rest,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.home_team_id AND mp.is_starting = true), 11) as h_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_participation mp WHERE mp.match_id = m.id AND mp.team_id = m.away_team_id AND mp.is_starting = true), 11) as a_xi,
|
||||
COALESCE((SELECT COUNT(*) FROM match_player_events mpe WHERE mpe.match_id = m.id AND mpe.event_type = 'card'), 4) as cards,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '1' LIMIT 1) as oh,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = 'X' LIMIT 1) as od,
|
||||
(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 = m.id AND oc.name ILIKE 'Maç Sonucu' AND os.name = '2' LIMIT 1) as oa
|
||||
FROM matches m
|
||||
LEFT JOIN teams t1 ON m.home_team_id = t1.id
|
||||
LEFT JOIN teams t2 ON m.away_team_id = t2.id
|
||||
LEFT JOIN football_ai_features maf ON maf.match_id = m.id
|
||||
WHERE m.league_id IN %s AND m.status = 'FT' AND m.score_home IS NOT NULL
|
||||
ORDER BY m.mst_utc DESC
|
||||
LIMIT 1000
|
||||
""", (league_ids,))
|
||||
|
||||
rows = cur.fetchall()
|
||||
print(f"📊 {len(rows)} maç analiz ediliyor...")
|
||||
|
||||
results = {'ms': {'bet': 0, 'won': 0, 'profit': 0}, 'ou25': {'bet': 0, 'won': 0, 'profit': 0}, 'btts': {'bet': 0, 'won': 0, 'profit': 0}}
|
||||
|
||||
for row in rows:
|
||||
oh = float(row['oh'] or 0)
|
||||
od = float(row['od'] or 0)
|
||||
oa = float(row['oa'] or 0)
|
||||
if oh <= 1.0 or od <= 1.0 or oa <= 1.0: continue
|
||||
|
||||
# Features
|
||||
h_elo = float(row['home_elo'] or 1500)
|
||||
a_elo = float(row['away_elo'] or 1500)
|
||||
h_home_goals = float(row['h_home_goals'] or 1.2)
|
||||
a_away_goals = float(row['a_away_goals'] or 1.2)
|
||||
h_rest = float(row['h_rest'] or 7)
|
||||
a_rest = float(row['a_rest'] or 7)
|
||||
h_xi = float(row['h_xi'] or 11)
|
||||
a_xi = float(row['a_xi'] or 11)
|
||||
cards = float(row['cards'] or 4)
|
||||
|
||||
def fatigue(rest):
|
||||
if rest < 3: return 0.85
|
||||
if rest < 5: return 0.95
|
||||
return 1.0
|
||||
|
||||
h_fat = fatigue(h_rest)
|
||||
a_fat = fatigue(a_rest)
|
||||
|
||||
h_xg = h_home_goals * h_fat
|
||||
a_xg = a_away_goals * a_fat
|
||||
total_xg = h_xg + a_xg
|
||||
|
||||
margin = (1/oh) + (1/od) + (1/oa)
|
||||
f = pd.DataFrame([{
|
||||
'elo_diff': h_elo - a_elo,
|
||||
'h_xg': h_xg, 'a_xg': a_xg,
|
||||
'total_xg': total_xg,
|
||||
'pow_diff': (h_elo/100)*h_fat - (a_elo/100)*a_fat,
|
||||
'rest_diff': h_rest - a_rest,
|
||||
'h_fatigue': h_fat, 'a_fatigue': a_fat,
|
||||
'imp_h': (1/oh)/margin, 'imp_d': (1/od)/margin, 'imp_a': (1/oa)/margin,
|
||||
'h_xi': h_xi, 'a_xi': a_xi,
|
||||
'cards': cards
|
||||
}])
|
||||
|
||||
# MS
|
||||
ms_probs = model_ms.predict(f)[0]
|
||||
for i, (pick, prob, odd) in enumerate(zip(['1', 'X', '2'], ms_probs, [oh, od, oa])):
|
||||
if odd <= 1.0: continue
|
||||
edge = prob - (1/odd)
|
||||
if edge > 0.05 and prob > 0.45:
|
||||
results['ms']['bet'] += 1
|
||||
h, a = row['score_home'], row['score_away']
|
||||
w = (pick=='1' and h>a) or (pick=='X' and h==a) or (pick=='2' and a>h)
|
||||
if w: results['ms']['won'] += 1; results['ms']['profit'] += (odd - 1.0)
|
||||
else: results['ms']['profit'] -= 1.0
|
||||
break
|
||||
|
||||
# OU2.5
|
||||
p_over = float(model_ou.predict(f)[0])
|
||||
if p_over > 0.55:
|
||||
results['ou25']['bet'] += 1
|
||||
if (row['score_home'] + row['score_away']) > 2.5: results['ou25']['won'] += 1; results['ou25']['profit'] += 0.85
|
||||
else: results['ou25']['profit'] -= 1.0
|
||||
|
||||
# BTTS
|
||||
p_btts = float(model_btts.predict(f)[0])
|
||||
if p_btts > 0.55:
|
||||
results['btts']['bet'] += 1
|
||||
if row['score_home'] > 0 and row['score_away'] > 0: results['btts']['won'] += 1; results['btts']['profit'] += 0.85
|
||||
else: results['btts']['profit'] -= 1.0
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("📊 VQWEN FINAL SONUÇLAR")
|
||||
print("="*60)
|
||||
for mkt in ['ms', 'ou25', 'btts']:
|
||||
r = results[mkt]
|
||||
wr = (r['won'] / r['bet'] * 100) if r['bet'] > 0 else 0
|
||||
print(f"{mkt.upper():<10} Oyn: {r['bet']:<5} Kaz: {r['won']:<5} WR: {wr:.1f}% Kâr: {r['profit']:+.2f}")
|
||||
|
||||
total = sum(r['profit'] for r in results.values())
|
||||
print(f"\n💰 TOPLAM: {total:+.2f} Units")
|
||||
print("🟢 PARA KAZANDIK!" if total > 0 else "🔴 ZARARDA")
|
||||
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_final_backtest()
|
||||
@@ -1,182 +0,0 @@
|
||||
"""
|
||||
VQWEN v3 Shared-Contract Backtest
|
||||
=================================
|
||||
|
||||
Evaluates the retrained VQWEN models on the temporal validation slice using
|
||||
the exact same pre-match feature contract as training/runtime.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import psycopg2
|
||||
from dotenv import load_dotenv
|
||||
|
||||
AI_DIR = Path(__file__).resolve().parent
|
||||
ENGINE_DIR = AI_DIR.parent
|
||||
REPO_DIR = ENGINE_DIR.parent
|
||||
MODELS_DIR = ENGINE_DIR / "models" / "vqwen"
|
||||
|
||||
if str(ENGINE_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(ENGINE_DIR))
|
||||
|
||||
from features.vqwen_contract import FEATURE_COLUMNS # noqa: E402
|
||||
from train_vqwen_v3 import ( # noqa: E402
|
||||
_enrich_pre_match_context,
|
||||
_fetch_dataframe,
|
||||
_prepare_features,
|
||||
_temporal_split,
|
||||
load_top_league_ids,
|
||||
)
|
||||
|
||||
|
||||
def _load_env() -> None:
|
||||
load_dotenv(REPO_DIR / ".env", override=False)
|
||||
load_dotenv(ENGINE_DIR / ".env", override=False)
|
||||
|
||||
|
||||
def get_clean_dsn() -> str:
|
||||
_load_env()
|
||||
raw = os.getenv("DATABASE_URL", "").strip().strip('"').strip("'")
|
||||
if not raw:
|
||||
raise RuntimeError("DATABASE_URL is missing.")
|
||||
return raw.split("?", 1)[0]
|
||||
|
||||
|
||||
def _accuracy(y_true: np.ndarray, y_pred: np.ndarray) -> float:
|
||||
if len(y_true) == 0:
|
||||
return 0.0
|
||||
return float((y_true == y_pred).mean())
|
||||
|
||||
|
||||
def _binary_metrics(prob: np.ndarray, y_true: np.ndarray) -> tuple[float, float]:
|
||||
pred = (prob >= 0.5).astype(int)
|
||||
acc = _accuracy(y_true, pred)
|
||||
brier = float(np.mean((prob - y_true) ** 2)) if len(y_true) else 1.0
|
||||
return acc, brier
|
||||
|
||||
|
||||
def _multiclass_brier(prob: np.ndarray, y_true: np.ndarray, n_classes: int = 3) -> float:
|
||||
if len(y_true) == 0:
|
||||
return 1.0
|
||||
target = np.zeros((len(y_true), n_classes), dtype=np.float64)
|
||||
target[np.arange(len(y_true)), y_true.astype(int)] = 1.0
|
||||
return float(np.mean(np.sum((prob - target) ** 2, axis=1)))
|
||||
|
||||
|
||||
def _band_label(probability: float) -> str:
|
||||
if probability >= 0.70:
|
||||
return "HIGH"
|
||||
if probability >= 0.60:
|
||||
return "MEDIUM"
|
||||
if probability >= 0.50:
|
||||
return "LOW"
|
||||
return "NO_BET"
|
||||
|
||||
|
||||
def _summarize_bands(
|
||||
name: str,
|
||||
confidence: np.ndarray,
|
||||
is_correct: np.ndarray,
|
||||
) -> list[str]:
|
||||
lines: list[str] = []
|
||||
for band in ("HIGH", "MEDIUM", "LOW"):
|
||||
mask = np.array([_band_label(float(p)) == band for p in confidence], dtype=bool)
|
||||
count = int(mask.sum())
|
||||
accuracy = float(is_correct[mask].mean()) if count else 0.0
|
||||
avg_conf = float(confidence[mask].mean()) if count else 0.0
|
||||
lines.append(
|
||||
f"{name} {band:<6} count={count:<4} accuracy={accuracy*100:5.1f}% avg_conf={avg_conf*100:5.1f}%"
|
||||
)
|
||||
return lines
|
||||
|
||||
|
||||
def run_v3_backtest() -> None:
|
||||
print("VQWEN v3 SHARED-CONTRACT BACKTEST")
|
||||
print("=" * 60)
|
||||
|
||||
league_ids = load_top_league_ids()
|
||||
dsn = get_clean_dsn()
|
||||
|
||||
with psycopg2.connect(dsn) as conn:
|
||||
with conn.cursor() as cur:
|
||||
df = _fetch_dataframe(cur, league_ids)
|
||||
df = _enrich_pre_match_context(cur, df)
|
||||
df = _prepare_features(df)
|
||||
|
||||
train_df, valid_df = _temporal_split(df)
|
||||
print(f"Toplam ornek: {len(df)} | Train: {len(train_df)} | Valid: {len(valid_df)}")
|
||||
|
||||
with (MODELS_DIR / "vqwen_ms.pkl").open("rb") as handle:
|
||||
model_ms = pickle.load(handle)
|
||||
with (MODELS_DIR / "vqwen_ou25.pkl").open("rb") as handle:
|
||||
model_ou25 = pickle.load(handle)
|
||||
with (MODELS_DIR / "vqwen_btts.pkl").open("rb") as handle:
|
||||
model_btts = pickle.load(handle)
|
||||
|
||||
X_valid = valid_df[FEATURE_COLUMNS]
|
||||
y_ms = valid_df["t_ms"].to_numpy(dtype=np.int64)
|
||||
y_ou25 = valid_df["t_ou"].to_numpy(dtype=np.int64)
|
||||
y_btts = valid_df["t_btts"].to_numpy(dtype=np.int64)
|
||||
|
||||
ms_prob = np.asarray(model_ms.predict(X_valid), dtype=np.float64)
|
||||
ou25_prob = np.asarray(model_ou25.predict(X_valid), dtype=np.float64).reshape(-1)
|
||||
btts_prob = np.asarray(model_btts.predict(X_valid), dtype=np.float64).reshape(-1)
|
||||
|
||||
ms_pred = np.argmax(ms_prob, axis=1)
|
||||
ms_conf = np.max(ms_prob, axis=1)
|
||||
ms_correct = (ms_pred == y_ms).astype(np.int64)
|
||||
|
||||
ou25_pred = (ou25_prob >= 0.5).astype(np.int64)
|
||||
ou25_conf = np.where(ou25_prob >= 0.5, ou25_prob, 1.0 - ou25_prob)
|
||||
ou25_correct = (ou25_pred == y_ou25).astype(np.int64)
|
||||
|
||||
btts_pred = (btts_prob >= 0.5).astype(np.int64)
|
||||
btts_conf = np.where(btts_prob >= 0.5, btts_prob, 1.0 - btts_prob)
|
||||
btts_correct = (btts_pred == y_btts).astype(np.int64)
|
||||
|
||||
ms_acc = _accuracy(y_ms, ms_pred)
|
||||
ou25_acc, ou25_brier = _binary_metrics(ou25_prob, y_ou25)
|
||||
btts_acc, btts_brier = _binary_metrics(btts_prob, y_btts)
|
||||
ms_brier = _multiclass_brier(ms_prob, y_ms)
|
||||
|
||||
print("\nGenel metrikler")
|
||||
print(f"MS accuracy : {ms_acc*100:.2f}% | multiclass_brier={ms_brier:.4f}")
|
||||
print(f"OU25 accuracy : {ou25_acc*100:.2f}% | brier={ou25_brier:.4f}")
|
||||
print(f"BTTS accuracy : {btts_acc*100:.2f}% | brier={btts_brier:.4f}")
|
||||
|
||||
print("\nConfidence band")
|
||||
for line in _summarize_bands("MS", ms_conf, ms_correct):
|
||||
print(line)
|
||||
for line in _summarize_bands("OU25", ou25_conf, ou25_correct):
|
||||
print(line)
|
||||
for line in _summarize_bands("BTTS", btts_conf, btts_correct):
|
||||
print(line)
|
||||
|
||||
summary = {
|
||||
"validation_samples": int(len(valid_df)),
|
||||
"metrics": {
|
||||
"ms_accuracy": round(ms_acc, 4),
|
||||
"ms_brier": round(ms_brier, 4),
|
||||
"ou25_accuracy": round(ou25_acc, 4),
|
||||
"ou25_brier": round(ou25_brier, 4),
|
||||
"btts_accuracy": round(btts_acc, 4),
|
||||
"btts_brier": round(btts_brier, 4),
|
||||
},
|
||||
}
|
||||
(MODELS_DIR / "vqwen_backtest_v3_summary.json").write_text(
|
||||
json.dumps(summary, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print("\nKaydedildi: vqwen_backtest_v3_summary.json")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_v3_backtest()
|
||||
Reference in New Issue
Block a user