This commit is contained in:
2026-05-10 10:37:45 +03:00
parent 4f7090e2d9
commit c525b12dfd
32 changed files with 2374 additions and 209 deletions
+367
View File
@@ -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}"