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
+63 -26
View File
@@ -91,22 +91,26 @@ class Calibrator:
def __init__(self):
self.calibrators: Dict[str, IsotonicRegression] = {}
self.metrics: Dict[str, CalibrationMetrics] = {}
# Less aggressive shrinkage — only meaningful overconfident bands are pulled.
# Default raised from ~0.85-0.90 to 0.95+ since the orchestrator and config
# already apply market-level multipliers; double-shrinkage was the root cause
# of 24-35pt avg calibrated-vs-raw drops in production traces.
self.heuristic_fallback: Dict[str, float] = {
"ms": 0.90,
"ms_home": 0.90,
"ms_home_heavy_fav": 0.95,
"ms_home_fav": 0.90,
"ms_home_balanced": 0.85,
"ms_home_underdog": 0.80,
"ms_draw": 0.90,
"ms_away": 0.90,
"ou15": 0.90,
"ou25": 0.90,
"ou35": 0.90,
"btts": 0.90,
"ht_ft": 0.85,
"dc": 0.93,
"ht": 0.85,
"ms": 0.96,
"ms_home": 0.96,
"ms_home_heavy_fav": 0.98,
"ms_home_fav": 0.96,
"ms_home_balanced": 0.94,
"ms_home_underdog": 0.92,
"ms_draw": 0.94,
"ms_away": 0.96,
"ou15": 0.96,
"ou25": 0.96,
"ou35": 0.94,
"btts": 0.96,
"ht_ft": 0.92,
"dc": 0.97,
"ht": 0.92,
}
self._load_calibrators()
@@ -139,21 +143,32 @@ class Calibrator:
except Exception as e:
print(f"[Calibrator] Warning: Failed to load metrics for {market}: {e}")
# Below this sample count, blend isotonic with raw_prob to dampen overfit jumps.
# Above this count, trust isotonic fully.
TRUSTED_SAMPLE_FLOOR = 30
TRUSTED_SAMPLE_CEILING = 200
# Hard cap on how far calibration can move probability in either direction.
MAX_DELTA = 0.20
def calibrate(self, market_type: str, raw_prob: float, odds_val: Optional[float] = None) -> float:
"""
Calibrate a raw probability using Isotonic Regression.
Calibrate a raw probability using Isotonic Regression with safeguards.
Args:
market_type (str): 'ms_home', 'ou25', 'btts', 'ht_ft', etc.
raw_prob (float): The raw probability from XGBoost (0.0 - 1.0)
odds_val (float, optional): The pre-match odds, used for context-aware bucket mapping
Returns:
float: Calibrated probability (0.0 - 1.0)
Safeguards:
* Low-sample trained models are blended with raw_prob to dampen overfit.
* MAX_DELTA caps the per-call adjustment (prevents 40pp swings).
"""
# Normalize market type
market_key = market_type.lower().replace("-", "_")
# Route to bucket if ms_home and odds provided
if market_key == "ms_home" and odds_val is not None and odds_val > 1.0:
if odds_val <= 1.40:
@@ -164,20 +179,42 @@ class Calibrator:
bucket_key = "ms_home_balanced"
else:
bucket_key = "ms_home_underdog"
if bucket_key in self.calibrators:
market_key = bucket_key
# If we have a trained Isotonic Regression model, use it
# If we have a trained Isotonic Regression model, use it (with safeguards)
if market_key in self.calibrators:
try:
calibrated = self.calibrators[market_key].predict([raw_prob])[0]
# Ensure output is valid probability
return float(np.clip(calibrated, 0.01, 0.99))
iso_pred = float(self.calibrators[market_key].predict([raw_prob])[0])
# Sample-count weighted blend with raw probability.
# Sparse models barely move probability; mature models dominate.
metrics = self.metrics.get(market_key)
n_samples = metrics.sample_count if metrics else 0
if n_samples >= self.TRUSTED_SAMPLE_CEILING:
iso_weight = 1.0
elif n_samples <= self.TRUSTED_SAMPLE_FLOOR:
# Very sparse: at least 30% trust to surface the signal
iso_weight = max(0.30, n_samples / self.TRUSTED_SAMPLE_CEILING)
else:
# Linearly ramp 30% → 100% between floor and ceiling
span = self.TRUSTED_SAMPLE_CEILING - self.TRUSTED_SAMPLE_FLOOR
iso_weight = 0.30 + 0.70 * (n_samples - self.TRUSTED_SAMPLE_FLOOR) / span
blended = iso_weight * iso_pred + (1.0 - iso_weight) * raw_prob
# Cap delta to avoid huge swings on noisy calibrators
delta = blended - raw_prob
if delta > self.MAX_DELTA:
blended = raw_prob + self.MAX_DELTA
elif delta < -self.MAX_DELTA:
blended = raw_prob - self.MAX_DELTA
return float(np.clip(blended, 0.01, 0.99))
except Exception as e:
print(f"[Calibrator] Warning: Isotonic failed for {market_key}: {e}")
# Fall through to heuristic
# Fallback to heuristic calibration
return self._heuristic_calibrate(market_key, raw_prob)
+33 -26
View File
@@ -139,7 +139,7 @@ class FullMatchPrediction:
ht_confidence: float = 0.0
# === SKOR TAHMİNLERİ ===
score: ScorePrediction = None
score: Optional[ScorePrediction] = None
predicted_ft_score: str = "1-1"
predicted_ht_score: str = "0-0"
ft_scores_top5: List[Dict] = field(default_factory=list)
@@ -161,7 +161,13 @@ class FullMatchPrediction:
upset_score: int = 0 # 0-100 arası sürpriz skoru
upset_level: str = "LOW" # LOW, MEDIUM, HIGH, EXTREME
upset_reasons: List[str] = field(default_factory=list)
# === SÜRPRİZ PROFİLİ ===
surprise_score: float = 0.0 # 0-100 overall surprise risk score
surprise_comment: str = "" # Human-readable surprise commentary
surprise_reasons: List[str] = field(default_factory=list) # Flagged risk reasons
surprise_breakdown: List[Dict[str, Any]] = field(default_factory=list) # Per-factor {code, points, label}
# === ENGINE KATKILARI ===
team_confidence: float = 0.0
player_confidence: float = 0.0
@@ -412,18 +418,19 @@ class V20EnsemblePredictor:
# Calculators
print("⚙️ Loading market calculators...")
self.match_result_calc = MatchResultCalculator(self.config)
self.over_under_calc = OverUnderCalculator(self.config)
self.half_time_calc = HalfTimeCalculator(self.config)
self.score_calc = ScoreCalculator(self.config)
cfg: Any = self.config
self.match_result_calc = MatchResultCalculator(cfg)
self.over_under_calc = OverUnderCalculator(cfg)
self.half_time_calc = HalfTimeCalculator(cfg)
self.score_calc = ScoreCalculator(cfg)
print(" ✅ Score Calculator (XGBoost FT+HT) loaded")
self.other_markets_calc = OtherMarketsCalculator(self.config)
self.risk_assessor = RiskAssessor(self.config)
self.bet_recommender = BetRecommender(self.config)
self.other_markets_calc = OtherMarketsCalculator(cfg)
self.risk_assessor = RiskAssessor(cfg)
self.bet_recommender = BetRecommender(cfg)
# Expert Recommender (New Logic)
from core.calculators.expert_recommender import ExpertRecommender
self.expert_recommender = ExpertRecommender(self.config)
self.expert_recommender = ExpertRecommender(cfg)
# XGBoost Integration
print("🤖 Loading XGBoost models...")
@@ -551,7 +558,7 @@ class V20EnsemblePredictor:
features = features.copy()
features[col] = 0.0
return features[expected]
return features[expected] # type: ignore[return-value]
def _favorite_profile_from_odds(self, odds_data: Dict[str, float]) -> Tuple[str, float]:
"""
@@ -838,10 +845,10 @@ class V20EnsemblePredictor:
home_team_name: str,
away_team_name: str,
match_date_ms: int,
odds_data: Dict[str, float] = None,
home_lineup: List[str] = None,
away_lineup: List[str] = None,
referee_name: str = None,
odds_data: Optional[Dict[str, float]] = None,
home_lineup: Optional[List[str]] = None,
away_lineup: Optional[List[str]] = None,
referee_name: Optional[str] = None,
home_goals_avg: float = 1.5,
home_conceded_avg: float = 1.2,
away_goals_avg: float = 1.2,
@@ -849,9 +856,9 @@ class V20EnsemblePredictor:
home_position: int = 10,
away_position: int = 10,
league_name: str = "",
league_id: str = None,
league_id: Optional[str] = None,
sport: str = "football",
sidelined_data: Dict = None) -> FullMatchPrediction:
sidelined_data: Optional[Dict] = None) -> FullMatchPrediction:
"""
Generate complete V20 ensemble prediction.
@@ -895,8 +902,8 @@ class V20EnsemblePredictor:
referee_pred = self.referee_engine.predict(
match_id=match_id,
referee_name=referee_name,
league_id=league_id
referee_name=referee_name or "",
league_id=league_id or ""
)
upset_factors = self.upset_engine.calculate_upset_potential(
@@ -935,9 +942,9 @@ class V20EnsemblePredictor:
away_position=away_position,
match_date_ms=match_date_ms,
odds_data=odds_data,
referee_name=referee_name,
home_form_score=team_pred.home_form_score if hasattr(team_pred, 'home_form_score') else 50.0,
away_form_score=team_pred.away_form_score if hasattr(team_pred, 'away_form_score') else 50.0,
referee_name=referee_name or "",
home_form_score=getattr(team_pred, 'home_form_score', 50.0),
away_form_score=getattr(team_pred, 'away_form_score', 50.0),
favorite_side=favorite_side,
favorite_odds=favorite_odds
)
@@ -1105,7 +1112,7 @@ class V20EnsemblePredictor:
best_bet = _map_dto(rec_result.best_bet)
alt_bet = _map_dto(rec_result.alternative_bet)
recommended = [_map_dto(r) for r in rec_result.recommended_bets]
recommended = [m for m in (_map_dto(r) for r in rec_result.recommended_bets) if m is not None]
# Analysis Details
analysis_details = {
@@ -1187,13 +1194,13 @@ class V20EnsemblePredictor:
# Others
total_corners_pred=other_result.total_corners_pred,
corner_pick=other_result.corner_pick,
corner_pick=other_result.corner_pick or "",
total_cards_pred=other_result.total_cards_pred,
card_pick=other_result.card_pick,
card_pick=other_result.card_pick or "",
cards_over_prob=other_result.cards_over_prob,
cards_under_prob=other_result.cards_under_prob,
cards_confidence=other_result.cards_confidence,
handicap_pick=other_result.handicap_pick,
handicap_pick=other_result.handicap_pick or "",
handicap_home_prob=other_result.handicap_home_prob,
handicap_draw_prob=other_result.handicap_draw_prob,
handicap_away_prob=other_result.handicap_away_prob,
+7 -8
View File
@@ -228,15 +228,13 @@ class V25Predictor:
print(f"[V25] Using fallback feature columns ({len(V25Predictor._FALLBACK_FEATURE_COLS)} features)")
return V25Predictor._FALLBACK_FEATURE_COLS
FEATURE_COLS = _load_feature_cols.__func__()
# Model weights for ensemble
DEFAULT_WEIGHTS = {
'xgb': 0.50,
'lgb': 0.50,
}
def __init__(self, models_dir: str = None):
def __init__(self, models_dir: Optional[str] = None):
"""
Initialize V25 Predictor.
@@ -246,6 +244,7 @@ class V25Predictor:
self.models_dir = models_dir or MODELS_DIR
self.models = {} # market -> {'xgb': model, 'lgb': model}
self._loaded = False
self.FEATURE_COLS = self._load_feature_cols()
# All trained market models available in V25
ALL_MARKETS = [
@@ -412,7 +411,7 @@ class V25Predictor:
return float(avg_prob), float(1 - avg_prob)
def predict_market(self, market: str, features: Dict[str, float]) -> np.ndarray:
def predict_market(self, market: str, features: Dict[str, float]) -> Optional[np.ndarray]:
"""
Generic prediction for any loaded market.
@@ -510,15 +509,15 @@ class V25Predictor:
# Determine picks
ms_probs = {'1': home_prob, 'X': draw_prob, '2': away_prob}
ms_pick = max(ms_probs, key=ms_probs.get)
ms_pick = max(ms_probs, key=ms_probs.__getitem__)
ms_confidence = ms_probs[ms_pick] * 100
ou25_probs = {'Over': over_prob, 'Under': under_prob}
ou25_pick = max(ou25_probs, key=ou25_probs.get)
ou25_pick = max(ou25_probs, key=ou25_probs.__getitem__)
ou25_confidence = ou25_probs[ou25_pick] * 100
btts_probs = {'Yes': btts_yes_prob, 'No': btts_no_prob}
btts_pick = max(btts_probs, key=btts_probs.get)
btts_pick = max(btts_probs, key=btts_probs.__getitem__)
btts_confidence = btts_probs[btts_pick] * 100
# Create prediction