""" Upset Engine - Dev Avcısı Tespit Sistemi V9 Model için Galatasaray-Liverpool tarzı sürpriz maçları tespit eder. Faktörler: 1. Atmosfer (Avrupa gecesi, taraftar baskısı) 2. Motivasyon asimetrisi (küme düşme vs şampiyon) 3. Yorgunluk (maç yoğunluğu, seyahat) 4. Tarihsel upset pattern """ import os import sys from typing import Dict, Any, Optional, Tuple from dataclasses import dataclass, field # Add parent directory to path for imports sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) try: import psycopg2 from psycopg2.extras import RealDictCursor except ImportError: psycopg2 = None @dataclass class UpsetFactors: """Upset potansiyelini etkileyen faktörler""" atmosphere_score: float = 0.0 # Atmosfer etkisi (0-1) motivation_score: float = 0.0 # Motivasyon asimetrisi (0-1) fatigue_score: float = 0.0 # Yorgunluk farkı (0-1) historical_upset_rate: float = 0.0 # Tarihsel upset oranı (0-1) total_upset_potential: float = 0.0 # Toplam upset potansiyeli (0-1) reasoning: list = field(default_factory=list) class UpsetEngine: """ Favori takımın kaybedeceği maçları tespit eder. Galatasaray-Liverpool tarzı sürprizleri yakalar. """ # Yüksek atmosferli stadyumlar (manuel tanımlı + hesaplanabilir) HIGH_ATMOSPHERE_TEAMS = { # Türkiye "galatasaray", "fenerbahce", "besiktas", "trabzonspor", # İngiltere "liverpool", "newcastle", "leeds", # Almanya "dortmund", "union berlin", # Yunanistan "olympiacos", "panathinaikos", "aek athens", # Arjantin "boca juniors", "river plate", # Diğer "celtic", "rangers", "red star belgrade" } # Avrupa kupaları (yüksek motivasyon) EUROPEAN_COMPETITIONS = { "şampiyonlar ligi", "champions league", "uefa champions league", "avrupa ligi", "europa league", "uefa europa league", "konferans ligi", "conference league", "uefa conference league" } def __init__(self): self.conn = None self._connect_db() def _connect_db(self): """Veritabanına bağlan""" if psycopg2 is None: return try: from data.db import get_clean_dsn self.conn = psycopg2.connect(get_clean_dsn()) except Exception as e: print(f"[UpsetEngine] DB connection failed: {e}") self.conn = None def _get_conn(self): """Bağlantıyı kontrol et ve döndür""" if self.conn is None or self.conn.closed: self._connect_db() return self.conn def calculate_atmosphere_score( self, home_team_name: str, league_name: str, is_cup_match: bool = False ) -> Tuple[float, list]: """ Atmosfer skorunu hesapla. Yüksek atmosferli stadyumlar upset potansiyelini artırır. """ score = 0.0 reasons = [] # Yüksek atmosferli takım mı? home_lower = home_team_name.lower() for team in self.HIGH_ATMOSPHERE_TEAMS: if team in home_lower: score += 0.25 reasons.append(f"🔥 {home_team_name} yüksek atmosferli stadyum") break # Avrupa kupası mı? league_lower = league_name.lower() for comp in self.EUROPEAN_COMPETITIONS: if comp in league_lower: score += 0.20 reasons.append("🌟 Avrupa gecesi - ekstra motivasyon") break # Kupa maçı mı? (tek maç eliminasyon) if is_cup_match: score += 0.10 reasons.append("🏆 Kupa maçı - her şey olabilir") return min(score, 1.0), reasons def calculate_motivation_score( self, home_position: int, away_position: int, home_points_to_safety: Optional[int] = None, away_already_champion: bool = False, total_teams: int = 20 ) -> Tuple[float, list]: """ Motivasyon asimetrisini hesapla. Alt sıradaki takımın üst sıradakine karşı ekstra motivasyonu. """ score = 0.0 reasons = [] # Pozisyon farkı position_diff = 0 if away_position is not None and home_position is not None: position_diff = away_position - home_position # Negatif = deplasman daha iyi sırada # Küme düşme hattı vs üst sıra (en güçlü upset faktörü) relegation_zone = total_teams - 3 # Son 3 takım if home_position is not None and away_position is not None: if home_position >= relegation_zone and away_position <= 3: score += 0.30 reasons.append("⚔️ Hayatta kalma savaşı vs şampiyonluk adayı") elif home_position >= relegation_zone: score += 0.15 reasons.append("🔥 Ev sahibi küme düşme hattında - ekstra motivasyon") elif home_position is not None and home_position >= relegation_zone: score += 0.15 reasons.append("🔥 Ev sahibi küme düşme hattında - ekstra motivasyon") # Deplasman takımı zaten şampiyon mu? if away_already_champion: score += 0.20 reasons.append("😴 Deplasman takımı zaten şampiyon - motivasyon düşük") # Büyük pozisyon farkı (underdog evinde) if position_diff < -10: score += 0.15 reasons.append(f"📊 {abs(position_diff)} sıra fark - büyük maç heyecanı") elif position_diff < -5: score += 0.08 return min(score, 1.0), reasons def calculate_fatigue_score( self, home_matches_last_14d: int = 0, away_matches_last_14d: int = 0, home_days_rest: int = 7, away_days_rest: int = 7, away_travel_km: float = 0 ) -> Tuple[float, list]: """ Yorgunluk farkını hesapla. Yorgun deplasman takımı = yüksek upset potansiyeli. """ score = 0.0 reasons = [] # Maç yoğunluğu farkı match_diff = away_matches_last_14d - home_matches_last_14d if match_diff >= 3: score += 0.20 reasons.append(f"🏃 Deplasman {match_diff} maç daha fazla oynamış") elif match_diff >= 2: score += 0.10 # Dinlenme süresi farkı rest_diff = home_days_rest - away_days_rest if rest_diff >= 4: score += 0.15 reasons.append(f"💤 Ev sahibi {rest_diff} gün daha fazla dinlenmiş") elif rest_diff >= 2: score += 0.08 # Uzun deplasman if away_travel_km > 3000: score += 0.15 reasons.append(f"✈️ Uzun deplasman ({int(away_travel_km)} km)") elif away_travel_km > 1500: score += 0.08 return min(score, 1.0), reasons def get_historical_upset_rate( self, home_team_id: str, before_date_ms: int, lookback_matches: int = 20 ) -> Tuple[float, list]: """ Ev sahibi takımın tarihsel upset oranını hesapla. Üst sıradaki takımlara karşı galibiyetler. """ reasons = [] conn = self._get_conn() if conn is None: return 0.0, reasons try: cursor = conn.cursor(cursor_factory=RealDictCursor) # Ev sahibi olarak oynadığı ve sıralamada geride olduğu maçlar query = """ WITH home_matches AS ( SELECT m.id, m.score_home, m.score_away, m.home_team_id, m.away_team_id FROM matches m WHERE m.home_team_id = %s AND m.mst_utc < %s AND m.score_home IS NOT NULL AND m.score_away IS NOT NULL ORDER BY m.mst_utc DESC LIMIT %s ) SELECT COUNT(*) as total, SUM(CASE WHEN score_home > score_away THEN 1 ELSE 0 END) as wins FROM home_matches """ cursor.execute(query, (home_team_id, before_date_ms, lookback_matches)) result = cursor.fetchone() if result and result['total'] > 0: win_rate = result['wins'] / result['total'] # Ev sahibi kazanma oranı yüksekse, upset potansiyeli de yüksek if win_rate > 0.5: rate = min((win_rate - 0.4) * 0.5, 0.3) reasons.append(f"📈 Güçlü ev sahibi performansı (%{int(win_rate*100)} kazanma)") return rate, reasons return 0.0, reasons except Exception as e: print(f"[UpsetEngine] Historical query error: {e}") return 0.0, reasons def calculate_upset_potential( self, home_team_name: str, home_team_id: str, away_team_name: str, league_name: str, home_position: int, away_position: int, match_date_ms: int, is_cup_match: bool = False, home_matches_last_14d: int = 2, away_matches_last_14d: int = 2, home_days_rest: int = 7, away_days_rest: int = 7, away_travel_km: float = 0, total_teams: int = 20 ) -> UpsetFactors: """ Tüm faktörleri birleştirerek upset potansiyelini hesapla. Returns: UpsetFactors: Tüm faktörler ve toplam skor """ factors = UpsetFactors() all_reasons = [] # 1. Atmosfer atm_score, atm_reasons = self.calculate_atmosphere_score( home_team_name, league_name, is_cup_match ) factors.atmosphere_score = atm_score all_reasons.extend(atm_reasons) # 2. Motivasyon mot_score, mot_reasons = self.calculate_motivation_score( home_position, away_position, total_teams=total_teams ) factors.motivation_score = mot_score all_reasons.extend(mot_reasons) # 3. Yorgunluk fat_score, fat_reasons = self.calculate_fatigue_score( home_matches_last_14d, away_matches_last_14d, home_days_rest, away_days_rest, away_travel_km ) factors.fatigue_score = fat_score all_reasons.extend(fat_reasons) # 4. Tarihsel (sadece DB varsa) hist_score, hist_reasons = self.get_historical_upset_rate( home_team_id, match_date_ms ) factors.historical_upset_rate = hist_score all_reasons.extend(hist_reasons) # Toplam skor (weighted average) factors.total_upset_potential = min( factors.atmosphere_score * 0.25 + factors.motivation_score * 0.35 + factors.fatigue_score * 0.25 + factors.historical_upset_rate * 0.15, 1.0 ) factors.reasoning = all_reasons return factors def get_features( self, home_team_name: str, home_team_id: str, away_team_name: str, league_name: str, home_position: int, away_position: int, match_date_ms: int, **kwargs ) -> Dict[str, float]: """ Model için feature dict döndür. Training ve inference'da kullanılır. """ factors = self.calculate_upset_potential( home_team_name=home_team_name, home_team_id=home_team_id, away_team_name=away_team_name, league_name=league_name, home_position=home_position, away_position=away_position, match_date_ms=match_date_ms, **kwargs ) return { "upset_atmosphere": factors.atmosphere_score, "upset_motivation": factors.motivation_score, "upset_fatigue": factors.fatigue_score, "upset_historical": factors.historical_upset_rate, "upset_potential": factors.total_upset_potential, } # Singleton instance _engine_instance = None def get_upset_engine() -> UpsetEngine: """Singleton pattern ile engine döndür""" global _engine_instance if _engine_instance is None: _engine_instance = UpsetEngine() return _engine_instance # Test if __name__ == "__main__": engine = get_upset_engine() # Galatasaray vs Liverpool örneği factors = engine.calculate_upset_potential( home_team_name="Galatasaray", home_team_id="test-gs-id", away_team_name="Liverpool", league_name="UEFA Champions League", home_position=12, away_position=1, match_date_ms=1700000000000, is_cup_match=False, away_matches_last_14d=5, home_matches_last_14d=2, away_days_rest=3, home_days_rest=7, away_travel_km=2800, total_teams=20 ) print("=" * 60) print("GALATASARAY vs LIVERPOOL - UPSET ANALİZİ") print("=" * 60) print(f"🏟️ Atmosfer Skoru: {factors.atmosphere_score:.2f}") print(f"💪 Motivasyon Skoru: {factors.motivation_score:.2f}") print(f"😓 Yorgunluk Skoru: {factors.fatigue_score:.2f}") print(f"📊 Tarihsel Skor: {factors.historical_upset_rate:.2f}") print(f"\n🎯 TOPLAM UPSET POTANSİYELİ: {factors.total_upset_potential:.2f}") print("\n📝 Sebepler:") for reason in factors.reasoning: print(f" {reason}")