Files
fahricansecer 94c7a4481a
Deploy Iddaai Backend / build-and-deploy (push) Successful in 37s
main
2026-05-17 02:17:22 +03:00

507 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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
"""
from typing import Dict, Any, Optional, Tuple, List
from dataclasses import dataclass, field
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}")