Browse code

Add first draft of "implied" module

Joseph Weston authored on 03/03/2021 06:42:35
Showing 2 changed files
... ...
@@ -18,5 +18,7 @@ dependencies:
18 18
     - httpx
19 19
     - jsonpatch
20 20
     - msgpack-python
21
+    - numpy
22
+    - scipy
21 23
     - toolz
22 24
     - tqdm
23 25
new file mode 100644
... ...
@@ -0,0 +1,165 @@
1
+"""Tools for dealing with bookmaker's odds and implied probabilities"""
2
+
3
+from math import sqrt
4
+from typing import Sequence
5
+
6
+import scipy.optimize
7
+import toolz
8
+
9
+
10
+# -- Adjusting implied probabilities to true probabilities
11
+
12
+_adjust_methods = {}
13
+
14
+
15
+@toolz.curry
16
+def add_method(name: str, f):
17
+    _adjust_methods[name] = f
18
+    return f
19
+
20
+
21
+def overround(odds: Sequence[float]) -> float:
22
+    return sum(odds) - 1
23
+
24
+
25
+def validate_odds(odds: Sequence[float]) -> None:
26
+    if len(odds) < 2:
27
+        raise ValueError(f"Expected at least 2 outcomes, but got {len(odds)}")
28
+    if any(x >= 1 for x in odds):
29
+        raise ValueError(f"Expected all outcomes to have an implied probability < 1")
30
+    r = overround(odds)
31
+    if r < 0:
32
+        raise ValueError(f"Odds {odds} have a negative overround ({r})")
33
+    elif r > 0.5:
34
+        raise ValueError(f"Odds {odds} have excessive overround ({r})")
35
+
36
+
37
+def adjust_odds(odds: Sequence[float], method="power") -> list[float]:
38
+    """Return true odds given bookmaker odds.
39
+
40
+    Parameters
41
+    ----------
42
+    odds
43
+        The odds of each outcome, given as implied probabilites.
44
+
45
+    The implied probabilities associated with the odds quoted by a bookmaker
46
+    sum to a number > 1. This excess is the "overround" of the odds. There
47
+    are a number of models
48
+
49
+    Notes
50
+    -----
51
+    All implemented methods are based on the descriptions in the paper by
52
+    S. Clarke in the references section.
53
+
54
+    References
55
+    ----------
56
+    S. Clarke, S. Kovalchik, M. Ingram
57
+        Adjusting Bookmaker's odds to Allow for Overround.
58
+    """
59
+    try:
60
+        f = _adjust_methods[method]
61
+    except KeyError:
62
+        raise ValueError(
63
+            f"Unknown method '{method}', expected one of {list(_adjust_methods)}"
64
+        )
65
+    validate_odds(odds)
66
+    return f(odds)
67
+
68
+
69
+@add_method("additive")
70
+def adjust_odds_additive(odds: Sequence[float]) -> list[float]:
71
+    """Adjust bookmaker odds using the additive method.
72
+
73
+    In this method the overround is assumed to be split equality between
74
+    the different outcomes, regardless of their relative implied probabilities.
75
+    """
76
+    y = overround(odds) / len(odds)
77
+    return [x - y for x in odds]
78
+
79
+
80
+@add_method("multiplicative")
81
+def adjust_odds_multiplicative(odds: Sequence[float]) -> list[float]:
82
+    """Adjust bookmaker odds using the multiplicative method.
83
+
84
+    In this method the overround is assumed to be split among the
85
+    different outcomes in proportion to their implied probabilities.
86
+    """
87
+    s = sum(odds)
88
+    r = overround(odds)
89
+    return [x - (r * x / s) for x in odds]
90
+
91
+
92
+@add_method("power")
93
+def adjust_odds_power(odds: Sequence[float]) -> list[float]:
94
+    """Adjust bookmaker odds using the power method.
95
+
96
+    In this method the true probabilities are assumed to be
97
+    related to the implied probabilities by a power law,
98
+    i.e. p_i = π_i^k, for some fixed k. The k is then
99
+    chosen such that the true probabilities sum to 1.
100
+
101
+    Notes
102
+    -----
103
+    The 'k' parameter for the power method is computed by
104
+    solving the nonlinear equation Σ_i π_i^k = 1 using
105
+    a root-finding algorithm.
106
+
107
+    References
108
+    ----------
109
+    S.R. Clarke
110
+        Adjusting true odds to allow for vigorish
111
+    """
112
+    # This bracketing interval should be sufficient for reasonable overrounds
113
+    # (not > 50%) and reasonable distribution of implied probability between
114
+    # the different outcomes.
115
+    try:
116
+        k = scipy.optimize.root_scalar(
117
+            lambda k: sum(x ** k for x in odds) - 1,
118
+            bracket=[0.9, 3],
119
+            xtol=1e-14,
120
+        ).root
121
+    except ValueError as e:
122
+        if "different signs" in str(e):
123
+            raise ValueError(f"Bracketing interval too small for odds {odds}") from None
124
+
125
+    return [x ** k for x in odds]
126
+
127
+
128
+@add_method("shin")
129
+def adjust_odds_shin(odds):
130
+    """Adjust bookmaker odds using the Shin method.
131
+
132
+    In this method we assume a fraction z of sharp bettors
133
+    and adjust the true probabilities accordingly.
134
+
135
+    Notes
136
+    -----
137
+    The 'z' parameter for the Shin method is computed
138
+    by solving a nonlinear equation using a root-finding algorithm.
139
+
140
+    References
141
+    ----------
142
+    H.S. Shin
143
+        Prices of State Contingent Claims with Insider Traders
144
+        and the Favorite-Longshot Bias
145
+    """
146
+    s = sum(odds)
147
+    n = len(odds)
148
+
149
+    def residual(z):
150
+        return (
151
+            sum(sqrt(z ** 2 + 4 * (1 - z) * pi ** 2 / s) for pi in odds)
152
+            - 2
153
+            - (n - 2) * z
154
+        )
155
+
156
+    # 'z' is in the interval [0, 1). We set the upper limit for the
157
+    # bracketing interval to just below 1 to exclude a (spurious) solution at z=1.
158
+    try:
159
+        z = scipy.optimize.root_scalar(residual, bracket=[0, 0.999], xtol=1e-14).root
160
+    except ValueError:
161
+        raise ValueError(f"Shin method failed to converge for odds {odds}") from None
162
+
163
+    return [
164
+        (sqrt(z ** 2 + 4 * (1 - z) * pi ** 2 / s) - z) / (2 * (1 - z)) for pi in odds
165
+    ]