511 lines
19 KiB
Python
511 lines
19 KiB
Python
"""
|
||
Upset Engine v2 - GLM-5 Tespitleri ile Geliştirilmiş Sürpriz Tespiti
|
||
====================================================================
|
||
|
||
Yeni Eklenen Faktörler (GLM-5 Analizinden):
|
||
1. MARGIN_ANALIZI - Bookmaker margin > %18 = sürpriz riski
|
||
2. FAVORI_ORAN_TUZAGI - 1.40-1.60 arası en yüksek sürpriz oranı
|
||
3. HAKEM_SURPRIZ_ORANI - Hakemin geçmiş maçlarında ev kayıp oranı
|
||
4. FORM_FARKI_TUZAGI - Form farkı > 40 = "çok iyi görünen" favori tuzak
|
||
|
||
Orijinal Faktörler:
|
||
- Atmosfer (Avrupa gecesi, taraftar baskısı)
|
||
- Motivasyon asimetrisi (küme düşme vs şampiyon)
|
||
- Yorgunluk (maç yoğunluğu, seyahat)
|
||
- Tarihsel upset pattern
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
from typing import Dict, Any, Optional, Tuple, List
|
||
from dataclasses import dataclass, field
|
||
|
||
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 UpsetFactorsV2:
|
||
"""Upset potansiyelini etkileyen faktörler - v2"""
|
||
# Orijinal faktörler
|
||
atmosphere_score: float = 0.0
|
||
motivation_score: float = 0.0
|
||
fatigue_score: float = 0.0
|
||
historical_upset_rate: float = 0.0
|
||
|
||
# YENİ FAKTÖRLER (GLM-5)
|
||
margin_score: float = 0.0 # Bookmaker margin analizi
|
||
favorite_odds_trap: float = 0.0 # Favori oran tuzağı
|
||
referee_upset_score: float = 0.0 # Hakem sürpriz oranı
|
||
form_trap_score: float = 0.0 # Form farkı tuzağı
|
||
|
||
# Toplam
|
||
total_upset_potential: float = 0.0
|
||
reasoning: List[str] = field(default_factory=list)
|
||
|
||
# YENİ: Sürpriz skoru (0-100)
|
||
upset_score: int = 0
|
||
upset_level: str = "LOW" # LOW, MEDIUM, HIGH, EXTREME
|
||
|
||
|
||
class UpsetEngineV2:
|
||
"""
|
||
Favori takımın kaybedeceği maçları tespit eder.
|
||
v2: GLM-5 analizlerinden elde edilen yeni faktörler eklendi.
|
||
"""
|
||
|
||
# Yüksek atmosferli stadyumlar
|
||
HIGH_ATMOSPHERE_TEAMS = {
|
||
"galatasaray", "fenerbahce", "besiktas", "trabzonspor",
|
||
"liverpool", "newcastle", "leeds",
|
||
"dortmund", "union berlin",
|
||
"olympiacos", "panathinaikos", "aek athens",
|
||
"boca juniors", "river plate",
|
||
"celtic", "rangers", "red star belgrade"
|
||
}
|
||
|
||
EUROPEAN_COMPETITIONS = {
|
||
"şampiyonlar ligi", "champions league", "uefa champions league",
|
||
"avrupa ligi", "europa league", "uefa europa league",
|
||
"konferans ligi", "conference league", "uefa conference league"
|
||
}
|
||
|
||
# YENİ: Sürpriz oranları (veritabanı analizinden)
|
||
# Favori oran aralığına göre sürpriz oranları
|
||
FAVORITE_ODDS_UPSET_RATES = {
|
||
(1.10, 1.20): 0.111, # %11.1 sürpriz
|
||
(1.20, 1.30): 0.150, # %15.0 sürpriz
|
||
(1.30, 1.40): 0.235, # %23.5 sürpriz
|
||
(1.40, 1.50): 0.333, # %33.3 sürpriz ← DİKKAT!
|
||
(1.50, 1.60): 0.350, # %35.0 sürpriz ← EN YÜKSEK!
|
||
}
|
||
|
||
def __init__(self):
|
||
self.conn = None
|
||
self._connect_db()
|
||
|
||
def _connect_db(self):
|
||
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"[UpsetEngineV2] DB connection failed: {e}")
|
||
self.conn = None
|
||
|
||
def _get_conn(self):
|
||
if self.conn is None or self.conn.closed:
|
||
self._connect_db()
|
||
return self.conn
|
||
|
||
# ═════════════════════════════════════════════════════════════════
|
||
# YENİ FAKTÖRLER (GLM-5 Analizinden)
|
||
# ═════════════════════════════════════════════════════════════════
|
||
|
||
def calculate_margin_score(
|
||
self,
|
||
odds_data: Dict[str, float]
|
||
) -> Tuple[float, List[str]]:
|
||
"""
|
||
GLM-5 Tespiti: Bookmaker margin analizi
|
||
|
||
Margin > %18 → Bookmaker kendini koruyor, favori riskli
|
||
Margin > %20 → Yüksek risk, sürpriz bekleniyor
|
||
"""
|
||
score = 0.0
|
||
reasons = []
|
||
|
||
ms_h = odds_data.get("ms_h", 0)
|
||
ms_d = odds_data.get("ms_d", 0)
|
||
ms_a = odds_data.get("ms_a", 0)
|
||
|
||
if ms_h > 0 and ms_d > 0 and ms_a > 0:
|
||
margin = (1/ms_h + 1/ms_d + 1/ms_a) - 1
|
||
|
||
if margin > 0.20:
|
||
score = 0.25
|
||
reasons.append(f"⚠️ Margin çok yüksek (%{margin*100:.1f}) - Bookmaker risk görüyor!")
|
||
elif margin > 0.18:
|
||
score = 0.15
|
||
reasons.append(f"⚠️ Margin yüksek (%{margin*100:.1f}) - Dikkat!")
|
||
|
||
return score, reasons
|
||
|
||
def calculate_favorite_odds_trap(
|
||
self,
|
||
favorite_odds: float,
|
||
favorite_side: str # 'home' or 'away'
|
||
) -> Tuple[float, List[str]]:
|
||
"""
|
||
GLM-5 Tespiti: Favori oran tuzağı
|
||
|
||
Veritabanı analizine göre:
|
||
- 1.40-1.50 arası: %33.3 sürpriz
|
||
- 1.50-1.60 arası: %35.0 sürpriz (EN YÜKSEK!)
|
||
- < 1.20: Tuzak oranı şüphesi
|
||
"""
|
||
score = 0.0
|
||
reasons = []
|
||
|
||
if favorite_odds <= 0:
|
||
return score, reasons
|
||
|
||
for (low, high), upset_rate in self.FAVORITE_ODDS_UPSET_RATES.items():
|
||
if low <= favorite_odds < high:
|
||
score = upset_rate # Doğrudan sürpriz olasılığı
|
||
if upset_rate >= 0.30:
|
||
reasons.append(f"🔴 Favori oran {favorite_odds:.2f} - %{upset_rate*100:.0f} sürpriz oranı!")
|
||
elif upset_rate >= 0.20:
|
||
reasons.append(f"⚠️ Favori oran {favorite_odds:.2f} - %{upset_rate*100:.0f} sürpriz riski")
|
||
break
|
||
|
||
# Çok düşük oran tuzağı
|
||
if favorite_odds < 1.20:
|
||
score = max(score, 0.20)
|
||
reasons.append(f"⚠️ Favori oran çok düşük ({favorite_odds:.2f}) - Tuzak oranı şüphesi")
|
||
|
||
return score, reasons
|
||
|
||
def calculate_referee_upset_score(
|
||
self,
|
||
referee_name: str
|
||
) -> Tuple[float, List[str]]:
|
||
"""
|
||
GLM-5 Tespiti: Hakem sürpriz oranı
|
||
|
||
Hakemin yönettiği maçlarda ev sahibi kayıp oranı
|
||
> %25 → Yüksek sürpriz riski
|
||
"""
|
||
score = 0.0
|
||
reasons = []
|
||
|
||
if not referee_name or not self._get_conn():
|
||
return score, reasons
|
||
|
||
try:
|
||
cur = self._get_conn().cursor()
|
||
|
||
# Hakemin yönettiği maçlarda sonuçlar
|
||
cur.execute("""
|
||
SELECT
|
||
COUNT(*) as total,
|
||
SUM(CASE WHEN m.score_home < m.score_away THEN 1 ELSE 0 END) as away_wins,
|
||
SUM(CASE WHEN m.score_home = m.score_away THEN 1 ELSE 0 END) as draws
|
||
FROM match_officials mo
|
||
JOIN matches m ON m.id = mo.match_id
|
||
WHERE mo.name = %s AND mo.role_id = 1
|
||
AND m.score_home IS NOT NULL
|
||
""", (referee_name,))
|
||
|
||
row = cur.fetchone()
|
||
cur.close()
|
||
|
||
if row and row[0] and row[0] >= 3:
|
||
total = row[0]
|
||
away_wins = row[1] or 0
|
||
draws = row[2] or 0
|
||
|
||
upset_rate = (away_wins + draws * 0.5) / total
|
||
|
||
if upset_rate > 0.40:
|
||
score = 0.25
|
||
reasons.append(f"👨⚖️ {referee_name}: %{upset_rate*100:.0f} sürpriz oranı (YÜKSEK!)")
|
||
elif upset_rate > 0.30:
|
||
score = 0.15
|
||
reasons.append(f"👨⚖️ {referee_name}: %{upset_rate*100:.0f} sürpriz oranı")
|
||
|
||
except Exception as e:
|
||
pass
|
||
|
||
return score, reasons
|
||
|
||
def calculate_form_trap_score(
|
||
self,
|
||
home_form_score: float,
|
||
away_form_score: float,
|
||
favorite_side: str
|
||
) -> Tuple[float, List[str]]:
|
||
"""
|
||
GLM-5 Tespiti: Form farkı tuzağı
|
||
|
||
Form farkı > 40 → "Çok iyi görünen" favori tuzak
|
||
Favori formu kötü ama oran düşük → Sürpriz bekleniyor
|
||
"""
|
||
score = 0.0
|
||
reasons = []
|
||
|
||
form_diff = home_form_score - away_form_score
|
||
|
||
# Form farkı çok büyük
|
||
if abs(form_diff) > 40:
|
||
score = 0.20
|
||
if form_diff > 0 and favorite_side == 'away':
|
||
reasons.append(f"🔴 Form tuzağı! Ev sahibi formda ({home_form_score:.0f}) ama deplasman favori")
|
||
elif form_diff < 0 and favorite_side == 'home':
|
||
reasons.append(f"🔴 Form tuzağı! Deplasman formda ({away_form_score:.0f}) ama ev sahibi favori")
|
||
|
||
# Favori formu kötü
|
||
if favorite_side == 'home' and home_form_score < 50:
|
||
score = max(score, 0.15)
|
||
reasons.append(f"⚠️ Favori ev sahibi formu düşük ({home_form_score:.0f})")
|
||
elif favorite_side == 'away' and away_form_score < 50:
|
||
score = max(score, 0.15)
|
||
reasons.append(f"⚠️ Favori deplasman formu düşük ({away_form_score:.0f})")
|
||
|
||
return score, reasons
|
||
|
||
# ═════════════════════════════════════════════════════════════════
|
||
# ORİJİNAL FAKTÖRLER
|
||
# ═════════════════════════════════════════════════════════════════
|
||
|
||
def calculate_atmosphere_score(
|
||
self,
|
||
home_team_name: str,
|
||
league_name: str,
|
||
is_cup_match: bool = False
|
||
) -> Tuple[float, List[str]]:
|
||
"""Orijinal: Atmosfer skoru"""
|
||
score = 0.0
|
||
reasons = []
|
||
|
||
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
|
||
|
||
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
|
||
|
||
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,
|
||
total_teams: int = 20
|
||
) -> Tuple[float, List[str]]:
|
||
"""Orijinal: Motivasyon asimetrisi"""
|
||
score = 0.0
|
||
reasons = []
|
||
|
||
if home_position is not None and away_position is not None:
|
||
position_diff = away_position - home_position
|
||
relegation_zone = total_teams - 3
|
||
|
||
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")
|
||
|
||
if position_diff < -10:
|
||
score += 0.15
|
||
reasons.append(f"📊 {abs(position_diff)} sıra fark")
|
||
|
||
return min(score, 1.0), reasons
|
||
|
||
# ═════════════════════════════════════════════════════════════════
|
||
# ANA FONKSİYON
|
||
# ═════════════════════════════════════════════════════════════════
|
||
|
||
def calculate_upset_potential(
|
||
self,
|
||
home_team_name: str,
|
||
home_team_id: str,
|
||
away_team_name: str,
|
||
league_name: str,
|
||
home_position: int = None,
|
||
away_position: int = None,
|
||
match_date_ms: int = None,
|
||
odds_data: Dict[str, float] = None,
|
||
referee_name: str = None,
|
||
home_form_score: float = 50.0,
|
||
away_form_score: float = 50.0,
|
||
favorite_side: str = None, # 'home', 'away', or 'draw'
|
||
favorite_odds: float = None
|
||
) -> UpsetFactorsV2:
|
||
"""
|
||
Tam upset analizi - v2 (GLM-5 geliştirmeleri ile)
|
||
"""
|
||
factors = UpsetFactorsV2()
|
||
all_reasons = []
|
||
|
||
# 1. Margin analizi (YENİ)
|
||
if odds_data:
|
||
factors.margin_score, reasons = self.calculate_margin_score(odds_data)
|
||
all_reasons.extend(reasons)
|
||
|
||
# 2. Favori oran tuzağı (YENİ)
|
||
if favorite_odds and favorite_side:
|
||
factors.favorite_odds_trap, reasons = self.calculate_favorite_odds_trap(
|
||
favorite_odds, favorite_side
|
||
)
|
||
all_reasons.extend(reasons)
|
||
|
||
# 3. Hakem sürpriz oranı (YENİ)
|
||
if referee_name:
|
||
factors.referee_upset_score, reasons = self.calculate_referee_upset_score(
|
||
referee_name
|
||
)
|
||
all_reasons.extend(reasons)
|
||
|
||
# 4. Form tuzağı (YENİ)
|
||
factors.form_trap_score, reasons = self.calculate_form_trap_score(
|
||
home_form_score, away_form_score, favorite_side or 'home'
|
||
)
|
||
all_reasons.extend(reasons)
|
||
|
||
# 5. Atmosfer (orijinal)
|
||
factors.atmosphere_score, reasons = self.calculate_atmosphere_score(
|
||
home_team_name, league_name
|
||
)
|
||
all_reasons.extend(reasons)
|
||
|
||
# 6. Motivasyon (orijinal)
|
||
if home_position is not None and away_position is not None:
|
||
factors.motivation_score, reasons = self.calculate_motivation_score(
|
||
home_position, away_position
|
||
)
|
||
all_reasons.extend(reasons)
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# SÜRPRİZ SKORU HESAPLAMA (0-100) - GÜÇLENDİRİLMİŞ v2.1
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
upset_score = 0
|
||
|
||
# Margin (> %18 = +20, > %20 = +30) - GÜÇLENDİRİLDİ
|
||
if factors.margin_score >= 0.25:
|
||
upset_score += 30 # Artırıldı: 20 -> 30
|
||
all_reasons.append("🔴 Margin > %20: Bookmaker büyük risk görüyor!")
|
||
elif factors.margin_score >= 0.15:
|
||
upset_score += 20 # Artırıldı: 15 -> 20
|
||
all_reasons.append("⚠️ Margin > %18: Dikkatli ol!")
|
||
|
||
# Favori oran tuzağı - GÜÇLENDİRİLDİ
|
||
if factors.favorite_odds_trap >= 0.30:
|
||
upset_score += 30 # Artırıldı: 25 -> 30
|
||
elif factors.favorite_odds_trap >= 0.20:
|
||
upset_score += 25 # Artırıldı: 20 -> 25
|
||
elif factors.favorite_odds_trap >= 0.15:
|
||
upset_score += 20 # Artırıldı: 15 -> 20
|
||
|
||
# Hakem
|
||
if factors.referee_upset_score >= 0.25:
|
||
upset_score += 20
|
||
elif factors.referee_upset_score >= 0.15:
|
||
upset_score += 10
|
||
|
||
# Form tuzağı - GÜÇLENDİRİLDİ
|
||
if factors.form_trap_score >= 0.20:
|
||
upset_score += 20 # Artırıldı: 15 -> 20
|
||
elif factors.form_trap_score >= 0.15:
|
||
upset_score += 15 # Artırıldı: 10 -> 15
|
||
|
||
# Atmosfer - GÜÇLENDİRİLDİ
|
||
if factors.atmosphere_score >= 0.40:
|
||
upset_score += 20 # Artırıldı: 15 -> 20
|
||
elif factors.atmosphere_score >= 0.25:
|
||
upset_score += 15 # Artırıldı: 10 -> 15
|
||
|
||
# Motivasyon
|
||
if factors.motivation_score >= 0.30:
|
||
upset_score += 15
|
||
elif factors.motivation_score >= 0.15:
|
||
upset_score += 10
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# YENİ: EKSTRA RİSK FAKTÖRLERİ
|
||
# ═══════════════════════════════════════════════════════════
|
||
|
||
# Deplasman favorisi ekstra risk (+10)
|
||
if favorite_side == 'away':
|
||
upset_score += 10
|
||
all_reasons.append("📍 Deplasman favorisi - ekstra risk!")
|
||
|
||
# Favori formu çok düşük (< 40) = +15
|
||
if favorite_side == 'home' and home_form_score < 40:
|
||
upset_score += 15
|
||
all_reasons.append(f"🔴 Favori ev sahibi formu ÇOK DÜŞÜK ({home_form_score:.0f})")
|
||
elif favorite_side == 'away' and away_form_score < 40:
|
||
upset_score += 15
|
||
all_reasons.append(f"🔴 Favori deplasman formu ÇOK DÜŞÜK ({away_form_score:.0f})")
|
||
|
||
# Çok düşük favori oranı (< 1.30) ama margin yüksek = tuzak şüphesi
|
||
if favorite_odds and favorite_odds < 1.30 and factors.margin_score >= 0.15:
|
||
upset_score += 10
|
||
all_reasons.append(f"⚠️ Düşük oran ({favorite_odds:.2f}) + yüksek margin = TUZAK ŞÜPHESİ!")
|
||
|
||
factors.upset_score = min(upset_score, 100)
|
||
|
||
# Seviye belirle
|
||
if factors.upset_score >= 60:
|
||
factors.upset_level = "EXTREME"
|
||
elif factors.upset_score >= 45:
|
||
factors.upset_level = "HIGH"
|
||
elif factors.upset_score >= 30:
|
||
factors.upset_level = "MEDIUM"
|
||
else:
|
||
factors.upset_level = "LOW"
|
||
|
||
# Toplam upset potansiyeli
|
||
factors.total_upset_potential = min(
|
||
(factors.margin_score + factors.favorite_odds_trap +
|
||
factors.referee_upset_score + factors.form_trap_score +
|
||
factors.atmosphere_score * 0.5 + factors.motivation_score * 0.5) / 1.5,
|
||
1.0
|
||
)
|
||
|
||
factors.reasoning = all_reasons
|
||
|
||
return factors
|
||
|
||
|
||
def get_upset_engine_v2():
|
||
"""Singleton pattern"""
|
||
return UpsetEngineV2()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Test
|
||
engine = get_upset_engine_v2()
|
||
|
||
# Real Madrid vs Getafe test
|
||
result = engine.calculate_upset_potential(
|
||
home_team_name="Real Madrid",
|
||
home_team_id="test",
|
||
away_team_name="Getafe",
|
||
league_name="LaLiga",
|
||
odds_data={"ms_h": 1.25, "ms_d": 3.92, "ms_a": 6.86},
|
||
referee_name="A. Muniz Ruiz",
|
||
home_form_score=80.0,
|
||
away_form_score=56.7,
|
||
favorite_side="home",
|
||
favorite_odds=1.25
|
||
)
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"Real Madrid vs Getafe - Sürpriz Analizi")
|
||
print(f"{'='*60}")
|
||
print(f"Sürpriz Skoru: {result.upset_score}/100")
|
||
print(f"Seviye: {result.upset_level}")
|
||
print(f"\nNedenler:")
|
||
for reason in result.reasoning:
|
||
print(f" {reason}") |