""" Odds Predictor Engine - V20 Ensemble Component Uses market odds and Poisson mathematics for predictions. Weight: 30% in ensemble """ import os import sys from typing import Dict, Optional from dataclasses import dataclass sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) from features.poisson_engine import get_poisson_engine from features.value_calculator import get_value_calculator @dataclass class OddsPrediction: """Odds engine prediction output.""" # Market-implied probabilities market_home_prob: float = 0.33 market_draw_prob: float = 0.33 market_away_prob: float = 0.33 # Poisson xG poisson_home_xg: float = 1.3 poisson_away_xg: float = 1.1 # Over/Under probabilities over_15_prob: float = 0.75 over_25_prob: float = 0.55 over_35_prob: float = 0.30 # BTTS btts_yes_prob: float = 0.50 # Most likely scores most_likely_score: str = "1-1" second_likely_score: str = "1-0" third_likely_score: str = "2-1" # Value bet opportunities value_bets: list = None confidence: float = 0.0 def __post_init__(self): if self.value_bets is None: self.value_bets = [] def to_dict(self) -> dict: return { "market_home_prob": round(self.market_home_prob * 100, 1), "market_draw_prob": round(self.market_draw_prob * 100, 1), "market_away_prob": round(self.market_away_prob * 100, 1), "poisson_home_xg": round(self.poisson_home_xg, 2), "poisson_away_xg": round(self.poisson_away_xg, 2), "over_15_prob": round(self.over_15_prob * 100, 1), "over_25_prob": round(self.over_25_prob * 100, 1), "over_35_prob": round(self.over_35_prob * 100, 1), "btts_yes_prob": round(self.btts_yes_prob * 100, 1), "most_likely_score": self.most_likely_score, "second_likely_score": self.second_likely_score, "third_likely_score": self.third_likely_score, "value_bets": self.value_bets, "confidence": round(self.confidence, 1) } class OddsPredictorEngine: """ Odds-based prediction engine. Uses: - Market odds to extract implied probabilities - Poisson distribution for mathematical xG - Value calculator for EV+ opportunities """ def __init__(self): self.poisson_engine = get_poisson_engine() try: self.value_calc = get_value_calculator() except Exception: self.value_calc = None self.default_ms_h = 2.65 self.default_ms_d = 3.20 self.default_ms_a = 2.65 print("✅ OddsPredictorEngine initialized") def _odds_to_prob(self, odds: float) -> float: """Convert decimal odds to probability.""" try: odds = float(odds) except (TypeError, ValueError): return 0.0 if odds <= 1.0: return 0.0 return 1.0 / odds def predict(self, odds_data: Dict[str, float], home_goals_avg: float = 1.5, home_conceded_avg: float = 1.2, away_goals_avg: float = 1.2, away_conceded_avg: float = 1.4) -> OddsPrediction: """ Generate odds-based prediction. Args: odds_data: Dict with keys like 'ms_h', 'ms_d', 'ms_a', 'ou25_o', 'btts_y' home_goals_avg: Home team's average goals scored home_conceded_avg: Home team's average goals conceded away_goals_avg: Away team's average goals scored away_conceded_avg: Away team's average goals conceded Returns: OddsPrediction with market and Poisson analysis """ # 1. Extract market probabilities from odds ms_h = odds_data.get("ms_h", self.default_ms_h) ms_d = odds_data.get("ms_d", self.default_ms_d) ms_a = odds_data.get("ms_a", self.default_ms_a) # Remove vig to get fair probabilities raw_probs = [ self._odds_to_prob(ms_h), self._odds_to_prob(ms_d), self._odds_to_prob(ms_a) ] total = sum(raw_probs) or 1 market_home = raw_probs[0] / total market_draw = raw_probs[1] / total market_away = raw_probs[2] / total # 2. Poisson prediction poisson_pred = self.poisson_engine.predict( home_goals_avg, home_conceded_avg, away_goals_avg, away_conceded_avg ) # 3. Get most likely scores likely_scores = poisson_pred.most_likely_scores[:3] if poisson_pred.most_likely_scores else [] score_1 = likely_scores[0]["score"] if len(likely_scores) > 0 else "1-1" score_2 = likely_scores[1]["score"] if len(likely_scores) > 1 else "1-0" score_3 = likely_scores[2]["score"] if len(likely_scores) > 2 else "2-1" # 4. Value bet detection value_bets = [] # Check if our Poisson model disagrees with market significantly if abs(poisson_pred.home_win_prob - market_home) > 0.10: if poisson_pred.home_win_prob > market_home: value_bets.append({ "market": "MS 1", "edge": round((poisson_pred.home_win_prob - market_home) * 100, 1), "confidence": "medium" }) else: value_bets.append({ "market": "MS 2", "edge": round((poisson_pred.away_win_prob - market_away) * 100, 1), "confidence": "medium" }) # O/U value check ou25_o = odds_data.get("ou25_o", 1.9) market_over25 = self._odds_to_prob(ou25_o) if abs(poisson_pred.over_25_prob - market_over25) > 0.08: pick = "2.5 Üst" if poisson_pred.over_25_prob > market_over25 else "2.5 Alt" edge = abs(poisson_pred.over_25_prob - market_over25) * 100 value_bets.append({ "market": pick, "edge": round(edge, 1), "confidence": "high" if edge > 10 else "medium" }) # Calculate confidence # Higher when market and Poisson agree agreement = 1.0 - abs(poisson_pred.home_win_prob - market_home) confidence = 50.0 + (agreement * 40) + (len(value_bets) * 5) return OddsPrediction( market_home_prob=market_home, market_draw_prob=market_draw, market_away_prob=market_away, poisson_home_xg=poisson_pred.home_xg, poisson_away_xg=poisson_pred.away_xg, over_15_prob=poisson_pred.over_15_prob, over_25_prob=poisson_pred.over_25_prob, over_35_prob=poisson_pred.over_35_prob, btts_yes_prob=poisson_pred.btts_yes_prob, most_likely_score=score_1, second_likely_score=score_2, third_likely_score=score_3, value_bets=value_bets, confidence=min(99.9, confidence) ) # Singleton _engine: Optional[OddsPredictorEngine] = None def get_odds_predictor() -> OddsPredictorEngine: global _engine if _engine is None: _engine = OddsPredictorEngine() return _engine if __name__ == "__main__": engine = get_odds_predictor() print("\n🧪 Odds Predictor Engine Test") print("=" * 50) pred = engine.predict( odds_data={ "ms_h": 1.85, "ms_d": 3.40, "ms_a": 4.20, "ou25_o": 1.90 }, home_goals_avg=1.8, home_conceded_avg=1.0, away_goals_avg=1.2, away_conceded_avg=1.5 ) print(f"\n📊 Prediction:") for k, v in pred.to_dict().items(): print(f" {k}: {v}")