""" 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}"