266 lines
9.9 KiB
Python
Executable File
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()
|