main
Deploy Iddaai Backend / build-and-deploy (push) Failing after 2m6s

This commit is contained in:
2026-05-12 02:43:02 +03:00
parent f8599bdb9a
commit b6d64b59bf
35 changed files with 1400 additions and 630 deletions
@@ -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
+40 -29
View File
@@ -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)