""" Value Detection Engine ====================== The Smart Way to Beat the Bookmakers This engine doesn't just predict winners - it finds VALUE. The key insight: We don't need to predict the winner, we need to find where the bookmaker made a mistake in their odds. Core Philosophy: - High Margin = High Uncertainty = Potential Value - Model Probability > Implied Probability = Value Bet - The goal is NOT to predict correctly, but to find +EV bets Author: AI Engine V21 """ import math from dataclasses import dataclass from typing import Dict, List, Optional, Tuple from collections import defaultdict @dataclass class ValueBet: """Represents a value bet opportunity""" outcome: str # "1", "X", "2" model_probability: float # Our model's probability (0-1) implied_probability: float # Bookmaker's implied probability (0-1) odds: float # Bookmaker's odds edge: float # model_prob - implied_prob (as percentage) expected_value: float # EV = (prob * odds) - 1 kelly_fraction: float # Optimal bet size confidence: str # "HIGH", "MEDIUM", "LOW" reasons: List[str] # Why this is value def to_dict(self) -> dict: return { "outcome": self.outcome, "model_prob": round(self.model_probability * 100, 1), "implied_prob": round(self.implied_probability * 100, 1), "odds": self.odds, "edge": round(self.edge * 100, 1), "ev": round(self.expected_value * 100, 1), "kelly": round(self.kelly_fraction * 100, 1), "confidence": self.confidence, "reasons": self.reasons } @dataclass class MarginAnalysis: """Analysis of bookmaker margin""" raw_margin: float # Sum of raw implied probabilities - 1 true_margin: float # Adjusted for favorite-longshot bias favorite_outcome: str favorite_odds: float uncertainty_level: str # "LOW", "MEDIUM", "HIGH", "EXTREME" def to_dict(self) -> dict: return { "raw_margin": round(self.raw_margin * 100, 1), "true_margin": round(self.true_margin * 100, 1), "favorite": self.favorite_outcome, "favorite_odds": self.favorite_odds, "uncertainty": self.uncertainty_level } class ValueDetectionEngine: """ The Smart Betting Engine This engine finds value bets by comparing model probabilities with bookmaker implied probabilities. Key Insights: 1. Margin > 18% → Bookmaker is unsure, potential value on underdog 2. Margin > 20% → Bookmaker sees high risk, BIG potential value 3. Favorite odds 1.40-1.60 → Highest upset rate historically 4. Away favorites have higher upset rate than home favorites """ # Historical upset rates by favorite odds range UPSET_RATES = { (1.00, 1.25): 0.08, # 8% upset rate (1.25, 1.40): 0.18, # 18% upset rate (1.40, 1.60): 0.33, # 33% upset rate - DANGER ZONE (1.60, 1.80): 0.28, # 28% upset rate (1.80, 2.00): 0.35, # 35% upset rate (2.00, 2.50): 0.42, # 42% upset rate (2.50, 3.00): 0.45, # 45% upset rate (3.00, 5.00): 0.55, # 55% upset rate } # Margin thresholds MARGIN_LOW = 0.06 # 6% - bookmaker very confident MARGIN_MEDIUM = 0.12 # 12% - normal margin MARGIN_HIGH = 0.18 # 18% - bookmaker unsure MARGIN_EXTREME = 0.22 # 22% - bookmaker very unsure def __init__(self): self.historical_data = [] # For learning self.value_threshold = 0.03 # Minimum 3% edge to consider value def calculate_margin(self, odds_1: float, odds_x: float, odds_2: float) -> MarginAnalysis: """ Calculate bookmaker margin and analyze uncertainty. Higher margin = More uncertainty = More potential value """ if not all([odds_1 > 1, odds_x > 1, odds_2 > 1]): return MarginAnalysis(0, 0, "X", 0, "UNKNOWN") # Raw implied probabilities imp_1 = 1 / odds_1 imp_x = 1 / odds_x imp_2 = 1 / odds_2 raw_margin = imp_1 + imp_x + imp_2 - 1 # Determine favorite if odds_1 <= odds_x and odds_1 <= odds_2: favorite_outcome = "1" favorite_odds = odds_1 elif odds_2 <= odds_1 and odds_2 <= odds_x: favorite_outcome = "2" favorite_odds = odds_2 else: favorite_outcome = "X" favorite_odds = odds_x # Adjust for favorite-longshot bias # Bookmakers typically overprice longshots true_margin = raw_margin * 0.85 # Simplified adjustment # Determine uncertainty level if raw_margin < self.MARGIN_LOW: uncertainty = "LOW" elif raw_margin < self.MARGIN_MEDIUM: uncertainty = "MEDIUM" elif raw_margin < self.MARGIN_HIGH: uncertainty = "HIGH" else: uncertainty = "EXTREME" return MarginAnalysis( raw_margin=raw_margin, true_margin=true_margin, favorite_outcome=favorite_outcome, favorite_odds=favorite_odds, uncertainty_level=uncertainty ) def get_historical_upset_rate(self, favorite_odds: float) -> float: """Get historical upset rate for given favorite odds""" for (low, high), rate in self.UPSET_RATES.items(): if low <= favorite_odds < high: return rate return 0.40 # Default for very high odds def calculate_edge( self, model_prob: float, odds: float, margin: float ) -> Tuple[float, float]: """ Calculate the edge (advantage) we have over the bookmaker. Returns: (edge, expected_value) Edge = Model Probability - True Implied Probability EV = (Probability * Odds) - 1 """ if odds <= 1: return 0, -1 # Raw implied probability implied = 1 / odds # Adjust for margin (proportional adjustment) # This gives us the "true" implied probability # Assuming bookmaker spreads margin proportionally true_implied = implied # Simplified - could be more sophisticated edge = model_prob - true_implied ev = (model_prob * odds) - 1 return edge, ev def calculate_kelly_fraction( self, probability: float, odds: float, half_kelly: bool = True ) -> float: """ Calculate optimal bet size using Kelly Criterion. Kelly = (p * b - 1) / (b - 1) where b = odds - 1 We use half Kelly for safety. """ if odds <= 1: return 0 b = odds - 1 kelly = (probability * b - 1) / b # Don't bet if negative if kelly < 0: return 0 # Use half Kelly for safety if half_kelly: kelly = kelly / 2 # Cap at 10% of bankroll return min(kelly, 0.10) def find_value_bets( self, model_probs: Dict[str, float], odds: Dict[str, float], match_context: Optional[Dict] = None ) -> List[ValueBet]: """ Find all value bets in a match. This is the MAIN method - it finds where we have an edge. Args: model_probs: {"1": 0.55, "X": 0.25, "2": 0.20} odds: {"1": 1.25, "X": 4.50, "2": 8.00} match_context: Additional context (form, h2h, etc.) Returns: List of ValueBet objects, sorted by edge """ value_bets = [] # Calculate margin margin_analysis = self.calculate_margin( odds.get("1", 0), odds.get("X", 0), odds.get("2", 0) ) # Analyze each outcome for outcome in ["1", "X", "2"]: prob = model_probs.get(outcome, 0) odd = odds.get(outcome, 0) if prob <= 0 or odd <= 1: continue edge, ev = self.calculate_edge(prob, odd, margin_analysis.raw_margin) kelly = self.calculate_kelly_fraction(prob, odd) # Determine if this is a value bet reasons = [] # 1. Basic edge if edge > self.value_threshold: reasons.append(f"Edge: +{round(edge*100, 1)}% over bookmaker") # 2. High margin bonus if margin_analysis.raw_margin > self.MARGIN_HIGH: reasons.append(f"High margin ({round(margin_analysis.raw_margin*100, 1)}%) = uncertainty") # Boost edge for underdogs in high margin matches if outcome != margin_analysis.favorite_outcome: edge += 0.02 # 2% bonus reasons.append("Underdog in high-margin match = bonus value") # 3. Favorite odds trap fav_odds = margin_analysis.favorite_odds if margin_analysis.favorite_outcome != outcome: upset_rate = self.get_historical_upset_rate(fav_odds) if upset_rate > 0.25: reasons.append(f"Favorite odds {fav_odds} has {round(upset_rate*100)}% upset rate") # Extra bonus for 1.40-1.60 range if 1.40 <= fav_odds <= 1.60: edge += 0.03 reasons.append("DANGER ZONE: 1.40-1.60 odds = highest upset risk") # 4. Away favorite risk if margin_analysis.favorite_outcome == "2" and outcome == "1": edge += 0.015 reasons.append("Away favorite = extra home value") # 5. EV positive if ev > 0: reasons.append(f"Positive EV: +{round(ev*100, 1)}%") # Only add if we have reasons (value detected) if reasons and edge > 0: # Determine confidence if edge > 0.08 or (edge > 0.05 and kelly > 0.03): confidence = "HIGH" elif edge > 0.05: confidence = "MEDIUM" else: confidence = "LOW" value_bets.append(ValueBet( outcome=outcome, model_probability=prob, implied_probability=1/odd, odds=odd, edge=edge, expected_value=ev, kelly_fraction=kelly, confidence=confidence, reasons=reasons )) # Sort by edge (highest first) value_bets.sort(key=lambda x: x.edge, reverse=True) return value_bets def predict_with_value( self, model_probs: Dict[str, float], odds: Dict[str, float], match_context: Optional[Dict] = None ) -> Dict: """ Make a prediction based on VALUE, not just probability. This is the smart way to bet: - If there's clear value on one outcome → Bet it - If there's no value → NO BET (don't force it) - If margin is extreme → Look for underdog value Returns: { "best_value": ValueBet or None, "alternative_value": ValueBet or None, "margin_analysis": MarginAnalysis, "recommendation": str, "confidence": str } """ margin_analysis = self.calculate_margin( odds.get("1", 0), odds.get("X", 0), odds.get("2", 0) ) value_bets = self.find_value_bets(model_probs, odds, match_context) result = { "margin_analysis": margin_analysis.to_dict(), "value_bets": [vb.to_dict() for vb in value_bets], "best_value": None, "alternative_value": None, "recommendation": "NO_BET", "confidence": "LOW", "reasoning": [] } if not value_bets: result["reasoning"].append("No value detected in any outcome") result["reasoning"].append("Bookmaker odds are efficient for this match") return result # Get best value bet best = value_bets[0] result["best_value"] = best.to_dict() if len(value_bets) > 1: result["alternative_value"] = value_bets[1].to_dict() # Determine recommendation if best.confidence == "HIGH" and best.edge > 0.05: result["recommendation"] = f"BET_{best.outcome}" result["confidence"] = "HIGH" result["reasoning"] = best.reasons result["reasoning"].append(f"Strong value on {best.outcome} with {round(best.edge*100, 1)}% edge") elif best.confidence == "MEDIUM" or best.edge > 0.03: result["recommendation"] = f"CONSIDER_{best.outcome}" result["confidence"] = "MEDIUM" result["reasoning"] = best.reasons result["reasoning"].append(f"Moderate value on {best.outcome}") else: result["recommendation"] = "NO_BET" result["confidence"] = "LOW" result["reasoning"].append("Edge too small to justify bet") result["reasoning"].append(f"Best edge: {round(best.edge*100, 1)}% (need >3%)") # Add margin context if margin_analysis.uncertainty_level == "EXTREME": result["reasoning"].append("⚠️ EXTREME margin - high volatility match") elif margin_analysis.uncertainty_level == "HIGH": result["reasoning"].append("⚠️ High margin - bookmaker sees risk") return result # Singleton instance _engine_instance = None def get_value_detection_engine() -> ValueDetectionEngine: """Get the singleton instance""" global _engine_instance if _engine_instance is None: _engine_instance = ValueDetectionEngine() return _engine_instance