hkjc 0.2.0__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hkjc-0.2.0 → hkjc-0.3.0}/PKG-INFO +2 -1
- {hkjc-0.2.0 → hkjc-0.3.0}/pyproject.toml +2 -1
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/__init__.py +3 -1
- hkjc-0.3.0/src/hkjc/harville_model.py +362 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/processing.py +14 -2
- {hkjc-0.2.0 → hkjc-0.3.0}/uv.lock +53 -1
- hkjc-0.2.0/.pypirc +0 -3
- hkjc-0.2.0/src/hkjc/odds_fitting.py +0 -1
- {hkjc-0.2.0 → hkjc-0.3.0}/.python-version +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/README.md +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/analysis.py +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/live_odds.py +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/optimization.py +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/py.typed +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/qpbanker.py +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/speedpro.py +0 -0
- {hkjc-0.2.0 → hkjc-0.3.0}/src/hkjc/visualization.py +0 -0
@@ -1,10 +1,11 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hkjc
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.3.0
|
4
4
|
Summary: Library for scrapping HKJC data and perform basic analysis
|
5
5
|
Requires-Python: >=3.11
|
6
6
|
Requires-Dist: cachetools>=6.2.0
|
7
7
|
Requires-Dist: fastexcel>=0.16.0
|
8
|
+
Requires-Dist: numba>=0.62.1
|
8
9
|
Requires-Dist: numpy>=2.3.3
|
9
10
|
Requires-Dist: polars>=1.33.1
|
10
11
|
Requires-Dist: pyarrow>=21.0.0
|
@@ -1,12 +1,13 @@
|
|
1
1
|
[project]
|
2
2
|
name = "hkjc"
|
3
|
-
version = "0.
|
3
|
+
version = "0.3.0"
|
4
4
|
description = "Library for scrapping HKJC data and perform basic analysis"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.11"
|
7
7
|
dependencies = [
|
8
8
|
"cachetools>=6.2.0",
|
9
9
|
"fastexcel>=0.16.0",
|
10
|
+
"numba>=0.62.1",
|
10
11
|
"numpy>=2.3.3",
|
11
12
|
"polars>=1.33.1",
|
12
13
|
"pyarrow>=21.0.0",
|
@@ -5,7 +5,8 @@ This module re-exports commonly used symbols from the submodules.
|
|
5
5
|
from importlib.metadata import version as _version
|
6
6
|
|
7
7
|
__all__ = ["live_odds", "qpbanker",
|
8
|
-
"generate_all_qp_trades", "generate_pareto_qp_trades"
|
8
|
+
"generate_all_qp_trades", "generate_pareto_qp_trades",
|
9
|
+
"speedpro_df", "speedmap"]
|
9
10
|
|
10
11
|
try:
|
11
12
|
__version__ = _version(__name__)
|
@@ -14,3 +15,4 @@ except Exception: # pragma: no cover - best-effort version resolution
|
|
14
15
|
|
15
16
|
from .live_odds import live_odds
|
16
17
|
from .processing import generate_all_qp_trades, generate_pareto_qp_trades
|
18
|
+
from .speedpro import speedmap, speedpro_df
|
@@ -0,0 +1,362 @@
|
|
1
|
+
"""
|
2
|
+
Harville Race Model Optimizer
|
3
|
+
|
4
|
+
Estimates horse racing outcome probabilities using the Harville model via dynamic
|
5
|
+
programming. Fits latent strength parameters from observed betting market odds across
|
6
|
+
multiple pool types (Win, Qin, Quinella, Banker).
|
7
|
+
|
8
|
+
The optimizer uses O(N * 2^N) complexity DP with Numba JIT compilation for speed.
|
9
|
+
Suitable for races with up to ~20 horses.
|
10
|
+
|
11
|
+
Example:
|
12
|
+
>>> optimizer = HarvilleOptimizer(n_horses=14)
|
13
|
+
>>> results = optimizer.fit(W_obs=win_odds, Qin_obs=qin_odds,
|
14
|
+
... Q_obs=quinella_odds, b_obs=banker_odds)
|
15
|
+
>>> print(results['theta']) # Fitted strength parameters
|
16
|
+
"""
|
17
|
+
|
18
|
+
import numpy as np
|
19
|
+
from scipy.optimize import minimize
|
20
|
+
from numba import njit
|
21
|
+
from typing import Tuple, Optional
|
22
|
+
|
23
|
+
|
24
|
+
@njit(cache=True)
|
25
|
+
def _popcount(mask: int) -> int:
|
26
|
+
count = 0
|
27
|
+
while mask:
|
28
|
+
count += 1
|
29
|
+
mask &= mask - 1
|
30
|
+
return count
|
31
|
+
|
32
|
+
|
33
|
+
@njit(cache=True)
|
34
|
+
def _precompute_mask_info(n: int) -> Tuple[np.ndarray, np.ndarray]:
|
35
|
+
max_mask = 1 << n
|
36
|
+
mask_strength_coef = np.zeros((max_mask, n), dtype=np.float64)
|
37
|
+
mask_popcount = np.zeros(max_mask, dtype=np.int32)
|
38
|
+
|
39
|
+
for mask in range(max_mask):
|
40
|
+
mask_popcount[mask] = _popcount(mask)
|
41
|
+
for i in range(n):
|
42
|
+
if mask & (1 << i):
|
43
|
+
mask_strength_coef[mask, i] = 1.0
|
44
|
+
|
45
|
+
return mask_strength_coef, mask_popcount
|
46
|
+
|
47
|
+
|
48
|
+
@njit(cache=True)
|
49
|
+
def _compute_dp_vectorized(theta: np.ndarray, k_max: int) -> np.ndarray:
|
50
|
+
n = len(theta)
|
51
|
+
max_mask = 1 << n
|
52
|
+
|
53
|
+
mask_strength_coef, mask_popcount = _precompute_mask_info(n)
|
54
|
+
mask_strength = mask_strength_coef @ theta
|
55
|
+
|
56
|
+
dp = np.zeros((k_max + 1, max_mask))
|
57
|
+
dp[0, 0] = 1.0
|
58
|
+
|
59
|
+
for k in range(k_max):
|
60
|
+
valid_masks = np.where(mask_popcount == k)[0]
|
61
|
+
|
62
|
+
for mask in valid_masks:
|
63
|
+
if dp[k, mask] == 0:
|
64
|
+
continue
|
65
|
+
|
66
|
+
s_mask = mask_strength[mask]
|
67
|
+
remaining = 1.0 - s_mask
|
68
|
+
|
69
|
+
if remaining < 1e-12:
|
70
|
+
continue
|
71
|
+
|
72
|
+
prob_current = dp[k, mask]
|
73
|
+
|
74
|
+
for i in range(n):
|
75
|
+
if not (mask & (1 << i)):
|
76
|
+
next_mask = mask | (1 << i)
|
77
|
+
dp[k + 1, next_mask] += prob_current * theta[i] / remaining
|
78
|
+
|
79
|
+
return dp
|
80
|
+
|
81
|
+
|
82
|
+
@njit(cache=True)
|
83
|
+
def _extract_pair_in_top_k(dp: np.ndarray, n: int, k: int) -> np.ndarray:
|
84
|
+
M = np.zeros((n, n))
|
85
|
+
max_mask = 1 << n
|
86
|
+
|
87
|
+
mask_popcount = np.zeros(max_mask, dtype=np.int32)
|
88
|
+
for mask in range(max_mask):
|
89
|
+
mask_popcount[mask] = _popcount(mask)
|
90
|
+
|
91
|
+
masks_size_k = np.where(mask_popcount == k)[0]
|
92
|
+
|
93
|
+
for mask in masks_size_k:
|
94
|
+
prob = dp[k, mask]
|
95
|
+
if prob == 0:
|
96
|
+
continue
|
97
|
+
|
98
|
+
horses = np.empty(k, dtype=np.int32)
|
99
|
+
idx = 0
|
100
|
+
for i in range(n):
|
101
|
+
if mask & (1 << i):
|
102
|
+
horses[idx] = i
|
103
|
+
idx += 1
|
104
|
+
|
105
|
+
for i in range(k):
|
106
|
+
for j in range(k):
|
107
|
+
M[horses[i], horses[j]] += prob
|
108
|
+
|
109
|
+
return M
|
110
|
+
|
111
|
+
|
112
|
+
@njit(cache=True)
|
113
|
+
def _extract_top_k_probs(dp: np.ndarray, n: int, k_max: int) -> np.ndarray:
|
114
|
+
T = np.zeros((n, k_max + 1))
|
115
|
+
max_mask = 1 << n
|
116
|
+
|
117
|
+
mask_popcount = np.zeros(max_mask, dtype=np.int32)
|
118
|
+
for mask in range(max_mask):
|
119
|
+
mask_popcount[mask] = _popcount(mask)
|
120
|
+
|
121
|
+
for k in range(1, k_max + 1):
|
122
|
+
masks_size_k = np.where(mask_popcount == k)[0]
|
123
|
+
|
124
|
+
for mask in masks_size_k:
|
125
|
+
prob = dp[k, mask]
|
126
|
+
if prob == 0:
|
127
|
+
continue
|
128
|
+
|
129
|
+
for i in range(n):
|
130
|
+
if mask & (1 << i):
|
131
|
+
T[i, k] += prob
|
132
|
+
|
133
|
+
return T
|
134
|
+
|
135
|
+
|
136
|
+
@njit(cache=True)
|
137
|
+
def _compute_probabilities(theta: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
138
|
+
n = len(theta)
|
139
|
+
|
140
|
+
dp = _compute_dp_vectorized(theta, n)
|
141
|
+
|
142
|
+
T = _extract_top_k_probs(dp, n, n)
|
143
|
+
|
144
|
+
P = np.zeros((n, n))
|
145
|
+
for i in range(n):
|
146
|
+
for j in range(n):
|
147
|
+
P[i, j] = T[i, j + 1] - T[i, j]
|
148
|
+
|
149
|
+
W = P[:, 0]
|
150
|
+
Qin = _extract_pair_in_top_k(dp, n, 2)
|
151
|
+
Q = _extract_pair_in_top_k(dp, n, 3)
|
152
|
+
b = T[:, 3]
|
153
|
+
|
154
|
+
return W, Qin, Q, b, P
|
155
|
+
|
156
|
+
|
157
|
+
@njit(cache=True)
|
158
|
+
def _kl_divergence(p_obs: np.ndarray, p_model: np.ndarray) -> float:
|
159
|
+
eps = 1e-10
|
160
|
+
|
161
|
+
p_obs_flat = np.maximum(p_obs.ravel(), eps)
|
162
|
+
p_model_flat = np.maximum(p_model.ravel(), eps)
|
163
|
+
|
164
|
+
sum_obs = p_obs_flat.sum()
|
165
|
+
sum_model = p_model_flat.sum()
|
166
|
+
|
167
|
+
if sum_obs > eps:
|
168
|
+
p_obs_flat = p_obs_flat / sum_obs
|
169
|
+
if sum_model > eps:
|
170
|
+
p_model_flat = p_model_flat / sum_model
|
171
|
+
|
172
|
+
return np.sum(p_obs_flat * np.log(p_obs_flat / p_model_flat))
|
173
|
+
|
174
|
+
|
175
|
+
class HarvilleOptimizer:
|
176
|
+
"""
|
177
|
+
Fits Harville race model to betting market odds using dynamic programming.
|
178
|
+
|
179
|
+
The Harville model assigns each horse a latent strength parameter theta_i, where
|
180
|
+
the probability of finishing next among remaining horses is proportional to
|
181
|
+
relative strength. This optimizer estimates theta from observed betting odds
|
182
|
+
across multiple pool types.
|
183
|
+
|
184
|
+
Default lambda weights (1.0, 2.0, 1.5, 0.7) reflect that early Win odds are
|
185
|
+
biased by informed traders waiting until closing, while exotic pools provide
|
186
|
+
more stable signals for ensemble estimation.
|
187
|
+
|
188
|
+
Attributes:
|
189
|
+
n (int): Number of horses
|
190
|
+
lambda_win (float): Weight for Win pool loss
|
191
|
+
lambda_qin (float): Weight for Qin pool loss
|
192
|
+
lambda_quinella (float): Weight for Quinella pool loss
|
193
|
+
lambda_banker (float): Weight for Banker pool loss
|
194
|
+
"""
|
195
|
+
|
196
|
+
def __init__(self, n_horses: int, lambda_win: float = 1.0, lambda_qin: float = 2.0,
|
197
|
+
lambda_quinella: float = 1.5, lambda_banker: float = 0.7):
|
198
|
+
"""
|
199
|
+
Initialize optimizer.
|
200
|
+
|
201
|
+
Args:
|
202
|
+
n_horses: Number of horses in race (recommend <= 20 for speed)
|
203
|
+
lambda_win: Weight for Win odds (prob horse finishes 1st)
|
204
|
+
lambda_qin: Weight for Qin odds (prob pair finishes 1st-2nd)
|
205
|
+
lambda_quinella: Weight for Quinella odds (prob pair in top 3)
|
206
|
+
lambda_banker: Weight for Banker odds (prob horse in top 3)
|
207
|
+
|
208
|
+
Raises:
|
209
|
+
ValueError: If n_horses > 20 (exponential complexity warning)
|
210
|
+
"""
|
211
|
+
if n_horses > 20:
|
212
|
+
raise ValueError("N > 20 may be too slow (2^N complexity)")
|
213
|
+
|
214
|
+
self.n = n_horses
|
215
|
+
self.lambda_win = lambda_win
|
216
|
+
self.lambda_qin = lambda_qin
|
217
|
+
self.lambda_quinella = lambda_quinella
|
218
|
+
self.lambda_banker = lambda_banker
|
219
|
+
self._eval_count = 0
|
220
|
+
|
221
|
+
def loss(self, theta: np.ndarray, W_obs: Optional[np.ndarray],
|
222
|
+
Qin_obs: Optional[np.ndarray], Q_obs: Optional[np.ndarray],
|
223
|
+
b_obs: Optional[np.ndarray]) -> float:
|
224
|
+
"""
|
225
|
+
Compute weighted KL divergence loss between observed and model odds.
|
226
|
+
|
227
|
+
Args:
|
228
|
+
theta: Strength parameters (will be normalized to simplex)
|
229
|
+
W_obs: Observed Win probabilities (n,) or None
|
230
|
+
Qin_obs: Observed Qin probabilities (n, n) or None
|
231
|
+
Q_obs: Observed Quinella probabilities (n, n) or None
|
232
|
+
b_obs: Observed Banker probabilities (n,) or None
|
233
|
+
|
234
|
+
Returns:
|
235
|
+
Scalar loss value (sum of weighted KL divergences)
|
236
|
+
"""
|
237
|
+
self._eval_count += 1
|
238
|
+
|
239
|
+
theta = np.abs(theta) + 1e-10
|
240
|
+
theta = theta / theta.sum()
|
241
|
+
|
242
|
+
W_model, Qin_model, Q_model, b_model, P_model = _compute_probabilities(theta)
|
243
|
+
|
244
|
+
loss = 0.0
|
245
|
+
|
246
|
+
if W_obs is not None:
|
247
|
+
loss += self.lambda_win * _kl_divergence(W_obs, W_model)
|
248
|
+
|
249
|
+
if Qin_obs is not None:
|
250
|
+
loss += self.lambda_qin * _kl_divergence(Qin_obs, Qin_model)
|
251
|
+
|
252
|
+
if Q_obs is not None:
|
253
|
+
loss += self.lambda_quinella * _kl_divergence(Q_obs, Q_model)
|
254
|
+
|
255
|
+
if b_obs is not None:
|
256
|
+
loss += self.lambda_banker * _kl_divergence(b_obs, b_model)
|
257
|
+
|
258
|
+
return loss
|
259
|
+
|
260
|
+
def fit(self, W_obs: Optional[np.ndarray] = None,
|
261
|
+
Qin_obs: Optional[np.ndarray] = None,
|
262
|
+
Q_obs: Optional[np.ndarray] = None,
|
263
|
+
b_obs: Optional[np.ndarray] = None,
|
264
|
+
theta_init: Optional[np.ndarray] = None,
|
265
|
+
method: str = 'L-BFGS-B') -> dict:
|
266
|
+
"""
|
267
|
+
Fit Harville model to observed betting odds.
|
268
|
+
|
269
|
+
At least one odds type must be provided. All odds should be probabilities
|
270
|
+
(not decimal/fractional odds). Matrices should be symmetric where applicable.
|
271
|
+
|
272
|
+
Args:
|
273
|
+
W_obs: Win probabilities, shape (n,). W_obs[i] = prob horse i wins
|
274
|
+
Qin_obs: Qin probabilities, shape (n, n). Qin_obs[i,j] = prob horses
|
275
|
+
i,j finish 1st-2nd in any order
|
276
|
+
Q_obs: Quinella probabilities, shape (n, n). Q_obs[i,j] = prob horses
|
277
|
+
i,j both finish in top 3
|
278
|
+
b_obs: Banker probabilities, shape (n,). b_obs[i] = prob horse i
|
279
|
+
finishes in top 3
|
280
|
+
theta_init: Initial strength guess (default: W_obs if available, else uniform)
|
281
|
+
method: Scipy optimizer ('L-BFGS-B' or 'SLSQP')
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
Dictionary containing:
|
285
|
+
- theta: Fitted strength parameters (n,)
|
286
|
+
- W_fitted: Fitted Win probabilities (n,)
|
287
|
+
- Qin_fitted: Fitted Qin probabilities (n, n)
|
288
|
+
- Q_fitted: Fitted Quinella probabilities (n, n)
|
289
|
+
- b_fitted: Fitted Banker probabilities (n,)
|
290
|
+
- P_fitted: Full place probability matrix (n, n), P[i,j] =
|
291
|
+
prob horse i finishes in position j
|
292
|
+
- loss: Final loss value
|
293
|
+
- success: Whether optimization converged
|
294
|
+
- message: Optimizer status message
|
295
|
+
- n_eval: Number of loss function evaluations
|
296
|
+
|
297
|
+
Raises:
|
298
|
+
ValueError: If no odds provided or shapes don't match n_horses
|
299
|
+
|
300
|
+
Example:
|
301
|
+
>>> opt = HarvilleOptimizer(n_horses=10)
|
302
|
+
>>> results = opt.fit(W_obs=win_probs, Q_obs=quinella_probs)
|
303
|
+
>>> print(f"Fitted strengths: {results['theta']}")
|
304
|
+
>>> print(f"Converged: {results['success']}")
|
305
|
+
"""
|
306
|
+
if W_obs is None and Qin_obs is None and Q_obs is None and b_obs is None:
|
307
|
+
raise ValueError("At least one type of odds must be provided")
|
308
|
+
|
309
|
+
if W_obs is not None and W_obs.shape != (self.n,):
|
310
|
+
raise ValueError(f"W_obs must be ({self.n},)")
|
311
|
+
if Qin_obs is not None and Qin_obs.shape != (self.n, self.n):
|
312
|
+
raise ValueError(f"Qin_obs must be ({self.n}, {self.n})")
|
313
|
+
if Q_obs is not None and Q_obs.shape != (self.n, self.n):
|
314
|
+
raise ValueError(f"Q_obs must be ({self.n}, {self.n})")
|
315
|
+
if b_obs is not None and b_obs.shape != (self.n,):
|
316
|
+
raise ValueError(f"b_obs must be ({self.n},)")
|
317
|
+
|
318
|
+
if theta_init is None:
|
319
|
+
if W_obs is not None:
|
320
|
+
theta_init = W_obs / W_obs.sum()
|
321
|
+
else:
|
322
|
+
theta_init = np.ones(self.n) / self.n
|
323
|
+
else:
|
324
|
+
theta_init = theta_init / theta_init.sum()
|
325
|
+
|
326
|
+
self._eval_count = 0
|
327
|
+
|
328
|
+
if method == 'L-BFGS-B':
|
329
|
+
result = minimize(
|
330
|
+
fun=lambda x: self.loss(x, W_obs, Qin_obs, Q_obs, b_obs),
|
331
|
+
x0=theta_init,
|
332
|
+
method='L-BFGS-B',
|
333
|
+
bounds=[(1e-6, 1.0) for _ in range(self.n)],
|
334
|
+
options={'maxiter': 500, 'ftol': 1e-9, 'maxls': 50}
|
335
|
+
)
|
336
|
+
else:
|
337
|
+
result = minimize(
|
338
|
+
fun=lambda x: self.loss(x, W_obs, Qin_obs, Q_obs, b_obs),
|
339
|
+
x0=theta_init,
|
340
|
+
method='SLSQP',
|
341
|
+
bounds=[(1e-6, 1.0) for _ in range(self.n)],
|
342
|
+
constraints={'type': 'eq', 'fun': lambda x: x.sum() - 1},
|
343
|
+
options={'maxiter': 500, 'ftol': 1e-9}
|
344
|
+
)
|
345
|
+
|
346
|
+
theta_opt = np.abs(result.x) + 1e-10
|
347
|
+
theta_opt = theta_opt / theta_opt.sum()
|
348
|
+
|
349
|
+
W_fitted, Qin_fitted, Q_fitted, b_fitted, P_fitted = _compute_probabilities(theta_opt)
|
350
|
+
|
351
|
+
return {
|
352
|
+
'theta': theta_opt,
|
353
|
+
'W_fitted': W_fitted,
|
354
|
+
'Qin_fitted': Qin_fitted,
|
355
|
+
'Q_fitted': Q_fitted,
|
356
|
+
'b_fitted': b_fitted,
|
357
|
+
'P_fitted': P_fitted,
|
358
|
+
'loss': result.fun,
|
359
|
+
'success': result.success,
|
360
|
+
'message': result.message,
|
361
|
+
'n_eval': self._eval_count
|
362
|
+
}
|
@@ -6,6 +6,7 @@ from typing import Tuple, List
|
|
6
6
|
from .live_odds import live_odds
|
7
7
|
from .qpbanker import win_probability, expected_value, average_odds
|
8
8
|
from .optimization import _pareto_filter
|
9
|
+
from .harville_model import HarvilleOptimizer
|
9
10
|
|
10
11
|
import polars as pl
|
11
12
|
import numpy as np
|
@@ -26,7 +27,7 @@ def _process_single_qp_trade(banker: int, covered: List[int], odds_pla: List[flo
|
|
26
27
|
return (banker, covered, win_prob, exp_value, ave_odds)
|
27
28
|
|
28
29
|
|
29
|
-
def generate_all_qp_trades(date: str, venue_code: str, race_number: int, rebate: float = 0.12) -> pl.DataFrame:
|
30
|
+
def generate_all_qp_trades(date: str, venue_code: str, race_number: int, rebate: float = 0.12, harville_fit=True) -> pl.DataFrame:
|
30
31
|
"""Generate all possible qp tickets for the specified race.
|
31
32
|
|
32
33
|
Args:
|
@@ -34,14 +35,25 @@ def generate_all_qp_trades(date: str, venue_code: str, race_number: int, rebate:
|
|
34
35
|
venue_code (str): Venue code, e.g., 'ST' for Shatin, 'HV' for Happy Valley.
|
35
36
|
race_number (int): Race number.
|
36
37
|
rebate (float, optional): The rebate percentage. Defaults to 0.12.
|
38
|
+
harville_fit (bool, optional): Whether to fit the odds using Harville model. Defaults to True.
|
37
39
|
|
38
40
|
Returns:
|
39
41
|
pl.DataFrame: DataFrame with all possible trades and their metrics.
|
40
42
|
"""
|
41
|
-
|
43
|
+
|
44
|
+
odds = live_odds(date, venue_code, race_number,
|
45
|
+
odds_type=['PLA', 'QPL', 'WIN', 'QIN'])
|
42
46
|
N = len(odds['PLA'])
|
43
47
|
candidates = np.arange(1, N+1)
|
44
48
|
|
49
|
+
if harville_fit:
|
50
|
+
ho = HarvilleOptimizer(N)
|
51
|
+
fit_res = ho.fit(1/odds['WIN'], 1/odds['QIN'],
|
52
|
+
1/odds['QPL'], 1/odds['PLA'])
|
53
|
+
if fit_res['success']:
|
54
|
+
odds['PLA'] = 1/fit_res['b_fitted']
|
55
|
+
odds['QPL'] = 1/fit_res['Q_fitted']
|
56
|
+
|
45
57
|
results = [_process_single_qp_trade(banker, covered, odds['PLA'], odds['QPL'], rebate)
|
46
58
|
for banker in tqdm(candidates, desc="Processing bankers")
|
47
59
|
for covered in _all_subsets(candidates[candidates != banker])]
|
@@ -97,11 +97,12 @@ wheels = [
|
|
97
97
|
|
98
98
|
[[package]]
|
99
99
|
name = "hkjc"
|
100
|
-
version = "0.
|
100
|
+
version = "0.3.0"
|
101
101
|
source = { editable = "." }
|
102
102
|
dependencies = [
|
103
103
|
{ name = "cachetools" },
|
104
104
|
{ name = "fastexcel" },
|
105
|
+
{ name = "numba" },
|
105
106
|
{ name = "numpy" },
|
106
107
|
{ name = "polars" },
|
107
108
|
{ name = "pyarrow" },
|
@@ -114,6 +115,7 @@ dependencies = [
|
|
114
115
|
requires-dist = [
|
115
116
|
{ name = "cachetools", specifier = ">=6.2.0" },
|
116
117
|
{ name = "fastexcel", specifier = ">=0.16.0" },
|
118
|
+
{ name = "numba", specifier = ">=0.62.1" },
|
117
119
|
{ name = "numpy", specifier = ">=2.3.3" },
|
118
120
|
{ name = "polars", specifier = ">=1.33.1" },
|
119
121
|
{ name = "pyarrow", specifier = ">=21.0.0" },
|
@@ -131,6 +133,56 @@ wheels = [
|
|
131
133
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
132
134
|
]
|
133
135
|
|
136
|
+
[[package]]
|
137
|
+
name = "llvmlite"
|
138
|
+
version = "0.45.1"
|
139
|
+
source = { registry = "https://pypi.org/simple" }
|
140
|
+
sdist = { url = "https://files.pythonhosted.org/packages/99/8d/5baf1cef7f9c084fb35a8afbde88074f0d6a727bc63ef764fe0e7543ba40/llvmlite-0.45.1.tar.gz", hash = "sha256:09430bb9d0bb58fc45a45a57c7eae912850bedc095cd0810a57de109c69e1c32", size = 185600, upload-time = "2025-10-01T17:59:52.046Z" }
|
141
|
+
wheels = [
|
142
|
+
{ url = "https://files.pythonhosted.org/packages/04/ad/9bdc87b2eb34642c1cfe6bcb4f5db64c21f91f26b010f263e7467e7536a3/llvmlite-0.45.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:60f92868d5d3af30b4239b50e1717cb4e4e54f6ac1c361a27903b318d0f07f42", size = 43043526, upload-time = "2025-10-01T18:03:15.051Z" },
|
143
|
+
{ url = "https://files.pythonhosted.org/packages/a5/ea/c25c6382f452a943b4082da5e8c1665ce29a62884e2ec80608533e8e82d5/llvmlite-0.45.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:98baab513e19beb210f1ef39066288784839a44cd504e24fff5d17f1b3cf0860", size = 37253118, upload-time = "2025-10-01T18:04:06.783Z" },
|
144
|
+
{ url = "https://files.pythonhosted.org/packages/fe/af/85fc237de98b181dbbe8647324331238d6c52a3554327ccdc83ced28efba/llvmlite-0.45.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3adc2355694d6a6fbcc024d59bb756677e7de506037c878022d7b877e7613a36", size = 56288209, upload-time = "2025-10-01T18:01:00.168Z" },
|
145
|
+
{ url = "https://files.pythonhosted.org/packages/0a/df/3daf95302ff49beff4230065e3178cd40e71294968e8d55baf4a9e560814/llvmlite-0.45.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f3377a6db40f563058c9515dedcc8a3e562d8693a106a28f2ddccf2c8fcf6ca", size = 55140958, upload-time = "2025-10-01T18:02:11.199Z" },
|
146
|
+
{ url = "https://files.pythonhosted.org/packages/a4/56/4c0d503fe03bac820ecdeb14590cf9a248e120f483bcd5c009f2534f23f0/llvmlite-0.45.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9c272682d91e0d57f2a76c6d9ebdfccc603a01828cdbe3d15273bdca0c3363a", size = 38132232, upload-time = "2025-10-01T18:04:52.181Z" },
|
147
|
+
{ url = "https://files.pythonhosted.org/packages/e2/7c/82cbd5c656e8991bcc110c69d05913be2229302a92acb96109e166ae31fb/llvmlite-0.45.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:28e763aba92fe9c72296911e040231d486447c01d4f90027c8e893d89d49b20e", size = 43043524, upload-time = "2025-10-01T18:03:30.666Z" },
|
148
|
+
{ url = "https://files.pythonhosted.org/packages/9d/bc/5314005bb2c7ee9f33102c6456c18cc81745d7055155d1218f1624463774/llvmlite-0.45.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1a53f4b74ee9fd30cb3d27d904dadece67a7575198bd80e687ee76474620735f", size = 37253123, upload-time = "2025-10-01T18:04:18.177Z" },
|
149
|
+
{ url = "https://files.pythonhosted.org/packages/96/76/0f7154952f037cb320b83e1c952ec4a19d5d689cf7d27cb8a26887d7bbc1/llvmlite-0.45.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b3796b1b1e1c14dcae34285d2f4ea488402fbd2c400ccf7137603ca3800864f", size = 56288211, upload-time = "2025-10-01T18:01:24.079Z" },
|
150
|
+
{ url = "https://files.pythonhosted.org/packages/00/b1/0b581942be2683ceb6862d558979e87387e14ad65a1e4db0e7dd671fa315/llvmlite-0.45.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:779e2f2ceefef0f4368548685f0b4adde34e5f4b457e90391f570a10b348d433", size = 55140958, upload-time = "2025-10-01T18:02:30.482Z" },
|
151
|
+
{ url = "https://files.pythonhosted.org/packages/33/94/9ba4ebcf4d541a325fd8098ddc073b663af75cc8b065b6059848f7d4dce7/llvmlite-0.45.1-cp312-cp312-win_amd64.whl", hash = "sha256:9e6c9949baf25d9aa9cd7cf0f6d011b9ca660dd17f5ba2b23bdbdb77cc86b116", size = 38132231, upload-time = "2025-10-01T18:05:03.664Z" },
|
152
|
+
{ url = "https://files.pythonhosted.org/packages/1d/e2/c185bb7e88514d5025f93c6c4092f6120c6cea8fe938974ec9860fb03bbb/llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d9ea9e6f17569a4253515cc01dade70aba536476e3d750b2e18d81d7e670eb15", size = 43043524, upload-time = "2025-10-01T18:03:43.249Z" },
|
153
|
+
{ url = "https://files.pythonhosted.org/packages/09/b8/b5437b9ecb2064e89ccf67dccae0d02cd38911705112dd0dcbfa9cd9a9de/llvmlite-0.45.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:c9f3cadee1630ce4ac18ea38adebf2a4f57a89bd2740ce83746876797f6e0bfb", size = 37253121, upload-time = "2025-10-01T18:04:30.557Z" },
|
154
|
+
{ url = "https://files.pythonhosted.org/packages/f7/97/ad1a907c0173a90dd4df7228f24a3ec61058bc1a9ff8a0caec20a0cc622e/llvmlite-0.45.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:57c48bf2e1083eedbc9406fb83c4e6483017879714916fe8be8a72a9672c995a", size = 56288210, upload-time = "2025-10-01T18:01:40.26Z" },
|
155
|
+
{ url = "https://files.pythonhosted.org/packages/32/d8/c99c8ac7a326e9735401ead3116f7685a7ec652691aeb2615aa732b1fc4a/llvmlite-0.45.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3aa3dfceda4219ae39cf18806c60eeb518c1680ff834b8b311bd784160b9ce40", size = 55140957, upload-time = "2025-10-01T18:02:46.244Z" },
|
156
|
+
{ url = "https://files.pythonhosted.org/packages/09/56/ed35668130e32dbfad2eb37356793b0a95f23494ab5be7d9bf5cb75850ee/llvmlite-0.45.1-cp313-cp313-win_amd64.whl", hash = "sha256:080e6f8d0778a8239cd47686d402cb66eb165e421efa9391366a9b7e5810a38b", size = 38132232, upload-time = "2025-10-01T18:05:14.477Z" },
|
157
|
+
]
|
158
|
+
|
159
|
+
[[package]]
|
160
|
+
name = "numba"
|
161
|
+
version = "0.62.1"
|
162
|
+
source = { registry = "https://pypi.org/simple" }
|
163
|
+
dependencies = [
|
164
|
+
{ name = "llvmlite" },
|
165
|
+
{ name = "numpy" },
|
166
|
+
]
|
167
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a3/20/33dbdbfe60e5fd8e3dbfde299d106279a33d9f8308346022316781368591/numba-0.62.1.tar.gz", hash = "sha256:7b774242aa890e34c21200a1fc62e5b5757d5286267e71103257f4e2af0d5161", size = 2749817, upload-time = "2025-09-29T10:46:31.551Z" }
|
168
|
+
wheels = [
|
169
|
+
{ url = "https://files.pythonhosted.org/packages/dd/5f/8b3491dd849474f55e33c16ef55678ace1455c490555337899c35826836c/numba-0.62.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:f43e24b057714e480fe44bc6031de499e7cf8150c63eb461192caa6cc8530bc8", size = 2684279, upload-time = "2025-09-29T10:43:37.213Z" },
|
170
|
+
{ url = "https://files.pythonhosted.org/packages/bf/18/71969149bfeb65a629e652b752b80167fe8a6a6f6e084f1f2060801f7f31/numba-0.62.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:57cbddc53b9ee02830b828a8428757f5c218831ccc96490a314ef569d8342b7b", size = 2687330, upload-time = "2025-09-29T10:43:59.601Z" },
|
171
|
+
{ url = "https://files.pythonhosted.org/packages/0e/7d/403be3fecae33088027bc8a95dc80a2fda1e3beff3e0e5fc4374ada3afbe/numba-0.62.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:604059730c637c7885386521bb1b0ddcbc91fd56131a6dcc54163d6f1804c872", size = 3739727, upload-time = "2025-09-29T10:42:45.922Z" },
|
172
|
+
{ url = "https://files.pythonhosted.org/packages/e0/c3/3d910d08b659a6d4c62ab3cd8cd93c4d8b7709f55afa0d79a87413027ff6/numba-0.62.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6c540880170bee817011757dc9049dba5a29db0c09b4d2349295991fe3ee55f", size = 3445490, upload-time = "2025-09-29T10:43:12.692Z" },
|
173
|
+
{ url = "https://files.pythonhosted.org/packages/5b/82/9d425c2f20d9f0a37f7cb955945a553a00fa06a2b025856c3550227c5543/numba-0.62.1-cp311-cp311-win_amd64.whl", hash = "sha256:03de6d691d6b6e2b76660ba0f38f37b81ece8b2cc524a62f2a0cfae2bfb6f9da", size = 2745550, upload-time = "2025-09-29T10:44:20.571Z" },
|
174
|
+
{ url = "https://files.pythonhosted.org/packages/5e/fa/30fa6873e9f821c0ae755915a3ca444e6ff8d6a7b6860b669a3d33377ac7/numba-0.62.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:1b743b32f8fa5fff22e19c2e906db2f0a340782caf024477b97801b918cf0494", size = 2685346, upload-time = "2025-09-29T10:43:43.677Z" },
|
175
|
+
{ url = "https://files.pythonhosted.org/packages/a9/d5/504ce8dc46e0dba2790c77e6b878ee65b60fe3e7d6d0006483ef6fde5a97/numba-0.62.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:90fa21b0142bcf08ad8e32a97d25d0b84b1e921bc9423f8dda07d3652860eef6", size = 2688139, upload-time = "2025-09-29T10:44:04.894Z" },
|
176
|
+
{ url = "https://files.pythonhosted.org/packages/50/5f/6a802741176c93f2ebe97ad90751894c7b0c922b52ba99a4395e79492205/numba-0.62.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ef84d0ac19f1bf80431347b6f4ce3c39b7ec13f48f233a48c01e2ec06ecbc59", size = 3796453, upload-time = "2025-09-29T10:42:52.771Z" },
|
177
|
+
{ url = "https://files.pythonhosted.org/packages/7e/df/efd21527d25150c4544eccc9d0b7260a5dec4b7e98b5a581990e05a133c0/numba-0.62.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9315cc5e441300e0ca07c828a627d92a6802bcbf27c5487f31ae73783c58da53", size = 3496451, upload-time = "2025-09-29T10:43:19.279Z" },
|
178
|
+
{ url = "https://files.pythonhosted.org/packages/80/44/79bfdab12a02796bf4f1841630355c82b5a69933b1d50eb15c7fa37dabe8/numba-0.62.1-cp312-cp312-win_amd64.whl", hash = "sha256:44e3aa6228039992f058f5ebfcfd372c83798e9464297bdad8cc79febcf7891e", size = 2745552, upload-time = "2025-09-29T10:44:26.399Z" },
|
179
|
+
{ url = "https://files.pythonhosted.org/packages/22/76/501ea2c07c089ef1386868f33dff2978f43f51b854e34397b20fc55e0a58/numba-0.62.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:b72489ba8411cc9fdcaa2458d8f7677751e94f0109eeb53e5becfdc818c64afb", size = 2685766, upload-time = "2025-09-29T10:43:49.161Z" },
|
180
|
+
{ url = "https://files.pythonhosted.org/packages/80/68/444986ed95350c0611d5c7b46828411c222ce41a0c76707c36425d27ce29/numba-0.62.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:44a1412095534a26fb5da2717bc755b57da5f3053965128fe3dc286652cc6a92", size = 2688741, upload-time = "2025-09-29T10:44:10.07Z" },
|
181
|
+
{ url = "https://files.pythonhosted.org/packages/78/7e/bf2e3634993d57f95305c7cee4c9c6cb3c9c78404ee7b49569a0dfecfe33/numba-0.62.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8c9460b9e936c5bd2f0570e20a0a5909ee6e8b694fd958b210e3bde3a6dba2d7", size = 3804576, upload-time = "2025-09-29T10:42:59.53Z" },
|
182
|
+
{ url = "https://files.pythonhosted.org/packages/e8/b6/8a1723fff71f63bbb1354bdc60a1513a068acc0f5322f58da6f022d20247/numba-0.62.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:728f91a874192df22d74e3fd42c12900b7ce7190b1aad3574c6c61b08313e4c5", size = 3503367, upload-time = "2025-09-29T10:43:26.326Z" },
|
183
|
+
{ url = "https://files.pythonhosted.org/packages/9c/ec/9d414e7a80d6d1dc4af0e07c6bfe293ce0b04ea4d0ed6c45dad9bd6e72eb/numba-0.62.1-cp313-cp313-win_amd64.whl", hash = "sha256:bbf3f88b461514287df66bc8d0307e949b09f2b6f67da92265094e8fa1282dd8", size = 2745529, upload-time = "2025-09-29T10:44:31.738Z" },
|
184
|
+
]
|
185
|
+
|
134
186
|
[[package]]
|
135
187
|
name = "numpy"
|
136
188
|
version = "2.3.3"
|
hkjc-0.2.0/.pypirc
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
## TODO: implement odds filtering
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|