... | ... |
@@ -1,12 +1,84 @@ |
1 | 1 |
"""Tools for dealing with bookmaker's odds and implied probabilities""" |
2 | 2 |
|
3 |
+from fractions import Fraction |
|
3 | 4 |
from math import sqrt |
4 |
-from typing import Sequence |
|
5 |
+from typing import Optional, Sequence |
|
5 | 6 |
|
6 | 7 |
import scipy.optimize |
7 | 8 |
import toolz |
8 | 9 |
|
9 | 10 |
|
11 |
+def overround(p: Sequence[float]) -> float: |
|
12 |
+ """Return the overround, given a sequence of implied probabilities.""" |
|
13 |
+ return sum(p) - 1 |
|
14 |
+ |
|
15 |
+ |
|
16 |
+def from_american(odds: int) -> float: |
|
17 |
+ """Return implied probability, given American odds.""" |
|
18 |
+ odds = round(odds) |
|
19 |
+ if abs(odds) < 100: |
|
20 |
+ raise ValueError(f"Invalid American odds {odds}") |
|
21 |
+ if odds > 0: |
|
22 |
+ return 100 / (100 + odds) |
|
23 |
+ else: |
|
24 |
+ odds = -odds |
|
25 |
+ return odds / (100 + odds) |
|
26 |
+ |
|
27 |
+ |
|
28 |
+def to_american(p: float, ndigits: Optional[int] = None) -> int: |
|
29 |
+ """Return American odds, given an implied probability. |
|
30 |
+ |
|
31 |
+ Parameters |
|
32 |
+ ---------- |
|
33 |
+ p |
|
34 |
+ Implied probability |
|
35 |
+ ndigits |
|
36 |
+ The number of digits to which to round the result. The default |
|
37 |
+ is to round to the nearest integer (American odds are not usually |
|
38 |
+ quoted to more decimal places), but this may be adjusted if |
|
39 |
+ more precision is needed (and the result will be a floating |
|
40 |
+ point number). |
|
41 |
+ """ |
|
42 |
+ if p < 0.5: |
|
43 |
+ a = 100 / p - 100 |
|
44 |
+ else: |
|
45 |
+ a = -(100 * p) / (1 - p) |
|
46 |
+ return round(a, ndigits) |
|
47 |
+ |
|
48 |
+ |
|
49 |
+def from_euro(odds: float) -> float: |
|
50 |
+ """Return implied probability, given Euro/Decimal odds.""" |
|
51 |
+ return 1 / odds |
|
52 |
+ |
|
53 |
+ |
|
54 |
+def to_euro(p: float) -> float: |
|
55 |
+ """Return Euro/Decimal odds, given an implied probability.""" |
|
56 |
+ return 1 / p |
|
57 |
+ |
|
58 |
+ |
|
59 |
+def from_british(odds: Fraction) -> float: |
|
60 |
+ """Return implied probability, given British odds.""" |
|
61 |
+ return float(1 / (odds + 1)) |
|
62 |
+ |
|
63 |
+ |
|
64 |
+def to_british(p: float, largest_denominator: int = 100) -> Fraction: |
|
65 |
+ """Return British odds, given an implied probability. |
|
66 |
+ |
|
67 |
+ Parameters |
|
68 |
+ ---------- |
|
69 |
+ p |
|
70 |
+ Implied probability |
|
71 |
+ largest_denominator |
|
72 |
+ The largest denominator to use when representing the result. |
|
73 |
+ The smaller this number the more accuracy is lost. |
|
74 |
+ """ |
|
75 |
+ return Fraction(1 / p - 1).limit_denominator(largest_denominator) |
|
76 |
+ |
|
77 |
+ |
|
78 |
+from_decimal = from_euro |
|
79 |
+to_decimal = to_euro |
|
80 |
+ |
|
81 |
+ |
|
10 | 82 |
# -- Adjusting implied probabilities to true probabilities |
11 | 83 |
|
12 | 84 |
_adjust_methods = {} |
... | ... |
@@ -18,10 +90,6 @@ def add_method(name: str, f): |
18 | 90 |
return f |
19 | 91 |
|
20 | 92 |
|
21 |
-def overround(odds: Sequence[float]) -> float: |
|
22 |
- return sum(odds) - 1 |
|
23 |
- |
|
24 |
- |
|
25 | 93 |
def validate_odds(odds: Sequence[float]) -> None: |
26 | 94 |
if len(odds) < 2: |
27 | 95 |
raise ValueError(f"Expected at least 2 outcomes, but got {len(odds)}") |
... | ... |
@@ -35,7 +103,7 @@ def validate_odds(odds: Sequence[float]) -> None: |
35 | 103 |
|
36 | 104 |
|
37 | 105 |
def adjust_odds(odds: Sequence[float], method="power") -> list[float]: |
38 |
- """Return true odds given bookmaker odds. |
|
106 |
+ """Return fair odds given bookmaker odds. |
|
39 | 107 |
|
40 | 108 |
Parameters |
41 | 109 |
---------- |
... | ... |
@@ -44,7 +112,8 @@ def adjust_odds(odds: Sequence[float], method="power") -> list[float]: |
44 | 112 |
|
45 | 113 |
The implied probabilities associated with the odds quoted by a bookmaker |
46 | 114 |
sum to a number > 1. This excess is the "overround" of the odds. There |
47 |
- are a number of models |
|
115 |
+ are a number of models for distributing the overround among the outcomes |
|
116 |
+ to recover the "fair odds" of each outcome. |
|
48 | 117 |
|
49 | 118 |
Notes |
50 | 119 |
----- |
... | ... |
@@ -61,7 +130,7 @@ def adjust_odds(odds: Sequence[float], method="power") -> list[float]: |
61 | 130 |
except KeyError: |
62 | 131 |
raise ValueError( |
63 | 132 |
f"Unknown method '{method}', expected one of {list(_adjust_methods)}" |
64 |
- ) |
|
133 |
+ ) from None |
|
65 | 134 |
validate_odds(odds) |
66 | 135 |
return f(odds) |
67 | 136 |
|