gg
Deploy Iddaai Backend / build-and-deploy (push) Failing after 2m1s

This commit is contained in:
2026-05-11 23:11:41 +03:00
parent 4dcc4ced50
commit f8599bdb9a
29 changed files with 4908 additions and 3 deletions
@@ -689,4 +689,4 @@
}
}
}
}
}
+206
View File
@@ -0,0 +1,206 @@
"""
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()
+240
View File
@@ -0,0 +1,240 @@
"""
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()
+191
View File
@@ -0,0 +1,191 @@
"""
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()
+145
View File
@@ -0,0 +1,145 @@
"""
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()
+223
View File
@@ -0,0 +1,223 @@
"""
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()
+231
View File
@@ -0,0 +1,231 @@
"""
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()
+164
View File
@@ -0,0 +1,164 @@
"""
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()
+162
View File
@@ -0,0 +1,162 @@
"""
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()
+230
View File
@@ -0,0 +1,230 @@
"""
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())
+147
View File
@@ -0,0 +1,147 @@
"""
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()
+153
View File
@@ -0,0 +1,153 @@
"""
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()
+136
View File
@@ -0,0 +1,136 @@
"""
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()
+141
View File
@@ -0,0 +1,141 @@
"""
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()
+159
View File
@@ -0,0 +1,159 @@
"""
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()
+182
View File
@@ -0,0 +1,182 @@
"""
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()
+7
View File
@@ -0,0 +1,7 @@
import os, psycopg2
from dotenv import load_dotenv
load_dotenv('/Users/piton/Documents/Suggest-Bet-BE/.env')
conn = psycopg2.connect(os.getenv('DATABASE_URL').split('?')[0])
cur = conn.cursor()
cur.execute('SELECT mpe.match_id, SUM(CASE WHEN event_type::text LIKE \'%yellow_card%\' THEN 1 WHEN event_type::text LIKE \'%red_card%\' THEN 2 ELSE 1 END) as cards FROM match_player_events mpe WHERE event_type::text LIKE \'%card%\' GROUP BY mpe.match_id LIMIT 5')
print(cur.fetchall())
+56
View File
@@ -0,0 +1,56 @@
"""Quick test: V20+Quant integration — EV Edge, Kelly staking, edge-based grading."""
import json
from services.single_match_orchestrator import SingleMatchOrchestrator
MATCH_IDS = [
"er7n8hqndkhvdsg6an72r7h90", # Def. Justicia vs Atl Lanus
"etpay8k4qr3gts3jjidfebaxg", # CA Tigre vs Gymnasia
]
o = SingleMatchOrchestrator()
for mid in MATCH_IDS:
print(f"\n{'='*60}")
print(f"MATCH: {mid}")
print(f"{'='*60}")
r = o.analyze_match(mid)
if not r:
print(" Match not found")
continue
info = r.get("match_info", {})
print(f" {info.get('match_name', '?')} | {info.get('league', '?')}")
mp = r.get("main_pick", {})
print(f"\n MAIN PICK: {mp.get('market')} {mp.get('pick')}")
print(f" probability: {mp.get('probability', 0):.4f}")
print(f" odds: {mp.get('odds', 0):.2f}")
print(f" ev_edge: {mp.get('ev_edge', mp.get('edge', 0)):+.4f}")
print(f" implied_prob: {mp.get('implied_prob', 0):.4f}")
print(f" bet_grade: {mp.get('bet_grade', 'N/A')}")
print(f" stake_units: {mp.get('stake_units', 0)}")
print(f" playable: {mp.get('playable', False)}")
print(f" reasons: {mp.get('decision_reasons', [])}")
print(f"\n ALL MARKETS (with EV Edge + Kelly):")
for b in r.get("bet_summary", []):
ev = b.get("ev_edge", 0)
imp = b.get("implied_prob", 0)
flag = ">>>" if b.get("playable") else " "
mkt = b["market"]
pick = b["pick"]
odds = b.get("odds", 0)
grade = b["bet_grade"]
stake = b["stake_units"]
conf = b.get("calibrated_confidence", 0)
print(
f" {flag} {mkt:8s} {pick:12s} "
f"ev_edge={ev:+.3f} "
f"odds={odds:.2f} "
f"stake={stake:.1f} "
f"grade={grade:4s} "
f"conf={conf:.1f}% "
f"implied={imp:.3f}"
)
print()
@@ -0,0 +1,75 @@
import sys
import unittest
from decimal import Decimal
from pathlib import Path
from unittest.mock import MagicMock
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
if str(AI_ENGINE_ROOT) not in sys.path:
sys.path.insert(0, str(AI_ENGINE_ROOT))
from core.engines.odds_predictor import OddsPredictorEngine
from features.sidelined_analyzer import SidelinedAnalyzer
class EngineNullSafetyTests(unittest.TestCase):
def test_odds_predictor_accepts_decimal_inputs_without_crashing(self):
engine = OddsPredictorEngine()
prediction = engine.predict(
odds_data={
"ms_h": Decimal("2.10"),
"ms_d": Decimal("3.25"),
"ms_a": Decimal("3.60"),
"ou25_o": Decimal("1.90"),
},
)
self.assertGreater(prediction.market_home_prob, 0.0)
self.assertGreater(prediction.market_draw_prob, 0.0)
self.assertGreater(prediction.market_away_prob, 0.0)
def test_sidelined_analyzer_handles_non_numeric_fields(self):
analyzer = SidelinedAnalyzer.__new__(SidelinedAnalyzer)
analyzer.position_weights = {"K": 0.35, "D": 0.20, "O": 0.25, "F": 0.30}
analyzer.max_rating = 10
analyzer.adaptation_threshold = 10
analyzer.adaptation_discount = 0.5
analyzer.goalkeeper_penalty = 0.15
analyzer.confidence_boost = 10
analyzer.max_impact = 0.85
analyzer.key_player_threshold = 3
analyzer.recent_matches_lookback = 15
analyzer._fetch_player_stats = MagicMock(return_value={})
result = analyzer.analyze(
{
"totalSidelined": 2,
"players": [
{
"playerId": "p1",
"playerName": "Player One",
"positionShort": "O",
"matchesMissed": "N/A",
"average": "?",
"type": "injury",
},
{
"playerId": "p2",
"playerName": "Player Two",
"positionShort": "K",
"matchesMissed": "12",
"average": "6.7",
"type": "suspension",
},
],
},
)
self.assertEqual(result.total_sidelined, 2)
self.assertGreaterEqual(result.impact_score, 0.0)
self.assertTrue(len(result.player_details) >= 2)
if __name__ == "__main__":
unittest.main()
+282
View File
@@ -0,0 +1,282 @@
"""
Unit tests for FeatureEnrichmentService
========================================
Tests all 6 enrichment methods with mocked DB cursor:
1. compute_team_stats
2. compute_h2h
3. compute_form_streaks
4. compute_referee_stats
5. compute_league_averages
6. compute_momentum
"""
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
if str(AI_ENGINE_ROOT) not in sys.path:
sys.path.insert(0, str(AI_ENGINE_ROOT))
from services.feature_enrichment import FeatureEnrichmentService, _safe_avg
def _make_cursor(rows=None, side_effect=None):
"""Create a mock RealDictCursor."""
cur = MagicMock()
if side_effect:
cur.execute.side_effect = side_effect
else:
cur.fetchall.return_value = rows or []
cur.fetchone.return_value = rows[0] if rows else None
return cur
class TestSafeAvg(unittest.TestCase):
def test_returns_average(self):
self.assertAlmostEqual(_safe_avg([2.0, 4.0, 6.0], 0.0), 4.0)
def test_returns_default_on_empty(self):
self.assertEqual(_safe_avg([], 99.0), 99.0)
def test_single_value(self):
self.assertAlmostEqual(_safe_avg([7.5], 0.0), 7.5)
class TestComputeTeamStats(unittest.TestCase):
def setUp(self):
self.svc = FeatureEnrichmentService()
self.ts = 1700000000000
def test_returns_defaults_when_no_team_id(self):
result = self.svc.compute_team_stats(MagicMock(), '', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
def test_returns_defaults_when_no_rows(self):
cur = _make_cursor(rows=[])
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
def test_returns_defaults_on_db_error(self):
cur = _make_cursor(side_effect=Exception('DB down'))
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_TEAM_STATS)
def test_calculates_averages_correctly(self):
rows = [
{'possession_percentage': 60.0, 'shots_on_target': 5, 'total_shots': 10, 'corners': 7},
{'possession_percentage': 40.0, 'shots_on_target': 3, 'total_shots': 12, 'corners': 3},
]
cur = _make_cursor(rows)
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
self.assertAlmostEqual(result['avg_possession'], 50.0)
self.assertAlmostEqual(result['avg_shots_on_target'], 4.0)
self.assertAlmostEqual(result['shot_conversion'], (5 / 10 + 3 / 12) / 2, places=4)
self.assertAlmostEqual(result['avg_corners'], 5.0)
def test_handles_none_subfields_gracefully(self):
"""Rows with None values should be skipped, not crash."""
rows = [
{'possession_percentage': 55.0, 'shots_on_target': None, 'total_shots': None, 'corners': 4},
{'possession_percentage': None, 'shots_on_target': 2, 'total_shots': 8, 'corners': None},
]
cur = _make_cursor(rows)
result = self.svc.compute_team_stats(cur, 'team1', self.ts)
self.assertAlmostEqual(result['avg_possession'], 55.0)
self.assertAlmostEqual(result['avg_shots_on_target'], 2.0)
self.assertAlmostEqual(result['avg_corners'], 4.0)
class TestComputeH2H(unittest.TestCase):
def setUp(self):
self.svc = FeatureEnrichmentService()
self.ts = 1700000000000
def test_returns_defaults_when_no_ids(self):
result = self.svc.compute_h2h(MagicMock(), '', 'away1', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
def test_returns_defaults_when_no_rows(self):
cur = _make_cursor(rows=[])
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
def test_calculates_h2h_stats(self):
rows = [
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 2, 'score_away': 1}, # home win, btts, over25
{'home_team_id': 'home1', 'away_team_id': 'away1', 'score_home': 0, 'score_away': 0}, # draw, no btts, no over25
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 1, 'score_away': 3}, # reversed: home wins again, btts, over25
{'home_team_id': 'away1', 'away_team_id': 'home1', 'score_home': 2, 'score_away': 0}, # reversed: away(=home1) lost
]
cur = _make_cursor(rows)
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
self.assertEqual(result['total_matches'], 4)
self.assertAlmostEqual(result['home_win_rate'], 2 / 4)
self.assertAlmostEqual(result['draw_rate'], 1 / 4)
self.assertAlmostEqual(result['btts_rate'], 2 / 4)
self.assertAlmostEqual(result['over25_rate'], 2 / 4)
def test_returns_defaults_on_db_error(self):
cur = _make_cursor(side_effect=Exception('connection lost'))
result = self.svc.compute_h2h(cur, 'home1', 'away1', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_H2H)
class TestComputeFormStreaks(unittest.TestCase):
def setUp(self):
self.svc = FeatureEnrichmentService()
self.ts = 1700000000000
def test_returns_defaults_when_no_team_id(self):
result = self.svc.compute_form_streaks(MagicMock(), '', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_FORM)
def test_calculates_streaks_correctly(self):
"""Most recent first: W, W, D, L → winning_streak=2, unbeaten_streak=3."""
rows = [
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 2, 'score_away': 0}, # W (clean sheet, scored)
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 1, 'score_away': 0}, # W (clean sheet, scored)
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 1, 'score_away': 1}, # D (scored, conceded)
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 2}, # L (not scored, conceded)
]
cur = _make_cursor(rows)
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
self.assertEqual(result['winning_streak'], 2)
self.assertEqual(result['unbeaten_streak'], 3)
self.assertAlmostEqual(result['clean_sheet_rate'], 2 / 4)
self.assertAlmostEqual(result['scoring_rate'], 3 / 4)
def test_all_losses(self):
rows = [
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 1},
{'home_team_id': 'team1', 'away_team_id': 'x', 'score_home': 0, 'score_away': 3},
]
cur = _make_cursor(rows)
result = self.svc.compute_form_streaks(cur, 'team1', self.ts)
self.assertEqual(result['winning_streak'], 0)
self.assertEqual(result['unbeaten_streak'], 0)
self.assertAlmostEqual(result['scoring_rate'], 0.0)
class TestComputeRefereeStats(unittest.TestCase):
def setUp(self):
self.svc = FeatureEnrichmentService()
self.ts = 1700000000000
def test_returns_defaults_when_no_name(self):
result = self.svc.compute_referee_stats(MagicMock(), None, self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
def test_calculates_referee_tendencies(self):
match_rows = [
{'home_team_id': 'h1', 'score_home': 2, 'score_away': 0, 'match_id': 'm1'}, # home win
{'home_team_id': 'h2', 'score_home': 1, 'score_away': 1, 'match_id': 'm2'}, # draw
]
card_row = {'yellows': 6, 'total_cards': 8}
cur = MagicMock()
# First execute (match query) → match_rows
# Second execute (card query) → card_row
cur.fetchall.return_value = match_rows
cur.fetchone.return_value = card_row
result = self.svc.compute_referee_stats(cur, 'Ref Name', self.ts)
self.assertEqual(result['experience'], 2)
self.assertAlmostEqual(result['avg_goals'], (2 + 0 + 1 + 1) / 2)
# home_bias = (1/2) - 0.46 = 0.04
self.assertAlmostEqual(result['home_bias'], 0.04, places=4)
self.assertAlmostEqual(result['avg_yellow'], 6 / 2)
self.assertAlmostEqual(result['cards_total'], 8 / 2)
def test_returns_defaults_on_db_error(self):
cur = _make_cursor(side_effect=Exception('timeout'))
result = self.svc.compute_referee_stats(cur, 'Some Ref', self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_REFEREE)
class TestComputeLeagueAverages(unittest.TestCase):
def setUp(self):
self.svc = FeatureEnrichmentService()
self.ts = 1700000000000
def test_returns_defaults_when_no_league_id(self):
result = self.svc.compute_league_averages(MagicMock(), None, self.ts)
self.assertEqual(result, FeatureEnrichmentService._DEFAULT_LEAGUE)
def test_calculates_league_averages(self):
rows = [
{'score_home': 1, 'score_away': 1}, # 2 goals
{'score_home': 0, 'score_away': 0}, # 0 goals (zero-goal match)
{'score_home': 3, 'score_away': 2}, # 5 goals
]
cur = _make_cursor(rows)
result = self.svc.compute_league_averages(cur, 'league1', self.ts)
self.assertAlmostEqual(result['avg_goals'], 7 / 3, places=4)
self.assertAlmostEqual(result['zero_goal_rate'], 1 / 3, places=4)
class TestComputeMomentum(unittest.TestCase):
def setUp(self):
self.svc = FeatureEnrichmentService()
self.ts = 1700000000000
def test_returns_zero_when_no_team_id(self):
result = self.svc.compute_momentum(MagicMock(), '', self.ts)
self.assertEqual(result, 0.0)
def test_returns_zero_when_no_rows(self):
cur = _make_cursor(rows=[])
result = self.svc.compute_momentum(cur, 'team1', self.ts)
self.assertEqual(result, 0.0)
def test_all_wins_returns_one(self):
"""All wins → momentum = 1.0 (max possible)."""
rows = [
{'home_team_id': 'team1', 'score_home': 3, 'score_away': 0},
{'home_team_id': 'team1', 'score_home': 2, 'score_away': 1},
]
cur = _make_cursor(rows)
result = self.svc.compute_momentum(cur, 'team1', self.ts)
self.assertAlmostEqual(result, 1.0, places=4)
def test_all_losses_returns_negative(self):
"""All losses → negative momentum."""
rows = [
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 2},
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 3},
]
cur = _make_cursor(rows)
result = self.svc.compute_momentum(cur, 'team1', self.ts)
self.assertLess(result, 0.0)
def test_mixed_results(self):
"""W, D, L → weighted score between -1 and 1."""
rows = [
{'home_team_id': 'team1', 'score_home': 1, 'score_away': 0}, # W (weight=3)
{'home_team_id': 'x', 'away_team_id': 'team1', 'score_home': 0, 'score_away': 0}, # D (weight=2)
{'home_team_id': 'team1', 'score_home': 0, 'score_away': 1}, # L (weight=1)
]
cur = _make_cursor(rows)
result = self.svc.compute_momentum(cur, 'team1', self.ts)
# weighted = 3*3 + 1*2 + (-1)*1 = 9+2-1 = 10
# max_possible = 3*3 + 3*2 + 3*1 = 18
# normalised = 10/18 ≈ 0.5556
self.assertAlmostEqual(result, round(10 / 18, 4), places=4)
def test_returns_zero_on_db_error(self):
cur = _make_cursor(side_effect=Exception('broken pipe'))
result = self.svc.compute_momentum(cur, 'team1', self.ts)
self.assertEqual(result, 0.0)
if __name__ == '__main__':
unittest.main()
+110
View File
@@ -0,0 +1,110 @@
import asyncio
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
from fastapi import HTTPException
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
if str(AI_ENGINE_ROOT) not in sys.path:
sys.path.insert(0, str(AI_ENGINE_ROOT))
import main as ai_main
def _run(coro):
return asyncio.run(coro)
class MainApiFunctionTests(unittest.TestCase):
def test_analyze_match_v20plus_returns_payload(self):
orchestrator = MagicMock()
orchestrator.analyze_match.return_value = {"match_info": {"match_id": "m1"}}
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
result = _run(ai_main.analyze_match_v20plus("m1"))
self.assertEqual(result["match_info"]["match_id"], "m1")
def test_analyze_match_v20plus_raises_404(self):
orchestrator = MagicMock()
orchestrator.analyze_match.return_value = None
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
with self.assertRaises(HTTPException) as ctx:
_run(ai_main.analyze_match_v20plus("missing"))
self.assertEqual(ctx.exception.status_code, 404)
def test_analyze_match_htms_v20plus_returns_payload(self):
orchestrator = MagicMock()
orchestrator.analyze_match_htms.return_value = {
"status": "ok",
"engine_used": "v20plus_top_htms",
}
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
result = _run(ai_main.analyze_match_htms_v20plus("m1"))
self.assertEqual(result["status"], "ok")
self.assertEqual(result["engine_used"], "v20plus_top_htms")
def test_analyze_match_htft_timeout_validation(self):
with self.assertRaises(HTTPException) as ctx:
_run(ai_main.analyze_match_htft_v20plus("m1", timeout_sec=2))
self.assertEqual(ctx.exception.status_code, 400)
def test_generate_coupon_v20plus_forwards_payload(self):
orchestrator = MagicMock()
orchestrator.build_coupon.return_value = {"bets": []}
request = ai_main.CouponRequest(
match_ids=["m1", "m2"],
strategy="SAFE",
max_matches=3,
min_confidence=70,
)
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
result = _run(ai_main.generate_coupon_v20plus(request))
self.assertEqual(result, {"bets": []})
orchestrator.build_coupon.assert_called_once_with(
match_ids=["m1", "m2"],
strategy="SAFE",
max_matches=3,
min_confidence=70.0,
)
def test_reversal_watchlist_validation(self):
with self.assertRaises(HTTPException) as ctx:
_run(ai_main.get_reversal_watchlist_v20plus(count=0))
self.assertEqual(ctx.exception.status_code, 400)
def test_reversal_watchlist_forwards_payload(self):
orchestrator = MagicMock()
orchestrator.get_reversal_watchlist.return_value = {"watchlist": []}
with patch("main.get_single_match_orchestrator", return_value=orchestrator):
result = _run(
ai_main.get_reversal_watchlist_v20plus(
count=12,
horizon_hours=48,
min_score=50.5,
top_leagues_only=True,
),
)
self.assertEqual(result, {"watchlist": []})
orchestrator.get_reversal_watchlist.assert_called_once_with(
count=12,
horizon_hours=48,
min_score=50.5,
top_leagues_only=True,
)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,766 @@
import json
import sys
import unittest
from pathlib import Path
from unittest.mock import MagicMock, patch
AI_ENGINE_ROOT = Path(__file__).resolve().parents[1]
if str(AI_ENGINE_ROOT) not in sys.path:
sys.path.insert(0, str(AI_ENGINE_ROOT))
from models.v20_ensemble import FullMatchPrediction
from models.basketball_v25 import BasketballMatchPrediction
from services.single_match_orchestrator import MatchData, SingleMatchOrchestrator
class _CursorContext:
def __init__(self, cursor):
self._cursor = cursor
def __enter__(self):
return self._cursor
def __exit__(self, exc_type, exc, tb):
return False
class _ConnContext:
def __init__(self, cursor):
self._cursor = cursor
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def cursor(self, cursor_factory=None):
return _CursorContext(self._cursor)
class _StaticFetchAllCursor:
def __init__(self, rows):
self.rows = rows
self.executed = []
def execute(self, query, params=None):
self.executed.append((query, params))
def fetchall(self):
return list(self.rows)
class _RouterCursor:
def __init__(
self,
*,
live_row=None,
hist_row=None,
relational_rows=None,
participation_rows=None,
probable_rows=None,
):
self.live_row = live_row
self.hist_row = hist_row
self.relational_rows = relational_rows or []
self.participation_rows = participation_rows or []
self.probable_rows = probable_rows or []
self.last_query = ""
def execute(self, query, params=None):
self.last_query = query
def fetchone(self):
if "FROM live_matches" in self.last_query:
return self.live_row
if "FROM matches m" in self.last_query:
return self.hist_row
return None
def fetchall(self):
if "FROM odd_categories" in self.last_query:
return list(self.relational_rows)
if "FROM match_player_participation" in self.last_query and "GROUP BY" not in self.last_query:
return list(self.participation_rows)
if "GROUP BY mpp.player_id" in self.last_query:
return list(self.probable_rows)
return []
def _build_orchestrator() -> SingleMatchOrchestrator:
orchestrator = SingleMatchOrchestrator.__new__(SingleMatchOrchestrator)
orchestrator.v25_predictor = MagicMock()
orchestrator.basketball_predictor = MagicMock()
orchestrator.dsn = "postgresql://unit-test"
orchestrator.league_reliability = {}
orchestrator.market_calibration = {
"MS": 0.82,
"DC": 0.93,
"OU15": 0.90,
"OU25": 0.85,
"OU35": 0.88,
"BTTS": 0.83,
"HT": 0.80,
"HT_OU05": 0.88,
}
orchestrator.market_min_conf = {
"MS": 52.0,
"DC": 56.0,
"OU15": 60.0,
"OU25": 58.0,
"OU35": 54.0,
"BTTS": 57.0,
"HT": 53.0,
"HT_OU05": 55.0,
}
orchestrator.market_min_play_score = {
"MS": 72.0,
"DC": 62.0,
"OU15": 64.0,
"OU25": 70.0,
"OU35": 76.0,
"BTTS": 70.0,
"HT": 74.0,
"HT_OU05": 64.0,
}
orchestrator.market_min_edge = {
"MS": 0.03,
"DC": 0.01,
"OU15": 0.01,
"OU25": 0.02,
"OU35": 0.04,
"BTTS": 0.03,
"HT": 0.04,
"HT_OU05": 0.01,
}
return orchestrator
class SingleMatchOrchestratorTests(unittest.TestCase):
def setUp(self):
self.orchestrator = _build_orchestrator()
def test_parse_odds_json_uses_exact_market_match_and_ignores_collisions(self):
odds_json = {
"Maç Sonucu": {"1": "2.15", "X": "3.20", "2": "3.30"},
"İlk Yarı/Maç Sonucu": {"1/1": "4.30"},
"2,5 Alt/Üst": {"Üst": "1.85", "Alt": "1.95"},
"İY 0,5 Alt/Üst": {"Üst": "1.49", "Alt": "2.20"},
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.33", "Alt": "2.95"},
"Karşılıklı Gol": {"Var": "1.75", "Yok": "2.05"},
"1. Yarı Karşılıklı Gol": {"Var": "2.10", "Yok": "1.60"},
"Çifte Şans": {"1-X": "1.33", "X-2": "1.62", "1-2": "1.30"},
"1. Yarı Sonucu": {"1": "2.45", "X": "2.00", "2": "3.80"},
}
parsed = self.orchestrator._parse_odds_json(odds_json)
self.assertEqual(parsed["ms_h"], 2.15)
self.assertEqual(parsed["ms_d"], 3.20)
self.assertEqual(parsed["ms_a"], 3.30)
self.assertEqual(parsed["ou25_o"], 1.85)
self.assertEqual(parsed["ou25_u"], 1.95)
self.assertEqual(parsed["btts_y"], 1.75)
self.assertEqual(parsed["btts_n"], 2.05)
self.assertEqual(parsed["dc_1x"], 1.33)
self.assertEqual(parsed["dc_x2"], 1.62)
self.assertEqual(parsed["dc_12"], 1.30)
self.assertEqual(parsed["ht_h"], 2.45)
self.assertEqual(parsed["ht_d"], 2.00)
self.assertEqual(parsed["ht_a"], 3.80)
self.assertEqual(parsed["ht_ou05_o"], 1.49)
self.assertEqual(parsed["ht_ou05_u"], 2.20)
self.assertEqual(parsed["htft_11"], 4.30)
def test_parse_odds_json_accepts_selection_variants(self):
odds_json = {
"2,5 Alt/Üst": {"2,5 Üst": "1.91", "2,5 Alt": "1.86"},
"Karşılıklı Gol": {"YES": "1.82", "NO": "1.96"},
"Çifte Şans": {"1X": "1.28", "X2": "1.44", "12": "1.32"},
}
parsed = self.orchestrator._parse_odds_json(odds_json)
self.assertEqual(parsed["ou25_o"], 1.91)
self.assertEqual(parsed["ou25_u"], 1.86)
self.assertEqual(parsed["btts_y"], 1.82)
self.assertEqual(parsed["btts_n"], 1.96)
self.assertEqual(parsed["dc_1x"], 1.28)
self.assertEqual(parsed["dc_x2"], 1.44)
self.assertEqual(parsed["dc_12"], 1.32)
def test_parse_odds_json_maps_all_football_markets_with_noise(self):
odds_json = {
"Maç Sonucu": {"1": "2.31", "X": "3.22", "2": "3.05"},
"Çifte Şans": {"1-X": "1.34", "X-2": "1.52", "1-2": "1.28"},
"1,5 Alt/Üst": {"Üst": "1.29", "Alt": "3.45"},
"2,5 Alt/Üst": {"Üst": "1.71", "Alt": "2.05"},
"3,5 Alt/Üst": {"Üst": "2.62", "Alt": "1.41"},
"Karşılıklı Gol": {"Var": "1.66", "Yok": "2.11"},
"1. Yarı Sonucu": {"1": "3.10", "X": "1.95", "2": "4.60"},
"1. Yarı 0,5 Alt/Üst": {"Üst": "1.21", "Alt": "2.72"},
# noise categories that must not overwrite football main markets
"1. Yarı Ev Sahibi 0,5 Alt/Üst": {"Üst": "1.99", "Alt": "1.45"},
"1. Yarı Deplasman 0,5 Alt/Üst": {"Üst": "1.73", "Alt": "1.63"},
"1.Yarı 3,5 Korner Alt/Üst": {"Üst": "1.26", "Alt": "2.30"},
"2,5 Kart Puanı Alt/Üst": {"Üst": "1.40", "Alt": "2.60"},
}
parsed = self.orchestrator._parse_odds_json(odds_json)
self.assertEqual(parsed["ms_h"], 2.31)
self.assertEqual(parsed["ms_d"], 3.22)
self.assertEqual(parsed["ms_a"], 3.05)
self.assertEqual(parsed["dc_1x"], 1.34)
self.assertEqual(parsed["dc_x2"], 1.52)
self.assertEqual(parsed["dc_12"], 1.28)
self.assertEqual(parsed["ou15_o"], 1.29)
self.assertEqual(parsed["ou15_u"], 3.45)
self.assertEqual(parsed["ou25_o"], 1.71)
self.assertEqual(parsed["ou25_u"], 2.05)
self.assertEqual(parsed["ou35_o"], 2.62)
self.assertEqual(parsed["ou35_u"], 1.41)
self.assertEqual(parsed["btts_y"], 1.66)
self.assertEqual(parsed["btts_n"], 2.11)
self.assertEqual(parsed["ht_h"], 3.10)
self.assertEqual(parsed["ht_d"], 1.95)
self.assertEqual(parsed["ht_a"], 4.60)
self.assertEqual(parsed["ht_ou05_o"], 1.21)
self.assertEqual(parsed["ht_ou05_u"], 2.72)
def test_v25_market_odds_ignores_synthetic_default_when_selection_missing(self):
odds_json = {
"1,5 Alt/Üst": {"Alt": 5.70},
"Çifte Şans": {"1-X": 1.30, "X-2": 1.38, "1-2": 1.09},
}
parsed = self.orchestrator._parse_odds_json(odds_json)
self.assertEqual(parsed["ou15_o"], 0.0)
self.assertEqual(
self.orchestrator._v25_market_odds(parsed, "OU15", "Over"),
1.0,
)
self.assertEqual(
self.orchestrator._v25_market_odds(parsed, "OU15", "Under"),
5.7,
)
self.assertEqual(
self.orchestrator._v25_market_odds(parsed, "DC", "X2"),
1.38,
)
def test_parse_odds_json_extracts_basketball_ml_total_spread(self):
odds_json = {
"Maç Sonucu (Uzt. Dahil)": {"1": "1.74", "2": "2.08"},
"Alt/Üst (163,5)": {"Üst": "1.86", "Alt": "1.94"},
"1. Yarı Alt/Üst (81,5)": {"Üst": "1.89", "Alt": "1.91"},
"1. Yarı Alt/Üst (100,5)": {"Üst": "1.83", "Alt": "1.97"},
"Hnd. MS (0:5,5)": {"1": "1.91", "+5.5h": "1.87"},
}
parsed = self.orchestrator._parse_odds_json(odds_json)
self.assertEqual(parsed["ml_h"], 1.74)
self.assertEqual(parsed["ml_a"], 2.08)
self.assertEqual(parsed["tot_line"], 163.5)
self.assertEqual(parsed["tot_o"], 1.86)
self.assertEqual(parsed["tot_u"], 1.94)
self.assertEqual(parsed["spread_home_line"], -5.5)
self.assertEqual(parsed["spread_h"], 1.91)
self.assertEqual(parsed["spread_a"], 1.87)
self.assertNotIn("ht_ou05_o", parsed)
self.assertNotIn("ht_ou05_u", parsed)
def test_extract_odds_merges_relational_when_live_json_is_incomplete(self):
row = {
"match_id": "m-1",
"odds": {"Maç Sonucu": {"1": 2.10, "X": 3.20, "2": 3.35}},
}
relational_rows = [
{"category_name": "Çifte Şans", "selection_name": "1-X", "odd_value": 1.28},
{"category_name": "Çifte Şans", "selection_name": "X-2", "odd_value": 1.44},
{"category_name": "Çifte Şans", "selection_name": "1-2", "odd_value": 1.31},
{"category_name": "2,5 Alt/Üst", "selection_name": "Üst", "odd_value": 1.89},
{"category_name": "2,5 Alt/Üst", "selection_name": "Alt", "odd_value": 1.94},
{"category_name": "Karşılıklı Gol", "selection_name": "Var", "odd_value": 1.77},
{"category_name": "Karşılıklı Gol", "selection_name": "Yok", "odd_value": 2.02},
{"category_name": "1. Yarı Sonucu", "selection_name": "1", "odd_value": 2.55},
{"category_name": "1. Yarı Sonucu", "selection_name": "X", "odd_value": 1.98},
{"category_name": "1. Yarı Sonucu", "selection_name": "2", "odd_value": 3.40},
]
cur = _StaticFetchAllCursor(relational_rows)
odds = self.orchestrator._extract_odds(cur, row)
self.assertEqual(odds["ms_h"], 2.10)
self.assertEqual(odds["ms_d"], 3.20)
self.assertEqual(odds["ms_a"], 3.35)
self.assertEqual(odds["dc_x2"], 1.44)
self.assertEqual(odds["ou25_o"], 1.89)
self.assertEqual(odds["btts_y"], 1.77)
self.assertEqual(odds["ht_d"], 1.98)
self.assertEqual(len(cur.executed), 1)
def test_extract_odds_fills_default_ms_when_no_source_available(self):
row = {"match_id": "m-2", "odds": None}
cur = _StaticFetchAllCursor([])
odds = self.orchestrator._extract_odds(cur, row)
self.assertEqual(odds["ms_h"], SingleMatchOrchestrator.DEFAULT_MS_H)
self.assertEqual(odds["ms_d"], SingleMatchOrchestrator.DEFAULT_MS_D)
self.assertEqual(odds["ms_a"], SingleMatchOrchestrator.DEFAULT_MS_A)
def test_parse_lineups_json_supports_id_playerid_personid(self):
lineups = {
"home": {
"xi": [
{"id": "11"},
{"playerId": "12"},
],
},
"away": {
"starting": [
{"personId": "21"},
"22",
],
},
}
home, away = self.orchestrator._parse_lineups_json(lineups)
self.assertEqual(home, ["11", "12"])
self.assertEqual(away, ["21", "22"])
def test_extract_lineups_uses_participation_and_probable_xi_fallbacks(self):
row = {
"match_id": "m-3",
"home_team_id": "h1",
"away_team_id": "a1",
"match_date_ms": 1700000000000,
"lineups": {
"home": {"xi": [{"personId": "h-live-1"}]},
"away": {},
},
}
participation = [
{"team_id": "a1", "player_id": "a-db-1"},
{"team_id": "a1", "player_id": "a-db-2"},
]
cur = _StaticFetchAllCursor(participation)
with patch.object(
self.orchestrator,
"_build_probable_xi",
side_effect=[["h-prob-1"], ["a-prob-1"]],
) as probable_xi:
home, away, source = self.orchestrator._extract_lineups(cur, row)
self.assertEqual(home, ["h-live-1"])
self.assertEqual(away, ["a-db-1", "a-db-2"])
self.assertEqual(source, "none")
probable_xi.assert_not_called()
def test_extract_lineups_falls_back_to_probable_xi_when_live_and_participation_missing(self):
row = {
"match_id": "m-4",
"home_team_id": "h2",
"away_team_id": "a2",
"match_date_ms": 1700000000000,
"lineups": None,
}
cur = _StaticFetchAllCursor([])
with patch.object(
self.orchestrator,
"_build_probable_xi",
side_effect=[["h-prob-1", "h-prob-2"], ["a-prob-1"]],
) as probable_xi:
home, away, source = self.orchestrator._extract_lineups(cur, row)
self.assertEqual(home, ["h-prob-1", "h-prob-2"])
self.assertEqual(away, ["a-prob-1"])
self.assertEqual(source, "probable_xi")
self.assertEqual(probable_xi.call_count, 2)
def test_load_match_data_parses_live_row_json_and_sidelined(self):
odds_payload = {
"Maç Sonucu": {"1": 2.10, "X": 3.30, "2": 3.50},
"Çifte Şans": {"1-X": 1.30, "X-2": 1.52, "1-2": 1.34},
"1,5 Alt/Üst": {"Üst": 1.33, "Alt": 2.90},
"2,5 Alt/Üst": {"Üst": 1.91, "Alt": 1.85},
"3,5 Alt/Üst": {"Üst": 2.95, "Alt": 1.38},
"Karşılıklı Gol": {"Var": 1.84, "Yok": 1.92},
"1. Yarı Sonucu": {"1": 2.55, "X": 1.97, "2": 3.45},
}
lineups_payload = {
"home": {"xi": [{"personId": "101"}, {"personId": "102"}]},
"away": {"xi": [{"personId": "201"}, {"personId": "202"}]},
}
live_row = {
"match_id": "live-101",
"home_team_id": "h-101",
"away_team_id": "a-101",
"league_id": "l-101",
"sport": "FOOTBALL",
"match_date_ms": 1760000000000,
"odds": json.dumps(odds_payload),
"lineups": json.dumps(lineups_payload),
"sidelined": json.dumps(
{
"homeTeam": {"totalSidelined": 1, "players": []},
"awayTeam": {"totalSidelined": 0, "players": []},
}
),
"referee_name": "John Ref",
"home_team_name": "Home FC",
"away_team_name": "Away FC",
"league_name": "League Name",
}
cursor = _RouterCursor(live_row=live_row)
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
data = self.orchestrator._load_match_data("live-101")
self.assertIsNotNone(data)
self.assertEqual(data.match_id, "live-101")
self.assertEqual(data.home_team_id, "h-101")
self.assertEqual(data.away_team_id, "a-101")
self.assertEqual(data.sport, "football")
self.assertEqual(data.referee_name, "John Ref")
self.assertEqual(data.home_lineup, ["101", "102"])
self.assertEqual(data.away_lineup, ["201", "202"])
self.assertEqual(data.lineup_source, "none")
self.assertEqual(data.sidelined_data["homeTeam"]["totalSidelined"], 1)
self.assertEqual(data.odds_data["dc_x2"], 1.52)
self.assertEqual(data.odds_data["ht_h"], 2.55)
def test_analyze_match_forwards_all_core_fields_to_predictor(self):
match_data = MatchData(
match_id="live-55",
home_team_id="home-55",
away_team_id="away-55",
home_team_name="Home 55",
away_team_name="Away 55",
match_date_ms=1760000000000,
sport="football",
league_id="league-55",
league_name="League 55",
referee_name="Ref 55",
odds_data={"ms_h": 2.4, "ms_d": 3.1, "ms_a": 2.9},
home_lineup=["h1", "h2"],
away_lineup=["a1", "a2"],
sidelined_data={
"homeTeam": {"totalSidelined": 2, "players": []},
"awayTeam": {"totalSidelined": 1, "players": []},
},
home_goals_avg=1.6,
home_conceded_avg=1.1,
away_goals_avg=1.2,
away_conceded_avg=1.4,
home_position=5,
away_position=8,
lineup_source="confirmed_live",
)
prediction = FullMatchPrediction(match_id="live-55", home_team="Home 55", away_team="Away 55")
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
self.orchestrator.v25_predictor.predict_market_bundle = MagicMock(return_value={"MS": {"pick": "1"}})
self.orchestrator._build_v25_features = MagicMock(return_value={})
self.orchestrator._get_v25_signal = MagicMock(return_value={"MS": {"pick": "1"}})
self.orchestrator._build_v25_prediction = MagicMock(return_value=prediction)
self.orchestrator._build_prediction_package = MagicMock(return_value={"ok": True})
result = self.orchestrator.analyze_match("live-55")
self.assertEqual(result, {"ok": True})
self.orchestrator._build_v25_features.assert_called_once_with(match_data)
self.orchestrator._get_v25_signal.assert_called_once_with(match_data, {})
self.orchestrator._build_v25_prediction.assert_called_once_with(
match_data,
{},
{"MS": {"pick": "1"}},
)
def test_analyze_match_routes_basketball_to_basketball_predictor(self):
match_data = MatchData(
match_id="b-live-1",
home_team_id="bh",
away_team_id="ba",
home_team_name="Home B",
away_team_name="Away B",
match_date_ms=1760000000000,
sport="basketball",
league_id="bleague",
league_name="B League",
referee_name=None,
odds_data={"ml_h": 1.75, "ml_a": 2.05, "tot_line": 161.5, "tot_o": 1.88, "tot_u": 1.92},
home_lineup=None,
away_lineup=None,
sidelined_data={"homeTeam": {"totalSidelined": 1}, "awayTeam": {"totalSidelined": 0}},
home_goals_avg=85.0,
home_conceded_avg=79.0,
away_goals_avg=82.0,
away_conceded_avg=81.0,
home_position=4,
away_position=7,
lineup_source="none",
)
prediction = BasketballMatchPrediction(
match_id="b-live-1",
home_team_name="Home B",
away_team_name="Away B",
league_name="B League",
)
self.orchestrator._load_match_data = MagicMock(return_value=match_data)
self.orchestrator.basketball_predictor.predict = MagicMock(return_value=prediction)
self.orchestrator._build_basketball_prediction_package = MagicMock(
return_value={"sport": "basketball", "ok": True}
)
result = self.orchestrator.analyze_match("b-live-1")
self.assertEqual(result, {"sport": "basketball", "ok": True})
self.orchestrator.basketball_predictor.predict.assert_called_once()
kwargs = self.orchestrator.basketball_predictor.predict.call_args.kwargs
self.assertEqual(kwargs["match_id"], "b-live-1")
self.assertEqual(kwargs["home_team_id"], "bh")
self.assertEqual(kwargs["away_team_id"], "ba")
self.assertEqual(kwargs["league_id"], "bleague")
self.assertEqual(kwargs["odds_data"]["ml_h"], 1.75)
self.orchestrator.v25_predictor.predict_market_bundle.assert_not_called()
def test_build_market_rows_maps_odds_keys_correctly(self):
data = MatchData(
match_id="m-rows",
home_team_id="h",
away_team_id="a",
home_team_name="Home",
away_team_name="Away",
match_date_ms=1760000000000,
sport="football",
league_id=None,
league_name="",
referee_name=None,
odds_data={
"ms_h": 2.3,
"ms_d": 3.2,
"ms_a": 3.1,
"dc_x2": 1.45,
"ou15_o": 1.36,
"ou25_u": 1.92,
"ou35_o": 2.85,
"btts_y": 1.88,
"ht_h": 2.55,
"ht_ou05_o": 1.47,
},
home_lineup=None,
away_lineup=None,
sidelined_data=None,
home_goals_avg=1.5,
home_conceded_avg=1.2,
away_goals_avg=1.2,
away_conceded_avg=1.4,
home_position=10,
away_position=10,
lineup_source="none",
)
pred = FullMatchPrediction(
match_id="m-rows",
home_team="Home",
away_team="Away",
ms_home_prob=0.25,
ms_draw_prob=0.30,
ms_away_prob=0.45,
ms_pick="2",
ms_confidence=69.0,
dc_1x_prob=0.60,
dc_x2_prob=0.72,
dc_12_prob=0.68,
dc_pick="X2",
dc_confidence=67.0,
over_15_prob=0.74,
under_15_prob=0.26,
ou15_pick="1.5 Üst",
ou15_confidence=72.0,
over_25_prob=0.44,
under_25_prob=0.56,
ou25_pick="2.5 Alt",
ou25_confidence=61.0,
over_35_prob=0.39,
under_35_prob=0.61,
ou35_pick="3.5 Over",
ou35_confidence=58.0,
btts_yes_prob=0.57,
btts_no_prob=0.43,
btts_pick="Yes",
btts_confidence=63.0,
ht_home_prob=0.41,
ht_draw_prob=0.39,
ht_away_prob=0.20,
ht_pick="1",
ht_confidence=60.0,
ht_over_05_prob=0.64,
ht_under_05_prob=0.36,
ht_ou_pick="Over 0.5",
)
rows = self.orchestrator._build_market_rows(data, pred)
by_market = {row["market"]: row for row in rows}
self.assertEqual(by_market["MS"]["odds"], 3.1)
self.assertEqual(by_market["DC"]["odds"], 1.45)
self.assertEqual(by_market["OU15"]["odds"], 1.36)
self.assertEqual(by_market["OU25"]["odds"], 1.92)
self.assertEqual(by_market["OU35"]["odds"], 2.85)
self.assertEqual(by_market["BTTS"]["odds"], 1.88)
self.assertEqual(by_market["HT"]["odds"], 2.55)
self.assertEqual(by_market["HT_OU05"]["odds"], 1.47)
def test_build_basketball_market_rows_maps_odds_keys_correctly(self):
data = MatchData(
match_id="b-rows",
home_team_id="bh",
away_team_id="ba",
home_team_name="Home B",
away_team_name="Away B",
match_date_ms=1760000000000,
sport="basketball",
league_id="bl",
league_name="Basketball League",
referee_name=None,
odds_data={
"ml_h": 1.73,
"ml_a": 2.10,
"tot_line": 162.5,
"tot_o": 1.89,
"tot_u": 1.93,
"spread_home_line": -4.5,
"spread_h": 1.91,
"spread_a": 1.88,
},
home_lineup=None,
away_lineup=None,
sidelined_data=None,
home_goals_avg=84.0,
home_conceded_avg=80.0,
away_goals_avg=82.0,
away_conceded_avg=81.0,
home_position=5,
away_position=8,
lineup_source="none",
)
pred = {
"match_id": "b-rows",
"market_board": {
"ML": {"1": "62%", "2": "38%"},
"Totals": {"Under 162.5": "43%", "Over 162.5": "57%"},
"Spread": {"Away +4.5": "46%", "Home -4.5": "54%"}
}
}
rows = self.orchestrator._build_basketball_market_rows(data, pred)
by_market = {row["market"]: row for row in rows}
self.assertEqual(by_market["ML"]["odds"], 1.73)
self.assertEqual(by_market["TOTAL"]["odds"], 1.89)
self.assertEqual(by_market["SPREAD"]["odds"], 1.91)
def test_compute_data_quality_flags_missing_referee_and_lineup(self):
data = MatchData(
match_id="dq-1",
home_team_id="h",
away_team_id="a",
home_team_name="Home",
away_team_name="Away",
match_date_ms=1760000000000,
sport="football",
league_id=None,
league_name="",
referee_name=None,
odds_data={"ms_h": 2.5, "ms_d": 3.2, "ms_a": 2.9},
home_lineup=["h1", "h2"],
away_lineup=["a1"],
sidelined_data=None,
home_goals_avg=1.5,
home_conceded_avg=1.2,
away_goals_avg=1.2,
away_conceded_avg=1.4,
home_position=10,
away_position=10,
lineup_source="none",
)
quality = self.orchestrator._compute_data_quality(data)
self.assertIn("lineup_incomplete", quality["flags"])
self.assertIn("missing_referee", quality["flags"])
self.assertEqual(quality["label"], "MEDIUM")
def test_load_match_data_returns_none_when_team_ids_missing(self):
live_row = {
"match_id": "live-missing-ids",
"home_team_id": None,
"away_team_id": None,
"league_id": "l-1",
"sport": "football",
"match_date_ms": 1760000000000,
"odds": None,
"lineups": None,
"sidelined": None,
"referee_name": None,
"home_team_name": "Home",
"away_team_name": "Away",
"league_name": "League",
}
cursor = _RouterCursor(live_row=live_row)
with patch("services.single_match_orchestrator.psycopg2.connect", return_value=_ConnContext(cursor)):
data = self.orchestrator._load_match_data("live-missing-ids")
self.assertIsNone(data)
def test_decorate_market_row_blocks_required_market_when_odds_missing(self):
data = MatchData(
match_id="dq-odds",
home_team_id="h",
away_team_id="a",
home_team_name="Home",
away_team_name="Away",
match_date_ms=1760000000000,
sport="football",
league_id="l1",
league_name="League",
referee_name="Ref",
odds_data={"ms_h": 2.2, "ms_d": 3.2, "ms_a": 3.0},
home_lineup=["h"] * 11,
away_lineup=["a"] * 11,
sidelined_data=None,
home_goals_avg=1.5,
home_conceded_avg=1.2,
away_goals_avg=1.2,
away_conceded_avg=1.4,
home_position=7,
away_position=9,
lineup_source="confirmed_live",
)
prediction = FullMatchPrediction(match_id="dq-odds", home_team="Home", away_team="Away")
quality = self.orchestrator._compute_data_quality(data)
row = {
"market": "HT_OU05",
"pick": "İY 0.5 Üst",
"probability": 0.65,
"confidence": 66.0,
"odds": 0.0,
}
out = self.orchestrator._decorate_market_row(data, prediction, quality, row)
self.assertFalse(out["playable"])
self.assertIn("market_odds_missing", out["decision_reasons"])
if __name__ == "__main__":
unittest.main()
+142
View File
@@ -0,0 +1,142 @@
"""
Unit Test for NEW Skip Logic in BetRecommender
==============================================
Run with: python ai-engine/tests/test_skip_logic.py
"""
import os
import sys
import unittest
from dataclasses import dataclass
from typing import Optional
# Add paths
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..'))
from core.calculators.bet_recommender import BetRecommender, RecommendationResult, MarketPredictionDTO
from core.calculators.risk_assessor import RiskAnalysis
from core.calculators.match_result_calculator import MatchResultPrediction
from core.calculators.over_under_calculator import OverUnderPrediction
from config.config_loader import get_config
@dataclass
class DummyContext:
"""Minimal mock for CalculationContext"""
odds_data: dict
class TestSkipLogic(unittest.TestCase):
def setUp(self):
# Mock config to pass into BetRecommender
self.mock_config = {
"recommendations.market_weights": {"MS": 1.0, "ÇŞ": 0.9, "BTTS": 0.9, "2.5 Üst/Alt": 0.9},
"recommendations.safe_markets": ["ÇŞ", "1.5 Üst/Alt"],
"recommendations.market_accuracy": {"MS": 65, "ÇŞ": 75, "BTTS": 60, "2.5 Üst/Alt": 65},
"recommendations.baseline_accuracy": 65.0,
"recommendations.confidence_threshold": 60,
"recommendations.value_confidence_min": 45,
"recommendations.value_confidence_max": 60,
"recommendations.value_edge_margin": 0.03,
"recommendations.value_upgrade_edge": 5.0,
"recommendations.risk_safe_boost": 1.2,
"recommendations.risk_ms_penalty_high": 0.5,
"recommendations.risk_other_penalty": 0.7,
"recommendations.risk_ms_penalty_medium": 0.8,
}
self.recommender = BetRecommender(self.mock_config)
def _make_risk(self, level="MEDIUM", is_surprise=False):
return RiskAnalysis(risk_level=level, is_surprise_risk=is_surprise, risk_score=0.5)
def _make_ms_pred(self, pick, conf):
# pick: "1", "X", "2"
probs = {"1": {"ms_home_prob": 0.5, "ms_draw_prob": 0.3, "ms_away_prob": 0.2},
"X": {"ms_home_prob": 0.2, "ms_draw_prob": 0.5, "ms_away_prob": 0.3},
"2": {"ms_home_prob": 0.2, "ms_draw_prob": 0.3, "ms_away_prob": 0.5}}
p = probs.get(pick, probs["1"])
return MatchResultPrediction(
ms_pick=pick, ms_confidence=conf,
dc_pick="1X", dc_confidence=0,
dc_1x_prob=0.7, dc_x2_prob=0.7, dc_12_prob=0.7,
**p
)
def _make_ou_pred(self):
return OverUnderPrediction(
ou25_pick="2.5 Üst", ou25_confidence=50.0,
over_25_prob=0.55, under_25_prob=0.45,
btts_pick="Var", btts_confidence=50.0,
btts_yes_prob=0.55, btts_no_prob=0.45,
ou15_pick="1.5 Üst", ou15_confidence=60.0, over_15_prob=0.7, under_15_prob=0.3,
ou35_pick="3.5 Alt", ou35_confidence=50.0, over_35_prob=0.3, under_35_prob=0.7
)
def test_low_confidence_should_skip(self):
"""Confidence < 45% should be SKIPPED"""
ms_pred = self._make_ms_pred(pick="2", conf=40.0)
ou_pred = self._make_ou_pred()
risk = self._make_risk("MEDIUM")
ctx = DummyContext(odds_data={"ms_2": 2.5})
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
# Check if MS bet is skipped
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
self.assertIsNotNone(ms_bet, "MS bet with 40% conf should be skipped!")
self.assertTrue(ms_bet.is_skip)
def test_good_confidence_should_recommend(self):
"""Confidence > 60% and Good Odds should be RECOMMENDED"""
ms_pred = self._make_ms_pred(pick="1", conf=70.0)
ou_pred = self._make_ou_pred()
risk = self._make_risk("MEDIUM")
# Odds 1.80 for 70% prob = Good Value (Need real odds for MS to pass)
ctx = DummyContext(odds_data={"ms_1": 1.80, "ou15_o": 1.50}) # Added ou15 odds
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
# Check if ANY bet is recommended (doesn't have to be MS, but usually is)
self.assertGreater(len(res.recommended_bets), 0, "At least one bet should be recommended!")
# Check that MS bet is NOT skipped
ms_bet = next((b for b in res.recommended_bets if b.market_type == "MS"), None)
if ms_bet:
self.assertFalse(ms_bet.is_skip)
def test_negative_edge_should_skip(self):
"""Even with high confidence, if Odds are too low (Bad Value), SKIP"""
ms_pred = self._make_ms_pred(pick="1", conf=70.0) # 70% prob
ou_pred = self._make_ou_pred()
risk = self._make_risk("MEDIUM")
# Odds 1.10 -> Implied 90%. Our prob is 70%. Edge is -20% -> SKIP
ctx = DummyContext(odds_data={"ms_1": 1.10})
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
ms_bet = next((b for b in res.skipped_bets if b.market_type == "MS"), None)
self.assertIsNotNone(ms_bet, "MS bet with terrible odds (Negative Edge) should be skipped!")
self.assertTrue(ms_bet.is_skip)
def test_no_bets_recommendation(self):
"""If all bets are low confidence, best_bet should be None"""
ms_pred = self._make_ms_pred(pick="1", conf=30.0) # Very low conf
ou_pred = self._make_ou_pred()
# Reset ALL OU confs to low
ou_pred.ou25_confidence = 30.0
ou_pred.btts_confidence = 30.0
ou_pred.ou15_confidence = 30.0 # This was 60 in setUp, causing the fail!
ou_pred.ou35_confidence = 30.0
risk = self._make_risk("MEDIUM")
ctx = DummyContext(odds_data={"ms_1": 2.0})
res = self.recommender.calculate(ctx, ms_pred, ou_pred, risk)
self.assertIsNone(res.best_bet, "If everything is skipped, there should be no best_bet.")
self.assertEqual(len(res.recommended_bets), 0, "No bets should be recommended!")
if __name__ == '__main__':
print("🧪 Running Skip Logic Unit Tests...")
print("="*50)
unittest.main(verbosity=2)
+3 -1
View File
@@ -30,6 +30,8 @@
"cleanup:live": "ts-node -r tsconfig-paths/register src/scripts/cleanup-live-matches.ts",
"swagger:summary": "ts-node -r tsconfig-paths/register src/scripts/export-swagger-endpoints-summary.ts",
"postman:export": "ts-node -r tsconfig-paths/register src/scripts/export-postman-collection.ts",
"predictions:backfill": "ts-node --transpile-only -r tsconfig-paths/register src/scripts/backfill-prediction-runs.ts",
"predictions:report": "ts-node --transpile-only -r tsconfig-paths/register src/scripts/print-backtest-report.ts",
"ai:extract:v26": "python3 ai-engine/scripts/extract_training_data_v26.py",
"ai:train:v26": "python3 ai-engine/scripts/train_v26_shadow.py",
"ai:backtest:v26": "python3 ai-engine/scripts/backtest_v26_shadow.py",
@@ -130,4 +132,4 @@
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
}
+234
View File
@@ -0,0 +1,234 @@
/**
* ===================================================
* HISTORICAL PREDICTION BACKFILL
* ===================================================
* Replays v28-pro-max (or any engine) against historical
* finished matches to populate prediction_runs with
* payload_summary + odds_snapshot + eventual_outcome +
* unit_profit, so we can compute true ROI / calibration.
*
* Requires ENABLE_BACKFILL=true to prevent accidental run.
*
* Usage:
* ENABLE_BACKFILL=true npx ts-node --transpile-only \
* -r tsconfig-paths/register src/scripts/backfill-prediction-runs.ts \
* --from 2023-05-01 --to 2025-12-31 --limit 50000 --sport football
*/
import { PrismaClient } from "@prisma/client";
import axios from "axios";
import {
resolveOutcomeForPick,
computeUnitProfit,
type MatchResult,
} from "../tasks/prediction-settlement.market-resolver";
const prisma = new PrismaClient();
const AI_ENGINE_URL = process.env.AI_ENGINE_URL || "http://127.0.0.1:3005";
const REQUEST_DELAY_MS = Number(process.env.BACKFILL_DELAY_MS ?? 300);
const CONCURRENCY = Number(process.env.BACKFILL_CONCURRENCY ?? 2);
function parseArgs() {
const args = process.argv.slice(2);
const opts: Record<string, string> = {};
for (let i = 0; i < args.length; i += 2) {
opts[args[i].replace(/^--/, "")] = args[i + 1];
}
return {
from: opts.from ?? "2023-05-01",
to: opts.to ?? new Date().toISOString().slice(0, 10),
limit: Number(opts.limit ?? 50000),
sport: (opts.sport ?? "football") as "football" | "basketball",
engine: opts.engine ?? "v28-pro-max",
};
}
function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
async function runOne(matchId: string, engine: string): Promise<any | null> {
try {
const resp = await axios.post(
`${AI_ENGINE_URL}/v20plus/analyze/${matchId}`,
{ engine_version: engine },
{ timeout: 25000 },
);
return resp.data;
} catch (err: any) {
console.error(
`${matchId} ${err?.response?.status ?? ""} ${err?.message ?? ""}`,
);
return null;
}
}
function buildPayloadSummary(payload: any): Record<string, any> {
const main = payload?.main_pick ?? null;
const value = payload?.value_pick ?? null;
const advice = payload?.bet_advice ?? {};
return {
model_version: payload?.model_version,
decision_trace_id: payload?.decision_trace_id ?? null,
main_pick: main
? {
market: main.market,
pick: main.pick,
playable: main.playable,
bet_grade: main.bet_grade,
calibrated_confidence: main.calibrated_confidence,
ev_edge: main.ev_edge ?? 0,
stake_units: main.stake_units,
odds: main.odds ?? null,
}
: null,
value_pick: value
? {
market: value.market,
pick: value.pick,
playable: value.playable,
calibrated_confidence: value.calibrated_confidence,
ev_edge: value.ev_edge ?? 0,
odds: value.odds ?? null,
}
: null,
bet_advice: {
playable: advice.playable ?? false,
suggested_stake_units: advice.suggested_stake_units ?? 0,
},
};
}
function settleFromPayload(
payload: any,
result: MatchResult,
): { outcome: string; unitProfit: number } {
const advice = payload?.bet_advice ?? {};
if (advice.playable !== true) return { outcome: "NO_BET", unitProfit: 0 };
const main = payload?.main_pick;
if (!main || !main.playable) return { outcome: "NO_BET", unitProfit: 0 };
const pickRef = {
market: String(main.market),
pick: String(main.pick),
stake_units: Number(main.stake_units ?? advice.suggested_stake_units ?? 1),
odds: Number(main.odds ?? 0) || null,
};
const won = resolveOutcomeForPick(pickRef, result);
if (won === null) {
return { outcome: "NO_BET", unitProfit: 0 };
}
return {
outcome: `${won ? "WON" : "LOST"}:${pickRef.market}:${pickRef.pick}`,
unitProfit: computeUnitProfit(won, pickRef.stake_units, pickRef.odds),
};
}
async function main() {
if (process.env.ENABLE_BACKFILL !== "true") {
console.error(
"✗ Backfill is gated. Re-run with ENABLE_BACKFILL=true to proceed.",
);
process.exit(1);
}
const opts = parseArgs();
console.log("📦 Backfill prediction_runs");
console.log(` Range: ${opts.from}${opts.to}`);
console.log(` Sport: ${opts.sport}`);
console.log(` Engine: ${opts.engine}`);
console.log(` Limit: ${opts.limit}`);
console.log(` AI engine: ${AI_ENGINE_URL}`);
const fromMs = BigInt(new Date(opts.from).getTime());
const toMs = BigInt(new Date(opts.to).getTime());
const matches = await prisma.match.findMany({
where: {
sport: opts.sport as any,
status: "FT",
scoreHome: { not: null },
scoreAway: { not: null },
mstUtc: { gte: fromMs, lte: toMs },
},
orderBy: { mstUtc: "asc" },
take: opts.limit,
select: {
id: true,
mstUtc: true,
scoreHome: true,
scoreAway: true,
htScoreHome: true,
htScoreAway: true,
},
});
console.log(`\n ${matches.length} finished matches in range\n`);
let predicted = 0;
let written = 0;
let skipped = 0;
for (let i = 0; i < matches.length; i += CONCURRENCY) {
const batch = matches.slice(i, i + CONCURRENCY);
await Promise.all(
batch.map(async (m) => {
const existing = await prisma.predictionRun.findFirst({
where: { matchId: m.id, engineVersion: opts.engine },
select: { id: true },
});
if (existing) {
skipped += 1;
return;
}
const payload = await runOne(m.id, opts.engine);
if (!payload) return;
predicted += 1;
const summary = buildPayloadSummary(payload);
const result: MatchResult = {
scoreHome: m.scoreHome!,
scoreAway: m.scoreAway!,
htScoreHome: m.htScoreHome,
htScoreAway: m.htScoreAway,
};
const settled = settleFromPayload(payload, result);
await prisma.predictionRun.create({
data: {
matchId: m.id,
engineVersion: payload?.model_version ?? opts.engine,
decisionTraceId: payload?.decision_trace_id ?? null,
oddsSnapshot: payload?.odds_snapshot ?? null,
payloadSummary: summary,
eventualOutcome: settled.outcome,
unitProfit: settled.unitProfit,
},
});
written += 1;
}),
);
if ((i / CONCURRENCY) % 25 === 0) {
console.log(
` [${i + batch.length}/${matches.length}] predicted=${predicted} written=${written} skipped=${skipped}`,
);
}
await sleep(REQUEST_DELAY_MS);
}
console.log("\n✅ Backfill complete");
console.log(` Predicted: ${predicted}`);
console.log(` Written: ${written}`);
console.log(` Skipped: ${skipped}`);
await prisma.$disconnect();
}
main().catch(async (err) => {
console.error(err);
await prisma.$disconnect();
process.exit(1);
});
+208
View File
@@ -0,0 +1,208 @@
/**
* ===================================================
* BACKTEST REPORT
* ===================================================
* Reads prediction_runs (with eventual_outcome + unit_profit
* already filled by settlement / backfill) and prints
* per-engine x per-market ROI tables. Also writes JSON.
*
* Usage:
* npx ts-node --transpile-only -r tsconfig-paths/register \
* src/scripts/print-backtest-report.ts [--engine v28-pro-max]
*/
import { PrismaClient, Prisma } from "@prisma/client";
import * as fs from "fs";
import * as path from "path";
const prisma = new PrismaClient();
interface RawRow {
engine_version: string;
market: string | null;
bet_grade: string | null;
outcome_kind: "WON" | "LOST" | "NO_BET";
unit_profit: number | null;
count: bigint;
}
interface AggregatedBucket {
engine: string;
market: string;
betGrade: string;
totalBets: number;
wins: number;
losses: number;
noBets: number;
totalProfit: number;
roi: number;
winRate: number;
}
function parseArgs() {
const args = process.argv.slice(2);
const opts: Record<string, string> = {};
for (let i = 0; i < args.length; i += 2) {
opts[args[i].replace(/^--/, "")] = args[i + 1];
}
return { engineFilter: opts.engine ?? null };
}
async function loadRows(engineFilter: string | null): Promise<RawRow[]> {
const engineClause = engineFilter
? Prisma.sql`AND pr.engine_version = ${engineFilter}`
: Prisma.sql``;
return prisma.$queryRaw<RawRow[]>(Prisma.sql`
SELECT pr.engine_version,
pr.payload_summary->'main_pick'->>'market' AS market,
pr.payload_summary->'main_pick'->>'bet_grade' AS bet_grade,
CASE
WHEN pr.eventual_outcome LIKE 'WON:%' THEN 'WON'
WHEN pr.eventual_outcome LIKE 'LOST:%' THEN 'LOST'
ELSE 'NO_BET'
END AS outcome_kind,
pr.unit_profit,
1::bigint AS count
FROM prediction_runs pr
WHERE pr.eventual_outcome IS NOT NULL
${engineClause}
`);
}
function aggregate(rows: RawRow[]): AggregatedBucket[] {
const map = new Map<string, AggregatedBucket>();
for (const r of rows) {
const market = r.market ?? "(none)";
const betGrade = r.bet_grade ?? "(none)";
const key = `${r.engine_version}|${market}|${betGrade}`;
if (!map.has(key)) {
map.set(key, {
engine: r.engine_version,
market,
betGrade,
totalBets: 0,
wins: 0,
losses: 0,
noBets: 0,
totalProfit: 0,
roi: 0,
winRate: 0,
});
}
const b = map.get(key)!;
if (r.outcome_kind === "WON") b.wins += 1;
else if (r.outcome_kind === "LOST") b.losses += 1;
else b.noBets += 1;
if (r.outcome_kind !== "NO_BET") b.totalBets += 1;
b.totalProfit += Number(r.unit_profit ?? 0);
}
const out = Array.from(map.values());
for (const b of out) {
b.roi = b.totalBets > 0 ? b.totalProfit / b.totalBets : 0;
b.winRate = b.totalBets > 0 ? b.wins / b.totalBets : 0;
}
return out.sort((a, b) => b.totalBets - a.totalBets);
}
function printTable(buckets: AggregatedBucket[]) {
const header = [
"engine".padEnd(20),
"market".padEnd(10),
"grade".padEnd(6),
"bets".padStart(7),
"wins".padStart(7),
"losses".padStart(7),
"noBet".padStart(7),
"winRate".padStart(8),
"profit".padStart(10),
"ROI".padStart(8),
].join(" │ ");
console.log(header);
console.log("─".repeat(header.length));
for (const b of buckets) {
console.log(
[
b.engine.padEnd(20),
b.market.padEnd(10),
b.betGrade.padEnd(6),
String(b.totalBets).padStart(7),
String(b.wins).padStart(7),
String(b.losses).padStart(7),
String(b.noBets).padStart(7),
`${(b.winRate * 100).toFixed(1)}%`.padStart(8),
b.totalProfit.toFixed(2).padStart(10),
`${(b.roi * 100).toFixed(2)}%`.padStart(8),
].join(" │ "),
);
}
}
function engineSummary(buckets: AggregatedBucket[]) {
const byEngine = new Map<string, AggregatedBucket>();
for (const b of buckets) {
if (!byEngine.has(b.engine)) {
byEngine.set(b.engine, {
engine: b.engine,
market: "ALL",
betGrade: "ALL",
totalBets: 0,
wins: 0,
losses: 0,
noBets: 0,
totalProfit: 0,
roi: 0,
winRate: 0,
});
}
const acc = byEngine.get(b.engine)!;
acc.totalBets += b.totalBets;
acc.wins += b.wins;
acc.losses += b.losses;
acc.noBets += b.noBets;
acc.totalProfit += b.totalProfit;
}
const out = Array.from(byEngine.values());
for (const e of out) {
e.roi = e.totalBets > 0 ? e.totalProfit / e.totalBets : 0;
e.winRate = e.totalBets > 0 ? e.wins / e.totalBets : 0;
}
return out;
}
async function main() {
const { engineFilter } = parseArgs();
console.log("📊 Backtest report");
if (engineFilter) console.log(` filter engine = ${engineFilter}`);
const rows = await loadRows(engineFilter);
console.log(` ${rows.length} resolved prediction_runs loaded\n`);
const buckets = aggregate(rows);
const enginesAll = engineSummary(buckets);
console.log("=== Per engine (all markets) ===");
printTable(enginesAll);
console.log("\n=== Per engine × market × bet_grade ===");
printTable(buckets);
const ts = new Date().toISOString().replace(/[:.]/g, "-");
const outDir = path.join(process.cwd(), "reports");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir);
const outFile = path.join(outDir, `backtest-${ts}.json`);
fs.writeFileSync(
outFile,
JSON.stringify({ engines: enginesAll, buckets }, null, 2),
"utf8",
);
console.log(`\n💾 Saved ${outFile}`);
await prisma.$disconnect();
}
main().catch(async (err) => {
console.error(err);
await prisma.$disconnect();
process.exit(1);
});
@@ -0,0 +1,127 @@
export interface MatchResult {
scoreHome: number;
scoreAway: number;
htScoreHome: number | null;
htScoreAway: number | null;
}
export interface PickRef {
market: string;
pick: string;
stake_units: number;
odds: number | null;
}
type Resolver = (pick: string, r: MatchResult) => boolean | null;
const ms1x2: Resolver = (pick, r) => {
const outcome = r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X";
return pick === outcome;
};
const ht1x2: Resolver = (pick, r) => {
if (r.htScoreHome === null || r.htScoreAway === null) return null;
const outcome =
r.htScoreHome > r.htScoreAway ? "1" : r.htScoreHome < r.htScoreAway ? "2" : "X";
return pick === outcome;
};
const overUnder = (line: number): Resolver => (pick, r) => {
const total = r.scoreHome + r.scoreAway;
if (total === line) return null;
const isOver = total > line;
if (pick === "Üst" || pick === "Ust" || pick.toLowerCase() === "over") return isOver;
if (pick === "Alt" || pick.toLowerCase() === "under") return !isOver;
return null;
};
const overUnderHt = (line: number): Resolver => (pick, r) => {
if (r.htScoreHome === null || r.htScoreAway === null) return null;
const total = r.htScoreHome + r.htScoreAway;
if (total === line) return null;
const isOver = total > line;
if (pick === "Üst" || pick === "Ust" || pick.toLowerCase() === "over") return isOver;
if (pick === "Alt" || pick.toLowerCase() === "under") return !isOver;
return null;
};
const btts: Resolver = (pick, r) => {
const both = r.scoreHome > 0 && r.scoreAway > 0;
if (pick === "Var" || pick === "KG Var" || pick.toLowerCase() === "yes") return both;
if (pick === "Yok" || pick === "KG Yok" || pick.toLowerCase() === "no") return !both;
return null;
};
const htft: Resolver = (pick, r) => {
if (r.htScoreHome === null || r.htScoreAway === null) return null;
const ht =
r.htScoreHome > r.htScoreAway ? "1" : r.htScoreHome < r.htScoreAway ? "2" : "X";
const ft = r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X";
const normalized = pick.replace(/\s/g, "").toUpperCase();
return normalized === `${ht}/${ft}`;
};
const doubleChance: Resolver = (pick, r) => {
const ft = r.scoreHome > r.scoreAway ? "1" : r.scoreHome < r.scoreAway ? "2" : "X";
const normalized = pick.replace(/\s/g, "").toUpperCase().split(/[\/\-]/);
if (normalized.length !== 2) return null;
return normalized.includes(ft);
};
const oddEven: Resolver = (pick, r) => {
const total = r.scoreHome + r.scoreAway;
const isOdd = total % 2 === 1;
if (pick === "Tek" || pick.toLowerCase() === "odd") return isOdd;
if (pick === "Çift" || pick === "Cift" || pick.toLowerCase() === "even") return !isOdd;
return null;
};
const resolvers: Record<string, Resolver> = {
MS: ms1x2,
ML: ms1x2,
"1X2": ms1x2,
HT: ht1x2,
IY: ht1x2,
OU05: overUnder(0.5),
OU15: overUnder(1.5),
OU25: overUnder(2.5),
OU35: overUnder(3.5),
OU45: overUnder(4.5),
TOTAL: overUnder(2.5),
OU05_HT: overUnderHt(0.5),
OU15_HT: overUnderHt(1.5),
OU25_HT: overUnderHt(2.5),
BTTS: btts,
KG: btts,
HTFT: htft,
IYMS: htft,
DC: doubleChance,
CIFTE_SANS: doubleChance,
OE: oddEven,
TEKCIFT: oddEven,
};
export function resolveOutcomeForPick(
pick: PickRef,
result: MatchResult,
): boolean | null {
const market = pick.market.toUpperCase().replace(/[\s\-]/g, "_");
const resolver = resolvers[market] ?? resolvers[pick.market];
if (!resolver) return null;
try {
return resolver(pick.pick, result);
} catch {
return null;
}
}
export function computeUnitProfit(
won: boolean,
stakeUnits: number,
odds: number | null,
): number {
const stake = Number.isFinite(stakeUnits) && stakeUnits > 0 ? stakeUnits : 1;
if (!won) return -stake;
if (!odds || odds <= 1) return 0;
return Number((stake * (odds - 1)).toFixed(4));
}
+179
View File
@@ -0,0 +1,179 @@
import { Injectable, Logger } from "@nestjs/common";
import { Cron } from "@nestjs/schedule";
import { Prisma } from "@prisma/client";
import { PrismaService } from "../database/prisma.service";
import { TaskLockService } from "./task-lock.service";
import {
resolveOutcomeForPick,
computeUnitProfit,
type MatchResult,
type PickRef,
} from "./prediction-settlement.market-resolver";
interface UnresolvedRow {
id: bigint;
match_id: string;
payload_summary: Prisma.JsonValue;
odds_snapshot: Prisma.JsonValue;
score_home: number | null;
score_away: number | null;
ht_score_home: number | null;
ht_score_away: number | null;
status: string | null;
}
const BATCH_SIZE = 500;
@Injectable()
export class PredictionSettlementTask {
private readonly logger = new Logger(PredictionSettlementTask.name);
constructor(
private readonly prisma: PrismaService,
private readonly taskLock: TaskLockService,
) {}
/**
* Runs after the previous-day match sync. Reconciles prediction_runs against actual results.
*/
@Cron("30 8 * * *", { timeZone: "Europe/Istanbul" })
async settleCompletedPredictions() {
if (process.env.FEEDER_MODE === "historical") {
this.logger.debug("Skipping settlement in historical feeder mode");
return;
}
await this.taskLock.runWithLease(
"settleCompletedPredictions",
2 * 60 * 60 * 1000,
() => this.runSettlement(),
this.logger,
);
}
async runSettlement(): Promise<{ scanned: number; updated: number }> {
let scanned = 0;
let updated = 0;
let cursor = 0n;
for (;;) {
const rows = await this.prisma.$queryRaw<UnresolvedRow[]>(Prisma.sql`
SELECT pr.id,
pr.match_id,
pr.payload_summary,
pr.odds_snapshot,
m.score_home,
m.score_away,
m.ht_score_home,
m.ht_score_away,
m.status
FROM prediction_runs pr
JOIN matches m ON m.id = pr.match_id
WHERE pr.eventual_outcome IS NULL
AND pr.id > ${cursor}
AND m.status = 'FT'
AND m.score_home IS NOT NULL
AND m.score_away IS NOT NULL
ORDER BY pr.id ASC
LIMIT ${BATCH_SIZE}
`);
if (rows.length === 0) break;
for (const row of rows) {
scanned += 1;
const settled = this.settleRow(row);
if (settled === null) continue;
await this.prisma.$executeRaw(Prisma.sql`
UPDATE prediction_runs
SET eventual_outcome = ${settled.outcome},
unit_profit = ${settled.unitProfit}
WHERE id = ${row.id}
`);
updated += 1;
}
cursor = rows[rows.length - 1].id;
}
this.logger.log(
`Settlement finished: scanned=${scanned} updated=${updated}`,
);
return { scanned, updated };
}
private settleRow(
row: UnresolvedRow,
): { outcome: string; unitProfit: number } | null {
const summary = (row.payload_summary ?? {}) as Record<string, any>;
const pick = this.pickToEvaluate(summary);
if (!pick) {
return { outcome: "NO_BET", unitProfit: 0 };
}
const result: MatchResult = {
scoreHome: row.score_home!,
scoreAway: row.score_away!,
htScoreHome: row.ht_score_home,
htScoreAway: row.ht_score_away,
};
const won = resolveOutcomeForPick(pick, result);
if (won === null) {
this.logger.debug(
`Cannot resolve market=${pick.market} pick=${pick.pick} for run=${row.id}`,
);
return null;
}
const odds = this.extractOdds(row.odds_snapshot, pick);
const stake = pick.stake_units ?? 1;
const profit = computeUnitProfit(won, stake, odds);
return {
outcome: `${won ? "WON" : "LOST"}:${pick.market}:${pick.pick}`,
unitProfit: profit,
};
}
private pickToEvaluate(summary: Record<string, any>): PickRef | null {
const advice = summary.bet_advice ?? {};
if (advice.playable !== true) return null;
const main = summary.main_pick;
if (main && main.playable && main.market && main.pick) {
return {
market: String(main.market),
pick: String(main.pick),
stake_units: Number(main.stake_units ?? advice.suggested_stake_units ?? 1),
odds: Number(main.odds ?? 0) || null,
};
}
const fallback = summary.value_pick;
if (fallback && fallback.playable && fallback.market && fallback.pick) {
return {
market: String(fallback.market),
pick: String(fallback.pick),
stake_units: Number(advice.suggested_stake_units ?? 1),
odds: Number(fallback.odds ?? 0) || null,
};
}
return null;
}
private extractOdds(
oddsSnapshot: Prisma.JsonValue,
pick: PickRef,
): number | null {
if (pick.odds && pick.odds > 1) return pick.odds;
if (!oddsSnapshot || typeof oddsSnapshot !== "object") return null;
const snap = oddsSnapshot as Record<string, any>;
const odds = snap.odds;
if (!odds || typeof odds !== "object") return null;
const category = odds[pick.market];
if (!category || typeof category !== "object") return null;
const value = Number(category[pick.pick]);
return Number.isFinite(value) && value > 1 ? value : null;
}
}
+8 -1
View File
@@ -3,6 +3,7 @@ import { HttpModule } from "@nestjs/axios";
import { DataFetcherTask } from "./data-fetcher.task";
import { HistoricalResultsSyncTask } from "./historical-results-sync.task";
import { LimitResetterTask } from "./limit-resetter.task";
import { PredictionSettlementTask } from "./prediction-settlement.task";
import { TaskLockService } from "./task-lock.service";
import { DatabaseModule } from "../database/database.module";
import { FeederModule } from "../modules/feeder/feeder.module";
@@ -24,7 +25,13 @@ import { FeederModule } from "../modules/feeder/feeder.module";
DataFetcherTask,
HistoricalResultsSyncTask,
LimitResetterTask,
PredictionSettlementTask,
],
exports: [
DataFetcherTask,
HistoricalResultsSyncTask,
LimitResetterTask,
PredictionSettlementTask,
],
exports: [DataFetcherTask, HistoricalResultsSyncTask, LimitResetterTask],
})
export class TasksModule {}