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()