gg
This commit is contained in:
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
Match Commentary Generator
|
||||
===========================
|
||||
Generates human-readable Turkish commentary from the analysis package.
|
||||
Reads all engine signals (model, odds band, betting brain, triple value)
|
||||
and produces a clear, actionable summary for end users.
|
||||
|
||||
No LLM required — fully template-based.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
|
||||
def generate_match_commentary(package: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Main entry point. Takes a full analysis package and returns a commentary dict.
|
||||
|
||||
Returns:
|
||||
{
|
||||
"action": "BET" | "WATCH" | "SKIP",
|
||||
"headline": "...",
|
||||
"summary": "...",
|
||||
"notes": ["...", "..."],
|
||||
"contradictions": ["...", "..."],
|
||||
"confidence_label": "YÜKSEK" | "ORTA" | "DÜŞÜK" | "ÇOK DÜŞÜK"
|
||||
}
|
||||
"""
|
||||
match_info = package.get("match_info") or {}
|
||||
home = match_info.get("home_team", "Ev Sahibi")
|
||||
away = match_info.get("away_team", "Deplasman")
|
||||
main_pick = package.get("main_pick") or {}
|
||||
betting_brain = package.get("betting_brain") or {}
|
||||
v27_engine = package.get("v27_engine") or {}
|
||||
market_board = package.get("market_board") or {}
|
||||
score_pred = package.get("score_prediction") or {}
|
||||
risk = package.get("risk") or {}
|
||||
data_quality = package.get("data_quality") or {}
|
||||
|
||||
# ── Determine action ──────────────────────────────────────────
|
||||
brain_decision = str(betting_brain.get("decision") or "NO_BET").upper()
|
||||
main_playable = bool(main_pick.get("playable"))
|
||||
main_vetoed = bool((main_pick.get("upper_brain") or {}).get("veto"))
|
||||
approved_count = int(betting_brain.get("approved_count", 0) or 0)
|
||||
|
||||
if main_playable and not main_vetoed and approved_count > 0:
|
||||
action = "BET"
|
||||
elif approved_count == 0 and brain_decision == "NO_BET":
|
||||
action = "SKIP"
|
||||
else:
|
||||
action = "WATCH"
|
||||
|
||||
# ── Headline ──────────────────────────────────────────────────
|
||||
headline = _build_headline(action, main_pick, home, away)
|
||||
|
||||
# ── Summary paragraph ─────────────────────────────────────────
|
||||
summary = _build_summary(
|
||||
action, main_pick, market_board, v27_engine,
|
||||
score_pred, risk, data_quality, home, away,
|
||||
)
|
||||
|
||||
# ── Quick notes ───────────────────────────────────────────────
|
||||
notes = _build_notes(market_board, v27_engine, score_pred, risk, home, away)
|
||||
|
||||
# ── Contradiction detection ───────────────────────────────────
|
||||
contradictions = _detect_contradictions(market_board, v27_engine, package)
|
||||
|
||||
# ── Overall confidence label ──────────────────────────────────
|
||||
confidence_label = _overall_confidence_label(main_pick, data_quality)
|
||||
|
||||
return {
|
||||
"action": action,
|
||||
"headline": headline,
|
||||
"summary": summary,
|
||||
"notes": notes[:6],
|
||||
"contradictions": contradictions[:4],
|
||||
"confidence_label": confidence_label,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Headline
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _build_headline(
|
||||
action: str,
|
||||
main_pick: Dict[str, Any],
|
||||
home: str,
|
||||
away: str,
|
||||
) -> str:
|
||||
if action == "BET":
|
||||
market = main_pick.get("market", "")
|
||||
pick = main_pick.get("pick", "")
|
||||
odds = main_pick.get("odds", 0.0)
|
||||
conf = main_pick.get("calibrated_confidence", main_pick.get("confidence", 0))
|
||||
market_tr = _market_to_turkish(market, pick)
|
||||
return f"🎯 {market_tr} önerisi — Oran: {odds}, Güven: %{conf:.0f}"
|
||||
|
||||
if action == "WATCH":
|
||||
return f"👀 {home} vs {away} — İzlemeye değer sinyaller var"
|
||||
|
||||
return f"⚠️ {home} vs {away} — Şu an net bir fırsat görülmüyor"
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Summary
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _build_summary(
|
||||
action: str,
|
||||
main_pick: Dict[str, Any],
|
||||
market_board: Dict[str, Any],
|
||||
v27_engine: Dict[str, Any],
|
||||
score_pred: Dict[str, Any],
|
||||
risk: Dict[str, Any],
|
||||
data_quality: Dict[str, Any],
|
||||
home: str,
|
||||
away: str,
|
||||
) -> str:
|
||||
parts: List[str] = []
|
||||
|
||||
# Who is the favourite?
|
||||
ms_board = market_board.get("MS") or {}
|
||||
ms_pick = ms_board.get("pick", "")
|
||||
ms_conf = float(ms_board.get("confidence", 50) or 50)
|
||||
|
||||
if ms_pick == "1" and ms_conf > 45:
|
||||
parts.append(f"{home} hafif favori görünüyor")
|
||||
elif ms_pick == "1" and ms_conf > 55:
|
||||
parts.append(f"{home} net favori")
|
||||
elif ms_pick == "2" and ms_conf > 45:
|
||||
parts.append(f"{away} hafif favori görünüyor")
|
||||
elif ms_pick == "2" and ms_conf > 55:
|
||||
parts.append(f"{away} net favori")
|
||||
else:
|
||||
parts.append("İki takım da birbirine yakın güçte")
|
||||
|
||||
# xG expectation
|
||||
xg_home = float(score_pred.get("xg_home", 0) or 0)
|
||||
xg_away = float(score_pred.get("xg_away", 0) or 0)
|
||||
xg_total = xg_home + xg_away
|
||||
if xg_total > 3.0:
|
||||
parts.append(f"Gol beklentisi yüksek (toplam xG: {xg_total:.1f})")
|
||||
elif xg_total < 2.0:
|
||||
parts.append(f"Düşük gol beklentisi (toplam xG: {xg_total:.1f})")
|
||||
|
||||
# Consensus check
|
||||
consensus = str(v27_engine.get("consensus") or "").upper()
|
||||
if consensus == "AGREE":
|
||||
parts.append("Model motorları aynı fikirde")
|
||||
elif consensus == "DISAGREE":
|
||||
parts.append("Model motorları farklı sonuçlara ulaşıyor — belirsizlik var")
|
||||
|
||||
# Action-specific
|
||||
if action == "BET":
|
||||
market_tr = _market_to_turkish(
|
||||
main_pick.get("market", ""), main_pick.get("pick", "")
|
||||
)
|
||||
edge = float(main_pick.get("ev_edge", 0) or 0)
|
||||
parts.append(
|
||||
f"{market_tr} yönünde değer tespit edildi (EV edge: {edge:+.1%})"
|
||||
)
|
||||
elif action == "SKIP":
|
||||
parts.append(
|
||||
"Hiçbir markette piyasanın fiyatlamadığı bir avantaj görülmüyor"
|
||||
)
|
||||
|
||||
# Risk
|
||||
risk_level = str(risk.get("level") or "MEDIUM").upper()
|
||||
if risk_level == "HIGH":
|
||||
parts.append("⚠️ Risk seviyesi yüksek")
|
||||
elif risk_level == "EXTREME":
|
||||
parts.append("🔴 Çok yüksek risk — dikkatli olun")
|
||||
|
||||
# Data quality
|
||||
quality_label = str(data_quality.get("label") or "MEDIUM").upper()
|
||||
if quality_label == "LOW":
|
||||
parts.append("Veri kalitesi düşük — tahminler daha az güvenilir")
|
||||
|
||||
return ". ".join(parts) + "."
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Quick Notes
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _build_notes(
|
||||
market_board: Dict[str, Any],
|
||||
v27_engine: Dict[str, Any],
|
||||
score_pred: Dict[str, Any],
|
||||
risk: Dict[str, Any],
|
||||
home: str,
|
||||
away: str,
|
||||
) -> List[str]:
|
||||
notes: List[str] = []
|
||||
triple_value = v27_engine.get("triple_value") or {}
|
||||
odds_band = v27_engine.get("odds_band") or {}
|
||||
|
||||
# MS note
|
||||
ms = market_board.get("MS") or {}
|
||||
ms_conf = float(ms.get("confidence", 0) or 0)
|
||||
if ms_conf < 45:
|
||||
notes.append("Maç sonucu belirsiz, net favori yok")
|
||||
elif ms.get("pick") == "1":
|
||||
notes.append(f"{home} favori ama oran değerli mi kontrol et")
|
||||
elif ms.get("pick") == "2":
|
||||
notes.append(f"{away} favori ama oran değerli mi kontrol et")
|
||||
|
||||
# OU25 note
|
||||
ou25 = market_board.get("OU25") or {}
|
||||
ou25_probs = ou25.get("probs") or {}
|
||||
over_prob = float(ou25_probs.get("over", 0.5) or 0.5)
|
||||
if over_prob > 0.58:
|
||||
notes.append("2.5 Üst yönünde eğilim var")
|
||||
elif over_prob < 0.42:
|
||||
notes.append("2.5 Alt yönünde eğilim var")
|
||||
else:
|
||||
notes.append("2.5 Üst/Alt dengeli — kesin sinyal yok")
|
||||
|
||||
# BTTS note
|
||||
btts = market_board.get("BTTS") or {}
|
||||
btts_probs = btts.get("probs") or {}
|
||||
btts_yes = float(btts_probs.get("yes", 0.5) or 0.5)
|
||||
if btts_yes > 0.58:
|
||||
notes.append("Her iki takımın da gol atması bekleniyor")
|
||||
elif btts_yes < 0.42:
|
||||
notes.append("KG olasılığı düşük")
|
||||
|
||||
# HT note
|
||||
ht = market_board.get("HT") or {}
|
||||
ht_pick = ht.get("pick", "")
|
||||
ht_conf = float(ht.get("confidence", 0) or 0)
|
||||
if ht_conf > 40 and ht_pick:
|
||||
ht_label = {"1": f"İY {home}", "2": f"İY {away}", "X": "İY beraberlik"}.get(
|
||||
ht_pick, f"İY {ht_pick}"
|
||||
)
|
||||
notes.append(f"{ht_label} yönünde hafif sinyal (%{ht_conf:.0f})")
|
||||
|
||||
# Risk warnings
|
||||
warnings = risk.get("warnings") or []
|
||||
for w in warnings[:2]:
|
||||
notes.append(f"⚠️ {w}")
|
||||
|
||||
return notes
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Contradiction Detection
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _detect_contradictions(
|
||||
market_board: Dict[str, Any],
|
||||
v27_engine: Dict[str, Any],
|
||||
package: Dict[str, Any],
|
||||
) -> List[str]:
|
||||
"""
|
||||
Detect cases where model prediction and odds band/triple value
|
||||
point in opposite directions — the user's main complaint.
|
||||
"""
|
||||
contradictions: List[str] = []
|
||||
triple_value = v27_engine.get("triple_value") or {}
|
||||
predictions = v27_engine.get("predictions") or {}
|
||||
|
||||
# MS contradiction: model says home but triple_value says away has value
|
||||
ms_preds = predictions.get("ms") or {}
|
||||
ms_home = float(ms_preds.get("home", 0) or 0)
|
||||
ms_away = float(ms_preds.get("away", 0) or 0)
|
||||
home_triple = triple_value.get("home") or {}
|
||||
away_triple = triple_value.get("away") or {}
|
||||
|
||||
model_favours_home = ms_home > ms_away
|
||||
away_is_value = bool(away_triple.get("is_value"))
|
||||
home_is_value = bool(home_triple.get("is_value"))
|
||||
|
||||
if model_favours_home and away_is_value:
|
||||
contradictions.append(
|
||||
"Model ev sahibini favori görüyor ama oran bandı deplasmanda değer buluyor — "
|
||||
"bu çelişki nedeniyle MS tahminine dikkatli yaklaş"
|
||||
)
|
||||
elif not model_favours_home and home_is_value:
|
||||
contradictions.append(
|
||||
"Model deplasmanı favori görüyor ama oran bandı ev sahibinde değer buluyor — "
|
||||
"bu çelişki nedeniyle MS tahminine dikkatli yaklaş"
|
||||
)
|
||||
|
||||
# HT contradiction
|
||||
ht_board = market_board.get("HT") or {}
|
||||
ht_pick = ht_board.get("pick", "")
|
||||
ht_home_triple = triple_value.get("ht_home") or {}
|
||||
ht_away_triple = triple_value.get("ht_away") or {}
|
||||
|
||||
if ht_pick == "1" and bool(ht_away_triple.get("is_value")):
|
||||
contradictions.append(
|
||||
"Model İY ev sahibi diyor ama oran bandı İY deplasmanda değer buluyor — "
|
||||
"İY tahmini güvenilir değil"
|
||||
)
|
||||
elif ht_pick == "2" and bool(ht_home_triple.get("is_value")):
|
||||
contradictions.append(
|
||||
"Model İY deplasman diyor ama oran bandı İY ev sahibinde değer buluyor — "
|
||||
"İY tahmini güvenilir değil"
|
||||
)
|
||||
|
||||
# OU25 contradiction
|
||||
ou25_board = market_board.get("OU25") or {}
|
||||
ou25_pick = ou25_board.get("pick", "")
|
||||
ou25_over_triple = triple_value.get("ou25_over") or {}
|
||||
ou25_under_triple = triple_value.get("ou25_under") or {}
|
||||
|
||||
if ou25_pick == "Üst" and bool(ou25_under_triple.get("is_value")):
|
||||
contradictions.append(
|
||||
"Model 2.5 Üst diyor ama oran bandı 2.5 Alt'ta değer buluyor — çelişki var"
|
||||
)
|
||||
elif ou25_pick == "Alt" and bool(ou25_over_triple.get("is_value")):
|
||||
contradictions.append(
|
||||
"Model 2.5 Alt diyor ama oran bandı 2.5 Üst'te değer buluyor — çelişki var"
|
||||
)
|
||||
|
||||
return contradictions
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# Helpers
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def _overall_confidence_label(
|
||||
main_pick: Dict[str, Any],
|
||||
data_quality: Dict[str, Any],
|
||||
) -> str:
|
||||
"""Overall confidence label for the entire analysis."""
|
||||
quality_score = float(data_quality.get("score", 0.5) or 0.5)
|
||||
main_conf = float(
|
||||
main_pick.get("calibrated_confidence", main_pick.get("confidence", 0)) or 0
|
||||
)
|
||||
main_playable = bool(main_pick.get("playable"))
|
||||
|
||||
if main_playable and main_conf >= 60 and quality_score >= 0.8:
|
||||
return "YÜKSEK"
|
||||
if main_playable and main_conf >= 45:
|
||||
return "ORTA"
|
||||
if main_conf >= 30:
|
||||
return "DÜŞÜK"
|
||||
return "ÇOK DÜŞÜK"
|
||||
|
||||
|
||||
_MARKET_TR_MAP = {
|
||||
"MS": {"1": "Maç Sonucu Ev Sahibi", "2": "Maç Sonucu Deplasman", "X": "Beraberlik"},
|
||||
"DC": {"1X": "Çifte Şans 1X", "X2": "Çifte Şans X2", "12": "Çifte Şans 12"},
|
||||
"OU25": {"Üst": "2.5 Üst", "Alt": "2.5 Alt", "Over": "2.5 Üst", "Under": "2.5 Alt"},
|
||||
"OU15": {"Üst": "1.5 Üst", "Alt": "1.5 Alt", "Over": "1.5 Üst", "Under": "1.5 Alt"},
|
||||
"OU35": {"Üst": "3.5 Üst", "Alt": "3.5 Alt", "Over": "3.5 Üst", "Under": "3.5 Alt"},
|
||||
"BTTS": {"KG Var": "Karşılıklı Gol Var", "KG Yok": "Karşılıklı Gol Yok",
|
||||
"Yes": "Karşılıklı Gol Var", "No": "Karşılıklı Gol Yok"},
|
||||
"HT": {"1": "İlk Yarı Ev Sahibi", "2": "İlk Yarı Deplasman", "X": "İlk Yarı Beraberlik"},
|
||||
"HT_OU05": {"Üst": "İY 0.5 Üst", "Alt": "İY 0.5 Alt"},
|
||||
"HT_OU15": {"Üst": "İY 1.5 Üst", "Alt": "İY 1.5 Alt"},
|
||||
"OE": {"Tek": "Tek", "Çift": "Çift", "Odd": "Tek", "Even": "Çift"},
|
||||
"CARDS": {"Üst": "Kart Üst", "Alt": "Kart Alt"},
|
||||
}
|
||||
|
||||
|
||||
def _market_to_turkish(market: str, pick: str) -> str:
|
||||
market_map = _MARKET_TR_MAP.get(market, {})
|
||||
result = market_map.get(pick)
|
||||
if result:
|
||||
return result
|
||||
return f"{market} {pick}"
|
||||
Reference in New Issue
Block a user