amarorn / models /ev_value.py
beAnalytic's picture
feat: sync main with feature/superbet-live-inplay
16c19b8 verified
Raw
History Blame Contribute Delete
2.78 kB
from dataclasses import dataclass
from config import settings
from models.economics import coase_effective_min_edge
@dataclass
class OutcomeValue:
outcome: str
model_prob: float
odd: float
implied_prob: float
fair_odd: float
expected_value: float
kelly_full: float
kelly_quarter: float
@dataclass
class MatchValueReport:
home_team: str
away_team: str
outcomes: list[OutcomeValue]
best: OutcomeValue | None
def _kelly_fraction(prob: float, odd: float) -> float:
if odd <= 1.0:
return 0.0
numerator = (prob * odd) - 1.0
denominator = odd - 1.0
if denominator <= 0:
return 0.0
return max(0.0, numerator / denominator)
def _sanitize_prob(prob: float) -> float:
return max(0.0, min(1.0, prob))
def evaluate_outcome(outcome: str, prob: float, odd: float, *, house_prob: float | None = None) -> OutcomeValue:
"""Avalia EV de uma aposta.
Se `house_prob` for fornecido (probabilidade real da casa, ex: generosity_prob
sem margem), usa ela no lugar de `1/odd` para calcular a prob implícita.
EV = P_modelo / P_casa * (1 - margem_casa) — quando `house_prob` está presente,
a margem já foi removida, então EV = (P_modelo * odd_cru) - 1 ainda funciona
corretamente, mas o `implied_prob` fica mais preciso para referência e o
filtro de "odd suspeita" ganha uma baseline real.
"""
p = _sanitize_prob(prob)
if house_prob is not None and house_prob > 0:
implied = _sanitize_prob(house_prob)
else:
implied = 1.0 / odd if odd > 0 else 1.0
fair_odd = (1.0 / p) if p > 0 else 999.0
ev = (p * odd) - 1.0 if odd > 0 else -1.0
kelly = _kelly_fraction(p, odd)
return OutcomeValue(
outcome=outcome,
model_prob=p,
odd=odd,
implied_prob=implied,
fair_odd=fair_odd,
expected_value=ev,
kelly_full=kelly,
kelly_quarter=kelly * 0.25,
)
def evaluate_match(
home_team: str,
away_team: str,
probabilities: dict[str, float],
odds: dict[str, float],
min_edge: float | None = None,
) -> MatchValueReport:
threshold = coase_effective_min_edge(min_edge or settings.ev_min_edge)
outcomes: list[OutcomeValue] = []
for key in ("1", "X", "2"):
odd = float(odds.get(key, 0.0))
prob = float(probabilities.get(key, 0.0))
if odd <= 1.0:
continue
outcomes.append(evaluate_outcome(key, prob, odd))
outcomes.sort(key=lambda item: item.expected_value, reverse=True)
best = outcomes[0] if outcomes and outcomes[0].expected_value >= threshold else None
return MatchValueReport(
home_team=home_team,
away_team=away_team,
outcomes=outcomes,
best=best,
)