This commit is contained in:
@@ -40,7 +40,7 @@ class CalculationContext:
|
||||
is_surprise: bool = False
|
||||
|
||||
# XGBoost Predictions (New)
|
||||
xgboost_preds: dict[str, dict[str, Any]] = field(default_factory=dict)
|
||||
xgboost_preds: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class BaseCalculator:
|
||||
|
||||
@@ -28,7 +28,7 @@ class RecommendationResult:
|
||||
|
||||
|
||||
class BetRecommender(BaseCalculator):
|
||||
def calculate(self,
|
||||
def calculate(self, # type: ignore[override]
|
||||
ctx: CalculationContext,
|
||||
ms_res: MatchResultPrediction,
|
||||
ou_res: OverUnderPrediction,
|
||||
|
||||
@@ -36,7 +36,7 @@ class ExpertResult:
|
||||
|
||||
|
||||
class ExpertRecommender(BaseCalculator):
|
||||
def calculate(self,
|
||||
def calculate(self, # type: ignore[override]
|
||||
ctx: CalculationContext,
|
||||
ms_res: MatchResultPrediction,
|
||||
ou_res: OverUnderPrediction,
|
||||
|
||||
@@ -31,7 +31,7 @@ class HalfTimeCalculator(BaseCalculator):
|
||||
return 1.0 if k == 0 else 0.0
|
||||
return (lam ** k) * math.exp(-lam) / math.factorial(k)
|
||||
|
||||
def calculate(self, ctx: CalculationContext) -> HalfTimePrediction:
|
||||
def calculate(self, ctx: CalculationContext) -> HalfTimePrediction: # type: ignore[override]
|
||||
team_pred = ctx.team_pred
|
||||
odds_pred = ctx.odds_pred
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ class MatchResultCalculator(BaseCalculator):
|
||||
def _get_engine_winner(self, home_prob: float, draw_prob: float, away_prob: float) -> str:
|
||||
"""Determine which outcome an engine favors."""
|
||||
probs = {"1": home_prob, "X": draw_prob, "2": away_prob}
|
||||
return max(probs, key=probs.get)
|
||||
return max(probs, key=probs.__getitem__)
|
||||
|
||||
def calculate(self, ctx: CalculationContext) -> MatchResultPrediction:
|
||||
def calculate(self, ctx: CalculationContext) -> MatchResultPrediction: # type: ignore[override]
|
||||
# Weights
|
||||
w_team = ctx.weights["team"]
|
||||
w_player = ctx.weights["player"]
|
||||
|
||||
@@ -28,7 +28,7 @@ class OtherMarketsPrediction:
|
||||
|
||||
|
||||
class OtherMarketsCalculator(BaseCalculator):
|
||||
def calculate(
|
||||
def calculate( # type: ignore[override]
|
||||
self,
|
||||
ctx: CalculationContext,
|
||||
ms_result: MatchResultPrediction,
|
||||
|
||||
@@ -55,7 +55,7 @@ class OverUnderCalculator(BaseCalculator):
|
||||
|
||||
return over_15, over_25, over_35, btts_yes
|
||||
|
||||
def calculate(self, ctx: CalculationContext) -> OverUnderPrediction:
|
||||
def calculate(self, ctx: CalculationContext) -> OverUnderPrediction: # type: ignore[override]
|
||||
odds_pred = ctx.odds_pred
|
||||
referee_mods = ctx.referee_mods
|
||||
|
||||
|
||||
@@ -67,12 +67,14 @@ class RiskAssessor(BaseCalculator):
|
||||
|
||||
if sport_key == "basketball":
|
||||
if is_top_league:
|
||||
return float(
|
||||
self.config.get("risk.surprise_threshold_basketball_top", self.config.get("risk.surprise_threshold_basketball", 0.30)),
|
||||
)
|
||||
return float(
|
||||
self.config.get("risk.surprise_threshold_basketball_non_top", 0.34),
|
||||
)
|
||||
top_val = self.config.get("risk.surprise_threshold_basketball_top")
|
||||
if top_val is not None:
|
||||
return float(top_val)
|
||||
base_val = self.config.get("risk.surprise_threshold_basketball")
|
||||
return float(base_val) if base_val is not None else 0.30
|
||||
|
||||
non_top_val = self.config.get("risk.surprise_threshold_basketball_non_top")
|
||||
return float(non_top_val) if non_top_val is not None else 0.34
|
||||
|
||||
if top_label not in ("1/2", "2/1"):
|
||||
return base_threshold
|
||||
@@ -81,27 +83,30 @@ class RiskAssessor(BaseCalculator):
|
||||
favorite_side, gap = self._favorite_profile_from_odds(ctx.odds_data)
|
||||
|
||||
if is_top_league:
|
||||
favorite_winner_threshold = float(
|
||||
self.config.get(
|
||||
"risk.surprise_threshold_favorite_reversal_top",
|
||||
self.config.get("risk.surprise_threshold_favorite_reversal", 0.26),
|
||||
),
|
||||
)
|
||||
underdog_winner_threshold = float(
|
||||
self.config.get(
|
||||
"risk.surprise_threshold_underdog_reversal_top",
|
||||
self.config.get("risk.surprise_threshold_underdog_reversal", 0.20),
|
||||
),
|
||||
)
|
||||
top_fav = self.config.get("risk.surprise_threshold_favorite_reversal_top")
|
||||
if top_fav is not None:
|
||||
favorite_winner_threshold = float(top_fav)
|
||||
else:
|
||||
base_fav = self.config.get("risk.surprise_threshold_favorite_reversal")
|
||||
favorite_winner_threshold = float(base_fav) if base_fav is not None else 0.26
|
||||
|
||||
top_ud = self.config.get("risk.surprise_threshold_underdog_reversal_top")
|
||||
if top_ud is not None:
|
||||
underdog_winner_threshold = float(top_ud)
|
||||
else:
|
||||
base_ud = self.config.get("risk.surprise_threshold_underdog_reversal")
|
||||
underdog_winner_threshold = float(base_ud) if base_ud is not None else 0.20
|
||||
else:
|
||||
favorite_winner_threshold = float(
|
||||
self.config.get("risk.surprise_threshold_favorite_reversal_non_top", 0.30),
|
||||
)
|
||||
underdog_winner_threshold = float(
|
||||
self.config.get("risk.surprise_threshold_underdog_reversal_non_top", 0.24),
|
||||
)
|
||||
gap_medium = float(self.config.get("risk.htft_reversal_gap_medium", 0.50))
|
||||
gap_strong = float(self.config.get("risk.htft_reversal_gap_strong", 1.00))
|
||||
nt_fav = self.config.get("risk.surprise_threshold_favorite_reversal_non_top")
|
||||
favorite_winner_threshold = float(nt_fav) if nt_fav is not None else 0.30
|
||||
nt_ud = self.config.get("risk.surprise_threshold_underdog_reversal_non_top")
|
||||
underdog_winner_threshold = float(nt_ud) if nt_ud is not None else 0.24
|
||||
|
||||
gm = self.config.get("risk.htft_reversal_gap_medium")
|
||||
gap_medium = float(gm) if gm is not None else 0.50
|
||||
|
||||
gs = self.config.get("risk.htft_reversal_gap_strong")
|
||||
gap_strong = float(gs) if gs is not None else 1.00
|
||||
|
||||
if favorite_side in ("H", "A"):
|
||||
threshold = (
|
||||
@@ -117,7 +122,7 @@ class RiskAssessor(BaseCalculator):
|
||||
|
||||
return base_threshold
|
||||
|
||||
def calculate(self, ctx: CalculationContext, ms_result=None) -> RiskAnalysis:
|
||||
def calculate(self, ctx: CalculationContext, ms_result: Any = None) -> RiskAnalysis: # type: ignore[override]
|
||||
"""
|
||||
Wrapper for assess_risk to match BaseCalculator interface but with extra arg.
|
||||
"""
|
||||
@@ -173,9 +178,15 @@ class RiskAssessor(BaseCalculator):
|
||||
|
||||
threshold = self._dynamic_reversal_threshold(ctx, top_label)
|
||||
if getattr(ctx, "is_top_league", False):
|
||||
min_gap = float(self.config.get("risk.surprise_min_top_gap_top", self.config.get("risk.surprise_min_top_gap", 0.02)))
|
||||
top_gap_val = self.config.get("risk.surprise_min_top_gap_top")
|
||||
if top_gap_val is not None:
|
||||
min_gap = float(top_gap_val)
|
||||
else:
|
||||
base_gap_val = self.config.get("risk.surprise_min_top_gap")
|
||||
min_gap = float(base_gap_val) if base_gap_val is not None else 0.02
|
||||
else:
|
||||
min_gap = float(self.config.get("risk.surprise_min_top_gap_non_top", 0.03))
|
||||
non_top_gap_val = self.config.get("risk.surprise_min_top_gap_non_top")
|
||||
min_gap = float(non_top_gap_val) if non_top_gap_val is not None else 0.03
|
||||
|
||||
# Trigger surprise only when reversal class is:
|
||||
# - top HT/FT outcome
|
||||
|
||||
@@ -3,7 +3,7 @@ import pickle
|
||||
import pandas as pd
|
||||
import xgboost as xgb
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Tuple
|
||||
from typing import List, Dict, Tuple, Optional
|
||||
import math
|
||||
from .base_calculator import BaseCalculator, CalculationContext
|
||||
from .confidence import calc_confidence_3way, calc_confidence_dc
|
||||
@@ -16,7 +16,7 @@ class ScorePrediction:
|
||||
ft_scores_top5: List[Dict]
|
||||
|
||||
# Reconciled MS/DC predictions (can be updated here)
|
||||
reconciled_ms: MatchResultPrediction = None
|
||||
reconciled_ms: Optional[MatchResultPrediction] = None
|
||||
|
||||
class ScoreCalculator(BaseCalculator):
|
||||
|
||||
@@ -57,7 +57,8 @@ class ScoreCalculator(BaseCalculator):
|
||||
return 1.0 if k == 0 else 0.0
|
||||
return (lam ** k) * math.exp(-lam) / math.factorial(k)
|
||||
|
||||
def calculate(self, ctx: CalculationContext, ms_result: MatchResultPrediction) -> ScorePrediction:
|
||||
def calculate(self, ctx: CalculationContext, ms_result: MatchResultPrediction) -> ScorePrediction: # type: ignore[override]
|
||||
predicted_ht = None
|
||||
# Default Lambdas (fallback)
|
||||
lambda_home = max(0.5, ctx.home_xg)
|
||||
lambda_away = max(0.5, ctx.away_xg)
|
||||
@@ -199,7 +200,7 @@ class ScoreCalculator(BaseCalculator):
|
||||
predicted_ft = top_overall_score
|
||||
|
||||
# If we didn't calculate HT via ML (exception case), do it now
|
||||
if 'predicted_ht' not in locals():
|
||||
if predicted_ht is None:
|
||||
ft_to_ht = self.config.get("half_time.ft_to_ht_ratio", 0.42)
|
||||
ht_h = round(lambda_home * ft_to_ht)
|
||||
ht_a = round(lambda_away * ft_to_ht)
|
||||
|
||||
Reference in New Issue
Block a user