Files
iddaai-be/ai-engine/core/calculators/bet_recommender.py
T
fahricansecer b6d64b59bf
Deploy Iddaai Backend / build-and-deploy (push) Failing after 2m6s
main
2026-05-12 02:43:02 +03:00

211 lines
8.9 KiB
Python
Executable File

from dataclasses import dataclass, field
from typing import List, Optional, Any
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 MarketPredictionDTO:
market_type: str
pick: str
probability: float
confidence: float
odds: float = 0.0
is_recommended: bool = False
is_value_bet: bool = False
edge: float = 0.0
is_skip: bool = False # NEW: If model is unsure, mark as skip
@dataclass
class RecommendationResult:
best_bet: Optional[MarketPredictionDTO]
recommended_bets: List[MarketPredictionDTO]
alternative_bet: Optional[MarketPredictionDTO]
value_bets: List[MarketPredictionDTO]
skipped_bets: List[MarketPredictionDTO] # NEW: Track what we decided NOT to predict
class BetRecommender(BaseCalculator):
def calculate(self, # type: ignore[override]
ctx: CalculationContext,
ms_res: MatchResultPrediction,
ou_res: OverUnderPrediction,
risk: RiskAnalysis) -> RecommendationResult:
odds_data = ctx.odds_data
# Market-Specific Minimum Confidence Thresholds (Hard Gates)
# Below these, we say "I don't know" (SKIP)
min_conf_thresholds = {
"MS": 45.0, # 3-way is hard, need at least 45%
"ÇŞ": 40.0, # Double chance is safer, but still need 40%
"1.5 Üst/Alt": 50.0,
"2.5 Üst/Alt": 45.0,
"3.5 Üst/Alt": 45.0,
"BTTS": 45.0,
"HT": 40.0,
}
# Prepare candidates
markets = [
MarketPredictionDTO("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),
ms_res.ms_confidence,
odds_data.get(f"ms_{ms_res.ms_pick.lower()}", 0)),
MarketPredictionDTO("ÇŞ", 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),
ms_res.dc_confidence,
odds_data.get(f"dc_{ms_res.dc_pick.lower()}", 0)),
MarketPredictionDTO("1.5 Üst/Alt", ou_res.ou15_pick,
ou_res.over_15_prob if "Üst" in ou_res.ou15_pick else ou_res.under_15_prob,
ou_res.ou15_confidence, 0),
MarketPredictionDTO("2.5 Üst/Alt", ou_res.ou25_pick,
ou_res.over_25_prob if "Üst" in ou_res.ou25_pick else ou_res.under_25_prob,
ou_res.ou25_confidence,
odds_data.get("ou25_o" if "Üst" in ou_res.ou25_pick else "ou25_u", 0)),
MarketPredictionDTO("3.5 Üst/Alt", ou_res.ou35_pick,
ou_res.over_35_prob if "Üst" in ou_res.ou35_pick else ou_res.under_35_prob,
ou_res.ou35_confidence, 0),
MarketPredictionDTO("BTTS", ou_res.btts_pick,
ou_res.btts_yes_prob if "Var" in ou_res.btts_pick else ou_res.btts_no_prob,
ou_res.btts_confidence,
odds_data.get("btts_y" if "Var" in ou_res.btts_pick else "btts_n", 0)),
]
# Market weights from config (historical accuracy weighting)
market_weights = self.config.get("recommendations.market_weights", {})
default_weight = 1.0
safe_markets = set(self.config.get("recommendations.safe_markets", ["ÇŞ", "1.5 Üst/Alt"]))
risk_level = risk.risk_level
# Confidence calibration (backtest-derived accuracy scaling)
market_accuracy = self.config.get("recommendations.market_accuracy", {})
baseline_accuracy = self.config.get("recommendations.baseline_accuracy", 65.0)
def _calibrated_confidence(m):
"""Scale raw confidence by market's historical accuracy ratio."""
accuracy = market_accuracy.get(m.market_type, baseline_accuracy) if isinstance(market_accuracy, dict) else baseline_accuracy
ratio = accuracy / baseline_accuracy
return m.confidence * ratio
def _score(m):
mw = market_weights.get(m.market_type, default_weight) if isinstance(market_weights, dict) else default_weight
# 1. Base Score: calibrated confidence * market weight
cal_conf = _calibrated_confidence(m)
score = cal_conf * mw
# 2. Value/Edge Bonus
odds_val = m.odds if m.odds is not None else 0.0
if odds_val > 0:
implied = 1.0 / odds_val
edge = (m.probability - implied) * 100
if edge > 0:
score += edge * 4.0
# 3. Risk adjustment
if risk_level in ("HIGH", "EXTREME"):
if m.market_type in safe_markets:
score *= self.config.get("recommendations.risk_safe_boost", 1.2)
elif m.market_type == "MS":
score *= self.config.get("recommendations.risk_ms_penalty_high", 0.5)
else:
score *= self.config.get("recommendations.risk_other_penalty", 0.7)
elif risk_level == "MEDIUM":
if m.market_type == "MS":
score *= self.config.get("recommendations.risk_ms_penalty_medium", 0.8)
# 4. Extreme Confidence Bonus
if cal_conf > 80:
score *= 1.15
return score
recommended = []
value_bets = []
skipped_bets = []
conf_thr = self.config.get("recommendations.confidence_threshold", 60)
val_min = self.config.get("recommendations.value_confidence_min", 45) # Increased from 30
val_max = self.config.get("recommendations.value_confidence_max", 60)
val_margin = self.config.get("recommendations.value_edge_margin", 0.03) # Increased from 0.02
val_upgrade = self.config.get("recommendations.value_upgrade_edge", 5.0)
for m in markets:
# --- SKIP LOGIC (Hard Gate) ---
# 1. Confidence is below market threshold
min_conf = min_conf_thresholds.get(m.market_type, 45.0)
if m.confidence < min_conf:
m.is_skip = True
skipped_bets.append(m)
continue
# 2. Negative Value Edge (Odds are too low for our probability)
if m.odds > 0:
implied = 1.0 / m.odds
edge = m.probability - implied
# If our prob is significantly lower than implied (negative edge > 3%), SKIP
if edge < -0.03:
m.is_skip = True
skipped_bets.append(m)
continue
# --- PROCESS BET ---
# 1. Regular recommended
if m.confidence >= conf_thr:
m.is_recommended = True
recommended.append(m)
# 2. Value bet logic
if m.confidence is not None and val_min <= m.confidence <= val_max and m.odds > 0:
implied = 1.0 / m.odds
if m.probability > (implied + val_margin):
m.is_value_bet = True
m.edge = (m.probability - implied) * 100
if m.edge > val_upgrade:
m.is_recommended = True
recommended.append(m)
else:
value_bets.append(m)
# Best bet (from recommended only)
best_bet = None
if recommended:
# Re-sort only recommended markets to find the best one
valid_markets = [m for m in markets if not m.is_skip and m.is_recommended]
if valid_markets:
valid_markets.sort(key=_score, reverse=True)
best_bet = valid_markets[0]
best_bet.is_recommended = True
# Alternative bet
alternative = None
if risk.is_surprise_risk and ms_res.ms_pick in ["1", "2"]:
# Check if alternative is not skipped
alt_candidate = MarketPredictionDTO(
"2.5 Üst/Alt", ou_res.ou25_pick,
ou_res.over_25_prob if "Üst" in ou_res.ou25_pick else ou_res.under_25_prob,
ou_res.ou25_confidence,
odds_data.get("ou25_o" if "Üst" in ou_res.ou25_pick else "ou25_u", 0)
)
if alt_candidate.confidence >= min_conf_thresholds.get("2.5 Üst/Alt", 45.0):
alternative = alt_candidate
return RecommendationResult(
best_bet=best_bet,
recommended_bets=recommended,
alternative_bet=alternative,
value_bets=value_bets,
skipped_bets=skipped_bets
)