This commit is contained in:
@@ -0,0 +1,511 @@
|
||||
"""
|
||||
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}")
|
||||
Reference in New Issue
Block a user