238 lines
7.8 KiB
Python
Executable File
238 lines
7.8 KiB
Python
Executable File
"""
|
|
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}")
|