340de185 |
"""Tools for dealing with bookmaker's odds and implied probabilities"""
|
ec276364 |
from fractions import Fraction
|
340de185 |
from math import sqrt
|
ec276364 |
from typing import Optional, Sequence
|
340de185 |
import scipy.optimize
import toolz
|
ec276364 |
def overround(p: Sequence[float]) -> float:
"""Return the overround, given a sequence of implied probabilities."""
return sum(p) - 1
def from_american(odds: int) -> float:
"""Return implied probability, given American odds."""
odds = round(odds)
if abs(odds) < 100:
raise ValueError(f"Invalid American odds {odds}")
if odds > 0:
return 100 / (100 + odds)
else:
odds = -odds
return odds / (100 + odds)
def to_american(p: float, ndigits: Optional[int] = None) -> int:
"""Return American odds, given an implied probability.
Parameters
----------
p
Implied probability
ndigits
The number of digits to which to round the result. The default
is to round to the nearest integer (American odds are not usually
quoted to more decimal places), but this may be adjusted if
more precision is needed (and the result will be a floating
point number).
"""
if p < 0.5:
a = 100 / p - 100
else:
a = -(100 * p) / (1 - p)
return round(a, ndigits)
def from_euro(odds: float) -> float:
"""Return implied probability, given Euro/Decimal odds."""
return 1 / odds
def to_euro(p: float) -> float:
"""Return Euro/Decimal odds, given an implied probability."""
return 1 / p
def from_british(odds: Fraction) -> float:
"""Return implied probability, given British odds."""
return float(1 / (odds + 1))
def to_british(p: float, largest_denominator: int = 100) -> Fraction:
"""Return British odds, given an implied probability.
Parameters
----------
p
Implied probability
largest_denominator
The largest denominator to use when representing the result.
The smaller this number the more accuracy is lost.
"""
return Fraction(1 / p - 1).limit_denominator(largest_denominator)
from_decimal = from_euro
to_decimal = to_euro
|
340de185 |
# -- Adjusting implied probabilities to true probabilities
_adjust_methods = {}
@toolz.curry
def add_method(name: str, f):
_adjust_methods[name] = f
return f
def validate_odds(odds: Sequence[float]) -> None:
if len(odds) < 2:
raise ValueError(f"Expected at least 2 outcomes, but got {len(odds)}")
if any(x >= 1 for x in odds):
raise ValueError(f"Expected all outcomes to have an implied probability < 1")
r = overround(odds)
if r < 0:
raise ValueError(f"Odds {odds} have a negative overround ({r})")
elif r > 0.5:
raise ValueError(f"Odds {odds} have excessive overround ({r})")
def adjust_odds(odds: Sequence[float], method="power") -> list[float]:
|
ec276364 |
"""Return fair odds given bookmaker odds.
|
340de185 |
Parameters
----------
odds
The odds of each outcome, given as implied probabilites.
The implied probabilities associated with the odds quoted by a bookmaker
sum to a number > 1. This excess is the "overround" of the odds. There
|
ec276364 |
are a number of models for distributing the overround among the outcomes
to recover the "fair odds" of each outcome.
|
340de185 |
Notes
-----
All implemented methods are based on the descriptions in the paper by
S. Clarke in the references section.
References
----------
S. Clarke, S. Kovalchik, M. Ingram
Adjusting Bookmaker's odds to Allow for Overround.
"""
try:
f = _adjust_methods[method]
except KeyError:
raise ValueError(
f"Unknown method '{method}', expected one of {list(_adjust_methods)}"
|
ec276364 |
) from None
|
340de185 |
validate_odds(odds)
return f(odds)
@add_method("additive")
def adjust_odds_additive(odds: Sequence[float]) -> list[float]:
"""Adjust bookmaker odds using the additive method.
In this method the overround is assumed to be split equality between
the different outcomes, regardless of their relative implied probabilities.
"""
y = overround(odds) / len(odds)
return [x - y for x in odds]
@add_method("multiplicative")
def adjust_odds_multiplicative(odds: Sequence[float]) -> list[float]:
"""Adjust bookmaker odds using the multiplicative method.
In this method the overround is assumed to be split among the
different outcomes in proportion to their implied probabilities.
"""
s = sum(odds)
r = overround(odds)
return [x - (r * x / s) for x in odds]
@add_method("power")
def adjust_odds_power(odds: Sequence[float]) -> list[float]:
"""Adjust bookmaker odds using the power method.
In this method the true probabilities are assumed to be
related to the implied probabilities by a power law,
i.e. p_i = π_i^k, for some fixed k. The k is then
chosen such that the true probabilities sum to 1.
Notes
-----
The 'k' parameter for the power method is computed by
solving the nonlinear equation Σ_i π_i^k = 1 using
a root-finding algorithm.
References
----------
S.R. Clarke
Adjusting true odds to allow for vigorish
"""
# This bracketing interval should be sufficient for reasonable overrounds
# (not > 50%) and reasonable distribution of implied probability between
# the different outcomes.
try:
k = scipy.optimize.root_scalar(
lambda k: sum(x ** k for x in odds) - 1,
bracket=[0.9, 3],
xtol=1e-14,
).root
except ValueError as e:
if "different signs" in str(e):
raise ValueError(f"Bracketing interval too small for odds {odds}") from None
return [x ** k for x in odds]
@add_method("shin")
def adjust_odds_shin(odds):
"""Adjust bookmaker odds using the Shin method.
In this method we assume a fraction z of sharp bettors
and adjust the true probabilities accordingly.
Notes
-----
The 'z' parameter for the Shin method is computed
by solving a nonlinear equation using a root-finding algorithm.
References
----------
H.S. Shin
Prices of State Contingent Claims with Insider Traders
and the Favorite-Longshot Bias
"""
s = sum(odds)
n = len(odds)
def residual(z):
return (
sum(sqrt(z ** 2 + 4 * (1 - z) * pi ** 2 / s) for pi in odds)
- 2
- (n - 2) * z
)
# 'z' is in the interval [0, 1). We set the upper limit for the
# bracketing interval to just below 1 to exclude a (spurious) solution at z=1.
try:
z = scipy.optimize.root_scalar(residual, bracket=[0, 0.999], xtol=1e-14).root
except ValueError:
raise ValueError(f"Shin method failed to converge for odds {odds}") from None
return [
(sqrt(z ** 2 + 4 * (1 - z) * pi ** 2 / s) - z) / (2 * (1 - z)) for pi in odds
]
|