""" Expert Recommendation Engine (Senior Level) ============================================ Evaluates ALL markets, classifies by risk, and ensures NO "empty" recommendations. Prioritizes user safety by clearly labeling risk levels. """ from dataclasses import dataclass, field from typing import List, Optional, Any, Dict from .base_calculator import BaseCalculator, CalculationContext from .match_result_calculator import MatchResultPrediction from .over_under_calculator import OverUnderPrediction from .risk_assessor import RiskAnalysis @dataclass class ExpertPick: market_type: str pick: str probability: float confidence: float odds: float edge: float # Expected value percentage # Risk Classification risk_level: str # SAFE, MEDIUM, RISKY, SURPRISE reasoning: str # Why this pick? (e.g., "High xG support", "Value detected") @dataclass class ExpertResult: main_pick: ExpertPick safe_alternative: Optional[ExpertPick] value_picks: List[ExpertPick] surprise_picks: List[ExpertPick] market_summary: Dict[str, float] # {market: probability} class ExpertRecommender(BaseCalculator): def calculate(self, ctx: CalculationContext, ms_res: MatchResultPrediction, ou_res: OverUnderPrediction, risk: RiskAnalysis) -> ExpertResult: odds_data = ctx.odds_data all_picks: List[ExpertPick] = [] # ─── 1. Helper to Evaluate Pick ─── def evaluate(market: str, pick: str, prob: float, odd_key: str): odd_val = float(odds_data.get(odd_key, 0)) # If odd is missing/low, estimate it via probability (Kelly-ish estimation) if odd_val <= 1.01: odd_val = round(1.0 / (prob + 0.05), 2) # Conservative estimation reasoning = "Derived (No market odd)" else: reasoning = "Market Confirmed" implied = 1.0 / odd_val edge = (prob - implied) * 100 # ─── Risk Classification ─── if prob >= 0.75 and odd_val <= 1.45: level = "SAFE" elif edge > 5.0: level = "VALUE" elif odd_val >= 2.50 and prob >= 0.35: level = "SURPRISE" else: level = "MEDIUM" all_picks.append(ExpertPick( market_type=market, pick=pick, probability=prob, confidence=prob * 100, odds=odd_val, edge=edge, risk_level=level, reasoning=reasoning )) # ─── 2. Evaluate All Major Markets ─── # MS evaluate("MS", ms_res.ms_pick, ms_res.ms_home_prob if ms_res.ms_pick == "1" else (ms_res.ms_away_prob if ms_res.ms_pick == "2" else ms_res.ms_draw_prob), f"ms_{ms_res.ms_pick.lower()}") # Double Chance evaluate("DC", ms_res.dc_pick, ms_res.dc_1x_prob if ms_res.dc_pick == "1X" else (ms_res.dc_x2_prob if ms_res.dc_pick == "X2" else ms_res.dc_12_prob), f"dc_{ms_res.dc_pick.lower()}") # OU25 evaluate("OU25", ou_res.ou25_pick, ou_res.over_25_prob if "Üst" in ou_res.ou25_pick else ou_res.under_25_prob, "ou25_o" if "Üst" in ou_res.ou25_pick else "ou25_u") # BTTS evaluate("BTTS", ou_res.btts_pick, ou_res.btts_yes_prob if "Var" in ou_res.btts_pick else ou_res.btts_no_prob, "btts_y" if "Var" in ou_res.btts_pick else "btts_n") # OU15 evaluate("OU15", ou_res.ou15_pick, ou_res.over_15_prob if "Üst" in ou_res.ou15_pick else ou_res.under_15_prob, "ou15_o" if "Üst" in ou_res.ou15_pick else "ou15_u") # ─── 3. Sort and Select ─── # Sort by a mix of Confidence and Edge all_picks.sort(key=lambda p: (p.probability * 0.6) + (max(0, p.edge/100) * 0.4), reverse=True) main = all_picks[0] # Find Safe Alternative (if main isn't Safe) safe_alt = next((p for p in all_picks if p.risk_level == "SAFE"), None) if safe_alt == main: safe_alt = None value_picks = [p for p in all_picks if p.risk_level == "VALUE" and p != main] surprise_picks = [p for p in all_picks if p.risk_level == "SURPRISE"] # Market Summary for UI market_summary = { "MS_Home": ms_res.ms_home_prob, "MS_Draw": ms_res.ms_draw_prob, "MS_Away": ms_res.ms_away_prob, "OU25_Over": ou_res.over_25_prob, "BTTS_Yes": ou_res.btts_yes_prob } return ExpertResult( main_pick=main, safe_alternative=safe_alt, value_picks=value_picks, surprise_picks=surprise_picks, market_summary=market_summary )