This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
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
|
||||
)
|
||||
Reference in New Issue
Block a user