132 lines
4.8 KiB
Python
132 lines
4.8 KiB
Python
"""
|
|
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
|
|
)
|