Files
iddaai-be/scripts/predict_live_match.py
fahricansecer 2f0b85a0c7
Deploy Iddaai Backend / build-and-deploy (push) Failing after 18s
first (part 2: other directories)
2026-04-16 15:11:25 +03:00

266 lines
9.9 KiB
Python
Executable File

import os
import sys
import torch
import torch.nn.functional as F
import pandas as pd
import numpy as np
import psycopg2
from psycopg2.extras import RealDictCursor
from datetime import datetime
# Add path
sys.path.append(os.getcwd())
sys.path.append(os.path.join(os.getcwd(), 'ai-engine'))
from models.hybrid_v11 import HybridDeepModel
from features.odds_history import OddsHistoryEngine
DEVICE = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
MODEL_PATH = 'ai-engine/models/v11_hybrid_model.pth'
MATCH_ID = '8yl78ecnv1fqynawwtf5159uc' # User Request Re-test
def get_db_conn():
db_url = os.environ.get('DATABASE_URL', 'postgresql://suggestbet:SuGGesT2026SecuRe@localhost:15432/boilerplate_db')
return psycopg2.connect(db_url, cursor_factory=RealDictCursor)
def get_team_history(conn, team_id, match_date, seq_len=10):
query = """
SELECT
m.home_team_id, m.away_team_id,
m.score_home, m.score_away,
m.ht_score_home, m.ht_score_away
FROM matches m
WHERE (m.home_team_id = %s OR m.away_team_id = %s)
AND m.mst_utc < %s
AND m.status = 'FT'
AND m.score_home IS NOT NULL
ORDER BY m.mst_utc DESC
LIMIT %s
"""
with conn.cursor() as cur:
cur.execute(query, (team_id, team_id, match_date, seq_len))
rows = cur.fetchall()
if len(rows) < seq_len:
print(f"⚠️ Warning: Not enough history for team {team_id} (Found {len(rows)})")
# Pad with zeros or return None?
# Let's pad with simple placeholders if needed, but better to just use what we have or duplicate
pass
# Process rows (Reverse order to be chronological)
history = []
for r in reversed(rows):
# Normalize to Team Perspective
is_home = (r['home_team_id'] == team_id)
if is_home:
gf = r['score_home']
ga = r['score_away']
res = 1.0 if gf > ga else (0.5 if gf == ga else 0.0)
loc = 1.0
else:
gf = r['score_away']
ga = r['score_home']
res = 1.0 if gf > ga else (0.5 if gf == ga else 0.0)
loc = 0.0
history.append([gf, ga, res, loc])
# Pad if short
while len(history) < seq_len:
history.insert(0, [0, 0, 0.5, 0.5]) # Neutral padding
return np.array(history, dtype=np.float32)
def predict():
print(f"🔮 Predicting Match: {MATCH_ID}")
conn = get_db_conn()
# 1. Get Match Info
with conn.cursor() as cur:
# Try 'matches' table first
cur.execute("""
SELECT m.home_team_id, m.away_team_id, m.mst_utc,
ht.name as home_name_db, at.name as away_name_db
FROM matches m
LEFT JOIN teams ht ON m.home_team_id = ht.id
LEFT JOIN teams at ON m.away_team_id = at.id
WHERE m.id = %s
""", (MATCH_ID,))
match = cur.fetchone()
if not match:
print("⚠️ Match not found in 'matches'. Checking 'live_matches'...")
cur.execute("""
SELECT m.home_team_id, m.away_team_id, m.mst_utc, m.match_name,
ht.name as home_name_db, at.name as away_name_db
FROM live_matches m
LEFT JOIN teams ht ON m.home_team_id = ht.id
LEFT JOIN teams at ON m.away_team_id = at.id
WHERE m.id = %s
""", (MATCH_ID,))
match = cur.fetchone()
if not match:
print("❌ Match not found in either table!")
return
# Fallback for names
home_name = match.get('home_name_db') or (match.get('match_name') or 'Unknown Home').split(' vs ')[0]
away_name = match.get('away_name_db') or (match.get('match_name') or 'Unknown Away').split(' vs ')[-1]
# CRITICAL FIX for match 8yl... where IDs are null
if MATCH_ID == '8yl78ecnv1fqynawwtf5159uc':
if not match['home_team_id']:
match['home_team_id'] = 'bmgtxgipsznlb1j20zwjti3xh' # Eyüpspor
print(f" 🛠️ Injected Home ID: {match['home_team_id']} (Eyüpspor)")
if not match['away_team_id']:
match['away_team_id'] = '2ez9cvam9lp9jyhng3eh3znb4' # Beşiktaş
print(f" 🛠️ Injected Away ID: {match['away_team_id']} (Beşiktaş)")
print(f"⚔️ {home_name} vs {away_name}")
print(f"📅 Date: {datetime.fromtimestamp(match['mst_utc']/1000)}")
# 2. Get Odds (Context) -> Odds might be in 'odds' column JSON in live_matches?
# Or odd_categories table might link to live_matches? Usually they link via match_id regardless of table.
# We will assume odd_categories works or default.
with conn.cursor() as cur:
try:
cur.execute("""
SELECT oc.name, os.name as selection, os.odd_value
FROM odd_categories oc
JOIN odd_selections os ON oc.db_id = os.odd_category_db_id
WHERE oc.match_id = %s AND oc.name IN ('Maç Sonucu', '2,5 Alt/Üst')
""", (MATCH_ID,))
odds_rows = cur.fetchall()
except Exception as e:
print(f"⚠️ Odds fetch failed: {e}")
conn.rollback()
odds_rows = []
odds = {'1': 2.5, 'X': 3.2, '2': 2.5, 'Over': 1.80, 'Under': 1.80} # Defaults
for r in odds_rows:
sel = r['selection']
val = float(r['odd_value'])
if r['name'] == 'Maç Sonucu':
if sel in ['1', 'X', '2']: odds[sel] = val
elif r['name'] == '2,5 Alt/Üst':
if 'Üst' in sel or 'Over' in sel: odds['Over'] = val
if 'Alt' in sel or 'Under' in sel: odds['Under'] = val
print(f"📊 Market Odds: 1:{odds['1']} X:{odds['X']} 2:{odds['2']} | O:{odds['Over']} U:{odds['Under']}")
# 3. Build Sequences
seq_home = get_team_history(conn, match['home_team_id'], match['mst_utc'])
# 4. Reconstruct Team Map (MUST match training logic)
# This ensures Team IDs map to the correct Embedding Indices.
from pipeline.sequence_builder import SequenceBuilder
print(" 🗺️ Reconstructing Team Map for Identity alignment...")
builder = SequenceBuilder()
_, _, meta_all = builder.build_sequences()
unique_teams = meta_all['team_id'].unique()
team_map = {tid: i for i, tid in enumerate(unique_teams)}
# Get Indices (Fallback to 0 if not found)
home_idx = team_map.get(match['home_team_id'], 0)
away_idx = team_map.get(match['away_team_id'], 0)
print(f" 🆔 Identity: {home_name} (Idx:{home_idx}) vs {away_name} (Idx:{away_idx})")
# 5. Load Model
# ... (Model loading logic follows)
try:
state = torch.load(MODEL_PATH, map_location=DEVICE)
# Handle shape mismatch if num_teams changed?
# State dict has specific size for 'entity_emb.weight'.
emb_key = 'entity_emb.weight' if 'entity_emb.weight' in state else 'team_embedding.weight'
saved_vocab_size = state[emb_key].shape[0]
# Initialize & Load
model = HybridDeepModel(num_teams=saved_vocab_size)
new_state = {k.replace('team_embedding', 'entity_emb'): v for k, v in state.items()}
model.load_state_dict(new_state, strict=False)
print("✅ Model loaded successfully.")
except Exception as e:
print(f"❌ Model load failed: {e}")
return
model.eval()
# 5. Prepare Input Tensors
entities = torch.LongTensor([home_idx, away_idx]).unsqueeze(0).to(DEVICE)
seq = torch.FloatTensor(seq_home).unsqueeze(0).to(DEVICE)
eng = OddsHistoryEngine()
hist_win_rate = eng.get_feature(match['home_team_id'], odds['1'])
syn_xg = 1.35 # Avg
ctx = torch.FloatTensor([
odds['1'], odds['X'], odds['2'],
odds['Over'], odds['Under'],
syn_xg, syn_xg,
hist_win_rate
]).unsqueeze(0).to(DEVICE)
# 6. Predict
with torch.no_grad():
logits_res, pred_goals, logits_btts, logits_ht_ft = model(entities, seq, ctx)
# 1X2
probs_1x2 = F.softmax(logits_res, dim=1).cpu().numpy()[0]
# Goals
exp_goals = pred_goals.item()
# BTTS
prob_btts = torch.sigmoid(logits_btts).item()
# HT/FT
probs_ht = F.softmax(logits_ht_ft, dim=1).cpu().numpy()[0]
# 7. Report with Value Analysis
print("\n🧠 AI PREDICTION REPORT (V11 REFINED)")
print("-" * 40)
print(f"\n🏆 MATCH RESULT (1X2) - VALUE ANALYSIS")
headers = ["Selection", "AI Prob", "Market Odd", "Exp. Value (EV)"]
print(f"{headers[0]:<10} | {headers[1]:<8} | {headers[2]:<10} | {headers[3]:<12}")
print("-" * 40)
outcomes = [('1', probs_1x2[0]), ('X', probs_1x2[1]), ('2', probs_1x2[2])]
best_ev = -99
value_bet = None
for label, prob in outcomes:
odd = float(odds.get(label, 1.0))
ev = (prob * odd) - 1.0 # Expected profit per 1 unit stake
color = "" if ev > 0.1 else ("⚠️" if ev > -0.1 else "")
print(f"{label:<10} | {prob*100:>7.1f}% | {odd:>10.2f} | {ev:>12.2f} {color}")
if ev > best_ev:
best_ev = ev
value_bet = label
print(f"\n👉 AI RECOMMENDATION: {value_bet} (EV: {best_ev:.2f})")
if best_ev > 1.0:
print("🔥 WARNING: HIGH VALUE ALERT! Odds significantly underpriced by market.")
print(f"\n⚽ GOALS ANALYSIS")
print(f" Expected Goals: {exp_goals:.2f}")
# Banko check
if exp_goals < 4.5: print(" 🛡️ BANKO: 4.5 Alt/Under (High Conf)")
if exp_goals > 1.5: print(" 🛡️ BANKO: 1.5 Üst/Over (High Conf)")
print(f"\n⌛ HT/FT (Half Time / Full Time) - ALL CLASSES")
ht_map = ["1/1", "1/X", "1/2", "X/1", "X/X", "X/2", "2/1", "2/X", "2/2"]
all_idx = np.argsort(probs_ht)[::-1]
for idx in all_idx:
print(f" {ht_map[idx]:<4}: {probs_ht[idx]*100:>5.1f}%")
if __name__ == "__main__":
predict()