420 lines
14 KiB
Python
Executable File
420 lines
14 KiB
Python
Executable File
"""
|
||
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}")
|