hkjc 0.3.17__py3-none-any.whl → 0.3.18__py3-none-any.whl
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/__init__.py +2 -3
- hkjc/historical.py +2 -3
- hkjc/live.py +375 -0
- hkjc/processing.py +1 -1
- {hkjc-0.3.17.dist-info → hkjc-0.3.18.dist-info}/METADATA +2 -2
- {hkjc-0.3.17.dist-info → hkjc-0.3.18.dist-info}/RECORD +7 -7
- hkjc/live_odds.py +0 -136
- {hkjc-0.3.17.dist-info → hkjc-0.3.18.dist-info}/WHEEL +0 -0
hkjc/__init__.py
CHANGED
@@ -4,7 +4,7 @@ This module re-exports commonly used symbols from the submodules.
|
|
4
4
|
"""
|
5
5
|
from importlib.metadata import version as _version
|
6
6
|
|
7
|
-
__all__ = ["
|
7
|
+
__all__ = ["live", "qpbanker",
|
8
8
|
"generate_all_qp_trades", "generate_all_pla_trades", "pareto_filter",
|
9
9
|
"speedpro_energy", "speedmap", "harveille_model",
|
10
10
|
"generate_historical_data"]
|
@@ -14,8 +14,7 @@ try:
|
|
14
14
|
except Exception: # pragma: no cover - best-effort version resolution
|
15
15
|
__version__ = "0.0.0"
|
16
16
|
|
17
|
-
from .live_odds import live_odds
|
18
17
|
from .processing import generate_all_qp_trades, generate_all_pla_trades, generate_historical_data
|
19
18
|
from .utils import pareto_filter
|
20
19
|
from .speedpro import speedmap, speedpro_energy
|
21
|
-
from . import harville_model
|
20
|
+
from . import harville_model, live
|
hkjc/historical.py
CHANGED
@@ -56,10 +56,9 @@ def _classify_running_style(df: pl.DataFrame, running_pos_col="RunningPosition")
|
|
56
56
|
|
57
57
|
df = df.with_columns([
|
58
58
|
(pl.col("StartPosition")-pl.col("FinishPosition")).alias("PositionChange"),
|
59
|
-
pl.mean_horizontal("StartPosition", "Position2",
|
60
|
-
"Position3", "FinishPosition").alias("AvgPosition"),
|
59
|
+
pl.mean_horizontal("StartPosition", "Position2").alias("AvgStartPosition"),
|
61
60
|
]).with_columns(pl.when(pl.col("StartPosition").is_null()).then(pl.lit("--"))
|
62
|
-
.when((pl.col("
|
61
|
+
.when((pl.col("AvgStartPosition") <= 3) & (pl.col("StartPosition") <= 3)).then(pl.lit("FrontRunner"))
|
63
62
|
.when((pl.col("PositionChange") >= 1) & (pl.col("StartPosition") >= 6)).then(pl.lit("Closer"))
|
64
63
|
.otherwise(pl.lit("Pacer")).alias("RunningStyle"))
|
65
64
|
|
hkjc/live.py
ADDED
@@ -0,0 +1,375 @@
|
|
1
|
+
"""Functions to fetch and process data from HKJC
|
2
|
+
"""
|
3
|
+
from __future__ import annotations
|
4
|
+
from typing import Tuple, List
|
5
|
+
|
6
|
+
import requests
|
7
|
+
from cachetools.func import ttl_cache
|
8
|
+
import numpy as np
|
9
|
+
|
10
|
+
from .utils import _validate_date, _validate_venue_code
|
11
|
+
|
12
|
+
HKJC_LIVEODDS_ENDPOINT = "https://info.cld.hkjc.com/graphql/base/"
|
13
|
+
|
14
|
+
RACEMTG_PAYLOAD = {
|
15
|
+
"operationName": "raceMeetings",
|
16
|
+
"variables": {"date": None, "venueCode": None},
|
17
|
+
"query": """
|
18
|
+
fragment raceFragment on Race {
|
19
|
+
id
|
20
|
+
no
|
21
|
+
status
|
22
|
+
raceName_en
|
23
|
+
raceName_ch
|
24
|
+
postTime
|
25
|
+
country_en
|
26
|
+
country_ch
|
27
|
+
distance
|
28
|
+
wageringFieldSize
|
29
|
+
go_en
|
30
|
+
go_ch
|
31
|
+
ratingType
|
32
|
+
raceTrack {
|
33
|
+
description_en
|
34
|
+
description_ch
|
35
|
+
}
|
36
|
+
raceCourse {
|
37
|
+
description_en
|
38
|
+
description_ch
|
39
|
+
displayCode
|
40
|
+
}
|
41
|
+
claCode
|
42
|
+
raceClass_en
|
43
|
+
raceClass_ch
|
44
|
+
judgeSigns {
|
45
|
+
value_en
|
46
|
+
}
|
47
|
+
}
|
48
|
+
|
49
|
+
fragment racingBlockFragment on RaceMeeting {
|
50
|
+
jpEsts: pmPools(
|
51
|
+
oddsTypes: [WIN, PLA, TCE, TRI, FF, QTT, DT, TT, SixUP]
|
52
|
+
filters: ["jackpot", "estimatedDividend"]
|
53
|
+
) {
|
54
|
+
leg {
|
55
|
+
number
|
56
|
+
races
|
57
|
+
}
|
58
|
+
oddsType
|
59
|
+
jackpot
|
60
|
+
estimatedDividend
|
61
|
+
mergedPoolId
|
62
|
+
}
|
63
|
+
poolInvs: pmPools(
|
64
|
+
oddsTypes: [WIN, PLA, QIN, QPL, CWA, CWB, CWC, IWN, FCT, TCE, TRI, FF, QTT, DBL, TBL, DT, TT, SixUP]
|
65
|
+
) {
|
66
|
+
id
|
67
|
+
leg {
|
68
|
+
races
|
69
|
+
}
|
70
|
+
}
|
71
|
+
penetrometerReadings(filters: ["first"]) {
|
72
|
+
reading
|
73
|
+
readingTime
|
74
|
+
}
|
75
|
+
hammerReadings(filters: ["first"]) {
|
76
|
+
reading
|
77
|
+
readingTime
|
78
|
+
}
|
79
|
+
changeHistories(filters: ["top3"]) {
|
80
|
+
type
|
81
|
+
time
|
82
|
+
raceNo
|
83
|
+
runnerNo
|
84
|
+
horseName_ch
|
85
|
+
horseName_en
|
86
|
+
jockeyName_ch
|
87
|
+
jockeyName_en
|
88
|
+
scratchHorseName_ch
|
89
|
+
scratchHorseName_en
|
90
|
+
handicapWeight
|
91
|
+
scrResvIndicator
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
query raceMeetings($date: String, $venueCode: String) {
|
96
|
+
timeOffset {
|
97
|
+
rc
|
98
|
+
}
|
99
|
+
activeMeetings: raceMeetings {
|
100
|
+
id
|
101
|
+
venueCode
|
102
|
+
date
|
103
|
+
status
|
104
|
+
races {
|
105
|
+
no
|
106
|
+
postTime
|
107
|
+
status
|
108
|
+
wageringFieldSize
|
109
|
+
}
|
110
|
+
}
|
111
|
+
raceMeetings(date: $date, venueCode: $venueCode) {
|
112
|
+
id
|
113
|
+
status
|
114
|
+
venueCode
|
115
|
+
date
|
116
|
+
totalNumberOfRace
|
117
|
+
currentNumberOfRace
|
118
|
+
dateOfWeek
|
119
|
+
meetingType
|
120
|
+
totalInvestment
|
121
|
+
country {
|
122
|
+
code
|
123
|
+
namech
|
124
|
+
nameen
|
125
|
+
seq
|
126
|
+
}
|
127
|
+
races {
|
128
|
+
...raceFragment
|
129
|
+
runners {
|
130
|
+
id
|
131
|
+
no
|
132
|
+
standbyNo
|
133
|
+
status
|
134
|
+
name_ch
|
135
|
+
name_en
|
136
|
+
horse {
|
137
|
+
id
|
138
|
+
code
|
139
|
+
}
|
140
|
+
color
|
141
|
+
barrierDrawNumber
|
142
|
+
handicapWeight
|
143
|
+
currentWeight
|
144
|
+
currentRating
|
145
|
+
internationalRating
|
146
|
+
gearInfo
|
147
|
+
racingColorFileName
|
148
|
+
allowance
|
149
|
+
trainerPreference
|
150
|
+
last6run
|
151
|
+
saddleClothNo
|
152
|
+
trumpCard
|
153
|
+
priority
|
154
|
+
finalPosition
|
155
|
+
deadHeat
|
156
|
+
winOdds
|
157
|
+
jockey {
|
158
|
+
code
|
159
|
+
name_en
|
160
|
+
name_ch
|
161
|
+
}
|
162
|
+
trainer {
|
163
|
+
code
|
164
|
+
name_en
|
165
|
+
name_ch
|
166
|
+
}
|
167
|
+
}
|
168
|
+
}
|
169
|
+
obSt: pmPools(oddsTypes: [WIN, PLA]) {
|
170
|
+
leg {
|
171
|
+
races
|
172
|
+
}
|
173
|
+
oddsType
|
174
|
+
comingleStatus
|
175
|
+
}
|
176
|
+
poolInvs: pmPools(
|
177
|
+
oddsTypes: [WIN, PLA, QIN, QPL, CWA, CWB, CWC, IWN, FCT, TCE, TRI, FF, QTT, DBL, TBL, DT, TT, SixUP]
|
178
|
+
) {
|
179
|
+
id
|
180
|
+
leg {
|
181
|
+
number
|
182
|
+
races
|
183
|
+
}
|
184
|
+
status
|
185
|
+
sellStatus
|
186
|
+
oddsType
|
187
|
+
investment
|
188
|
+
mergedPoolId
|
189
|
+
lastUpdateTime
|
190
|
+
}
|
191
|
+
...racingBlockFragment
|
192
|
+
pmPools(oddsTypes: []) {
|
193
|
+
id
|
194
|
+
}
|
195
|
+
jkcInstNo: foPools(oddsTypes: [JKC], filters: ["top"]) {
|
196
|
+
instNo
|
197
|
+
}
|
198
|
+
tncInstNo: foPools(oddsTypes: [TNC], filters: ["top"]) {
|
199
|
+
instNo
|
200
|
+
}
|
201
|
+
}
|
202
|
+
}
|
203
|
+
"""}
|
204
|
+
|
205
|
+
LIVEODDS_PAYLOAD = {
|
206
|
+
"operationName": "racing",
|
207
|
+
"variables": {"date": None, "venueCode": None, "raceNo": None, "oddsTypes": None},
|
208
|
+
"query": """
|
209
|
+
query racing($date: String, $venueCode: String, $oddsTypes: [OddsType], $raceNo: Int) {
|
210
|
+
raceMeetings(date: $date, venueCode: $venueCode) {
|
211
|
+
pmPools(oddsTypes: $oddsTypes, raceNo: $raceNo) {
|
212
|
+
id
|
213
|
+
status
|
214
|
+
sellStatus
|
215
|
+
oddsType
|
216
|
+
lastUpdateTime
|
217
|
+
guarantee
|
218
|
+
minTicketCost
|
219
|
+
name_en
|
220
|
+
name_ch
|
221
|
+
leg {
|
222
|
+
number
|
223
|
+
races
|
224
|
+
}
|
225
|
+
cWinSelections {
|
226
|
+
composite
|
227
|
+
name_ch
|
228
|
+
name_en
|
229
|
+
starters
|
230
|
+
}
|
231
|
+
oddsNodes {
|
232
|
+
combString
|
233
|
+
oddsValue
|
234
|
+
hotFavourite
|
235
|
+
oddsDropValue
|
236
|
+
bankerOdds {
|
237
|
+
combString
|
238
|
+
oddsValue
|
239
|
+
}
|
240
|
+
}
|
241
|
+
}
|
242
|
+
}
|
243
|
+
}""",
|
244
|
+
}
|
245
|
+
|
246
|
+
|
247
|
+
@ttl_cache(maxsize=12, ttl=1000)
|
248
|
+
def _fetch_live_races(date: str, venue_code: str) -> dict:
|
249
|
+
"""Fetch live race data from HKJC GraphQL endpoint."""
|
250
|
+
payload = RACEMTG_PAYLOAD.copy()
|
251
|
+
payload["variables"] = payload["variables"].copy()
|
252
|
+
payload["variables"]["date"] = date
|
253
|
+
payload["variables"]["venueCode"] = venue_code
|
254
|
+
|
255
|
+
headers = {
|
256
|
+
"Origin": "https://bet.hkjc.com",
|
257
|
+
"Referer": "https://bet.hkjc.com",
|
258
|
+
"Content-Type": "application/json",
|
259
|
+
"Accept": "application/json",
|
260
|
+
"User-Agent": "python-hkjc-fetch/0.1",
|
261
|
+
}
|
262
|
+
|
263
|
+
r = requests.post(HKJC_LIVEODDS_ENDPOINT, json=payload,
|
264
|
+
headers=headers, timeout=10)
|
265
|
+
if r.status_code != 200:
|
266
|
+
raise RuntimeError(f"Request failed: {r.status_code} - {r.text}")
|
267
|
+
|
268
|
+
races = r.json()['data']['raceMeetings'][0]['races']
|
269
|
+
|
270
|
+
race_info = {}
|
271
|
+
for race in races:
|
272
|
+
race_num = race['no']
|
273
|
+
race_name = race['raceName_en']
|
274
|
+
race_dist = race['distance']
|
275
|
+
race_going = race['go_en']
|
276
|
+
race_track = race['raceTrack']['description_en']
|
277
|
+
race_class = race['raceClass_en']
|
278
|
+
race_course = race['raceCourse']['displayCode']
|
279
|
+
|
280
|
+
runners = [{'Dr': runner['barrierDrawNumber'],
|
281
|
+
'Rtg' : int(runner['currentRating']),
|
282
|
+
'Wt' : int(runner['currentWeight']),
|
283
|
+
'HorseNo': runner['horse']['code']
|
284
|
+
} for runner in race['runners']]
|
285
|
+
race_info[race_num]={
|
286
|
+
'No': race_num,
|
287
|
+
'Name': race_name,
|
288
|
+
'Class': race_class,
|
289
|
+
'Course': race_course,
|
290
|
+
'Dist': race_dist,
|
291
|
+
'Going': race_going,
|
292
|
+
'Track': race_track,
|
293
|
+
'Runners': runners
|
294
|
+
}
|
295
|
+
return race_info
|
296
|
+
|
297
|
+
|
298
|
+
@ttl_cache(maxsize=12, ttl=30)
|
299
|
+
def _fetch_live_odds(date: str, venue_code: str, race_number: int, odds_type: Tuple[str] = ('PLA', 'QPL')) -> List[dict]:
|
300
|
+
"""Fetch live odds data from HKJC GraphQL endpoint."""
|
301
|
+
payload = LIVEODDS_PAYLOAD.copy()
|
302
|
+
payload["variables"] = payload["variables"].copy()
|
303
|
+
payload["variables"]["date"] = date
|
304
|
+
payload["variables"]["venueCode"] = venue_code
|
305
|
+
payload["variables"]["raceNo"] = race_number
|
306
|
+
payload["variables"]["oddsTypes"] = odds_type
|
307
|
+
|
308
|
+
headers = {
|
309
|
+
"Origin": "https://bet.hkjc.com",
|
310
|
+
"Referer": "https://bet.hkjc.com",
|
311
|
+
"Content-Type": "application/json",
|
312
|
+
"Accept": "application/json",
|
313
|
+
"User-Agent": "python-hkjc-fetch/0.1",
|
314
|
+
}
|
315
|
+
|
316
|
+
r = requests.post(HKJC_LIVEODDS_ENDPOINT, json=payload,
|
317
|
+
headers=headers, timeout=10)
|
318
|
+
if r.status_code != 200:
|
319
|
+
raise RuntimeError(f"Request failed: {r.status_code} - {r.text}")
|
320
|
+
|
321
|
+
meetings = r.json().get("data", {}).get("raceMeetings", [])
|
322
|
+
|
323
|
+
return [
|
324
|
+
{"HorseID": node["combString"], "Type": pool.get(
|
325
|
+
"oddsType"), "Odds": float(node["oddsValue"])}
|
326
|
+
for meeting in meetings
|
327
|
+
for pool in meeting.get("pmPools", [])
|
328
|
+
for node in pool.get("oddsNodes", [])
|
329
|
+
]
|
330
|
+
|
331
|
+
|
332
|
+
def live_odds(date: str, venue_code: str, race_number: int, odds_type: List[str] = ['PLA', 'QPL']) -> dict:
|
333
|
+
"""Fetch live odds as numpy arrays.
|
334
|
+
|
335
|
+
Args:
|
336
|
+
date (str): Date in 'YYYY-MM-DD' format.
|
337
|
+
venue_code (str): Venue code, e.g., 'ST' for Shatin, 'HV' for Happy Valley.
|
338
|
+
race_number (int): Race number.
|
339
|
+
odds_type (List[str]): Types of odds to fetch. Default is ['PLA', 'QPL']. Currently the following types are supported:
|
340
|
+
- 'WIN': Win odds
|
341
|
+
- 'PLA': Place odds
|
342
|
+
- 'QIN': Quinella odds
|
343
|
+
- 'QPL': Quinella Place odds
|
344
|
+
fit_harville (bool): Whether to fit the odds using Harville model. Default is False.
|
345
|
+
|
346
|
+
Returns:
|
347
|
+
dict: Dictionary with keys as odds types and values as numpy arrays containing the odds.
|
348
|
+
If odds_type is 'WIN','PLA', returns a 1D array of place odds.
|
349
|
+
If odds_type is 'QIN','QPL', returns a 2D array of quinella place odds.
|
350
|
+
"""
|
351
|
+
_validate_date(date)
|
352
|
+
_validate_venue_code(venue_code)
|
353
|
+
|
354
|
+
race_info = _fetch_live_races(date, venue_code)
|
355
|
+
N = len(race_info[race_number]['Runners'])
|
356
|
+
|
357
|
+
data = _fetch_live_odds(date, venue_code, race_number,
|
358
|
+
odds_type=tuple(odds_type))
|
359
|
+
|
360
|
+
odds = {'WIN': np.full(N, np.nan, dtype=float),
|
361
|
+
'PLA': np.full(N, np.nan, dtype=float),
|
362
|
+
'QIN': np.full((N, N), np.nan, dtype=float),
|
363
|
+
'QPL': np.full((N, N), np.nan, dtype=float)}
|
364
|
+
|
365
|
+
for entry in data:
|
366
|
+
if entry["Type"] in ["QIN", "QPL"]:
|
367
|
+
horse_ids = list(map(int, entry["HorseID"].split(",")))
|
368
|
+
odds[entry["Type"]][horse_ids[0] - 1,
|
369
|
+
horse_ids[1] - 1] = entry["Odds"]
|
370
|
+
odds[entry["Type"]][horse_ids[1] - 1,
|
371
|
+
horse_ids[0] - 1] = entry["Odds"]
|
372
|
+
elif entry["Type"] in ["PLA", "WIN"]:
|
373
|
+
odds[entry["Type"]][int(entry["HorseID"]) - 1] = entry["Odds"]
|
374
|
+
|
375
|
+
return {t: odds[t] for t in odds_type}
|
hkjc/processing.py
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
from __future__ import annotations
|
4
4
|
from typing import Tuple, List, Union
|
5
5
|
|
6
|
-
from .
|
6
|
+
from .live import live_odds
|
7
7
|
from .strategy import qpbanker, place_only
|
8
8
|
from .harville_model import fit_harville_to_odds
|
9
9
|
from .historical import _extract_horse_data, _extract_race_data, _clean_horse_data
|
@@ -1,12 +1,12 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hkjc
|
3
|
-
Version: 0.3.
|
3
|
+
Version: 0.3.18
|
4
4
|
Summary: Library for scrapping HKJC data and perform basic analysis
|
5
5
|
Requires-Python: >=3.11
|
6
6
|
Requires-Dist: beautifulsoup4>=4.14.2
|
7
7
|
Requires-Dist: cachetools>=6.2.0
|
8
8
|
Requires-Dist: fastexcel>=0.16.0
|
9
|
-
Requires-Dist:
|
9
|
+
Requires-Dist: flask>=3.1.2
|
10
10
|
Requires-Dist: numba>=0.62.1
|
11
11
|
Requires-Dist: numpy>=2.3.3
|
12
12
|
Requires-Dist: polars>=1.33.1
|
@@ -1,14 +1,14 @@
|
|
1
|
-
hkjc/__init__.py,sha256=
|
1
|
+
hkjc/__init__.py,sha256=5A9MzcITYJDcA2UbIBpkimZBYSqS4pgRuQJhTagOfpE,753
|
2
2
|
hkjc/analysis.py,sha256=0042_NMIkQCl0J6B0P4TFfrBDCnm2B6jsCZKOEO30yI,108
|
3
3
|
hkjc/harville_model.py,sha256=MZjPLS-1nbEhp1d4Syuq13DtraKnd7TlNqBmOOCwxgc,15976
|
4
|
-
hkjc/historical.py,sha256=
|
5
|
-
hkjc/
|
6
|
-
hkjc/processing.py,sha256=
|
4
|
+
hkjc/historical.py,sha256=v9k_R47Na5en5ftrocjIHofkNAUthE_lp4CyLaCTsQE,8280
|
5
|
+
hkjc/live.py,sha256=GqctH-BVdIL6Vi1g8XHe3p8fZBopCQf5KACLAR0meP0,10249
|
6
|
+
hkjc/processing.py,sha256=H0chtW_FBMMhK3IzcjYjrryd3fAPYimanc2fWuGiB0M,6807
|
7
7
|
hkjc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
8
|
hkjc/speedpro.py,sha256=Y2Z3GYGeePc4sM-ZnCHXCI1N7L-_j9nrMqS3CC5BBSo,2031
|
9
9
|
hkjc/utils.py,sha256=4CA_FPf_U3GvzoLkqBX0qDPZgrSvKJKvbP7VWqd5FiA,6323
|
10
10
|
hkjc/strategy/place_only.py,sha256=lHPjTSj8PzghxncNBg8FI4T4HJigekB9a3bV7l7VtPA,2079
|
11
11
|
hkjc/strategy/qpbanker.py,sha256=MQxjwsfhllKZroKS8w8Q3bi3HMjGc1DAyBIjNZAp3yQ,4805
|
12
|
-
hkjc-0.3.
|
13
|
-
hkjc-0.3.
|
14
|
-
hkjc-0.3.
|
12
|
+
hkjc-0.3.18.dist-info/METADATA,sha256=aoXp6Fvn3EkuXyv6p5LClSbZa5XS_bfcUxMKBJXcNvw,480
|
13
|
+
hkjc-0.3.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
14
|
+
hkjc-0.3.18.dist-info/RECORD,,
|
hkjc/live_odds.py
DELETED
@@ -1,136 +0,0 @@
|
|
1
|
-
"""Functions to fetch and process data from HKJC
|
2
|
-
"""
|
3
|
-
from __future__ import annotations
|
4
|
-
from typing import Tuple, List
|
5
|
-
|
6
|
-
import requests
|
7
|
-
from cachetools.func import ttl_cache
|
8
|
-
import numpy as np
|
9
|
-
|
10
|
-
from .utils import _validate_date, _validate_venue_code
|
11
|
-
|
12
|
-
HKJC_LIVEODDS_ENDPOINT = "https://info.cld.hkjc.com/graphql/base/"
|
13
|
-
|
14
|
-
LIVEODDS_PAYLOAD = {
|
15
|
-
"operationName": "racing",
|
16
|
-
"variables": {"date": None, "venueCode": None, "raceNo": None, "oddsTypes": None},
|
17
|
-
"query": """
|
18
|
-
query racing($date: String, $venueCode: String, $oddsTypes: [OddsType], $raceNo: Int) {
|
19
|
-
raceMeetings(date: $date, venueCode: $venueCode) {
|
20
|
-
pmPools(oddsTypes: $oddsTypes, raceNo: $raceNo) {
|
21
|
-
id
|
22
|
-
status
|
23
|
-
sellStatus
|
24
|
-
oddsType
|
25
|
-
lastUpdateTime
|
26
|
-
guarantee
|
27
|
-
minTicketCost
|
28
|
-
name_en
|
29
|
-
name_ch
|
30
|
-
leg {
|
31
|
-
number
|
32
|
-
races
|
33
|
-
}
|
34
|
-
cWinSelections {
|
35
|
-
composite
|
36
|
-
name_ch
|
37
|
-
name_en
|
38
|
-
starters
|
39
|
-
}
|
40
|
-
oddsNodes {
|
41
|
-
combString
|
42
|
-
oddsValue
|
43
|
-
hotFavourite
|
44
|
-
oddsDropValue
|
45
|
-
bankerOdds {
|
46
|
-
combString
|
47
|
-
oddsValue
|
48
|
-
}
|
49
|
-
}
|
50
|
-
}
|
51
|
-
}
|
52
|
-
}""",
|
53
|
-
}
|
54
|
-
|
55
|
-
|
56
|
-
@ttl_cache(maxsize=12, ttl=30)
|
57
|
-
def _fetch_live_odds(date: str, venue_code: str, race_number: int, odds_type: Tuple[str] = ('PLA', 'QPL')) -> Tuple[dict]:
|
58
|
-
"""Fetch live odds data from HKJC GraphQL endpoint."""
|
59
|
-
payload = LIVEODDS_PAYLOAD.copy()
|
60
|
-
payload["variables"] = payload["variables"].copy()
|
61
|
-
payload["variables"]["date"] = date
|
62
|
-
payload["variables"]["venueCode"] = venue_code
|
63
|
-
payload["variables"]["raceNo"] = race_number
|
64
|
-
payload["variables"]["oddsTypes"] = odds_type
|
65
|
-
|
66
|
-
headers = {
|
67
|
-
"Origin": "https://bet.hkjc.com",
|
68
|
-
"Referer": "https://bet.hkjc.com",
|
69
|
-
"Content-Type": "application/json",
|
70
|
-
"Accept": "application/json",
|
71
|
-
"User-Agent": "python-hkjc-fetch/0.1",
|
72
|
-
}
|
73
|
-
|
74
|
-
r = requests.post(HKJC_LIVEODDS_ENDPOINT, json=payload,
|
75
|
-
headers=headers, timeout=10)
|
76
|
-
if r.status_code != 200:
|
77
|
-
raise RuntimeError(f"Request failed: {r.status_code} - {r.text}")
|
78
|
-
|
79
|
-
meetings = r.json().get("data", {}).get("raceMeetings", [])
|
80
|
-
|
81
|
-
return [
|
82
|
-
{"HorseID": node["combString"], "Type": pool.get(
|
83
|
-
"oddsType"), "Odds": float(node["oddsValue"])}
|
84
|
-
for meeting in meetings
|
85
|
-
for pool in meeting.get("pmPools", [])
|
86
|
-
for node in pool.get("oddsNodes", [])
|
87
|
-
]
|
88
|
-
|
89
|
-
|
90
|
-
def live_odds(date: str, venue_code: str, race_number: int, odds_type: List[str] = ['PLA', 'QPL']) -> dict:
|
91
|
-
"""Fetch live odds as numpy arrays.
|
92
|
-
|
93
|
-
Args:
|
94
|
-
date (str): Date in 'YYYY-MM-DD' format.
|
95
|
-
venue_code (str): Venue code, e.g., 'ST' for Shatin, 'HV' for Happy Valley.
|
96
|
-
race_number (int): Race number.
|
97
|
-
odds_type (List[str]): Types of odds to fetch. Default is ['PLA', 'QPL']. Currently the following types are supported:
|
98
|
-
- 'WIN': Win odds
|
99
|
-
- 'PLA': Place odds
|
100
|
-
- 'QIN': Quinella odds
|
101
|
-
- 'QPL': Quinella Place odds
|
102
|
-
fit_harville (bool): Whether to fit the odds using Harville model. Default is False.
|
103
|
-
|
104
|
-
Returns:
|
105
|
-
dict: Dictionary with keys as odds types and values as numpy arrays containing the odds.
|
106
|
-
If odds_type is 'WIN','PLA', returns a 1D array of place odds.
|
107
|
-
If odds_type is 'QIN','QPL', returns a 2D array of quinella place odds.
|
108
|
-
"""
|
109
|
-
_validate_date(date)
|
110
|
-
_validate_venue_code(venue_code)
|
111
|
-
|
112
|
-
mandatory_types = ['PLA']
|
113
|
-
|
114
|
-
data = _fetch_live_odds(date, venue_code, race_number,
|
115
|
-
odds_type=tuple(set(mandatory_types+odds_type)))
|
116
|
-
|
117
|
-
# use place odds to determine number of horses
|
118
|
-
pla_data = [entry for entry in data if entry["Type"] == "PLA"]
|
119
|
-
N = len(pla_data)
|
120
|
-
|
121
|
-
odds = {'WIN': np.full(N, np.nan, dtype=float),
|
122
|
-
'PLA': np.full(N, np.nan, dtype=float),
|
123
|
-
'QIN': np.full((N, N), np.nan, dtype=float),
|
124
|
-
'QPL': np.full((N, N), np.nan, dtype=float)}
|
125
|
-
|
126
|
-
for entry in data:
|
127
|
-
if entry["Type"] in ["QIN", "QPL"]:
|
128
|
-
horse_ids = list(map(int, entry["HorseID"].split(",")))
|
129
|
-
odds[entry["Type"]][horse_ids[0] - 1,
|
130
|
-
horse_ids[1] - 1] = entry["Odds"]
|
131
|
-
odds[entry["Type"]][horse_ids[1] - 1,
|
132
|
-
horse_ids[0] - 1] = entry["Odds"]
|
133
|
-
elif entry["Type"] in ["PLA", "WIN"]:
|
134
|
-
odds[entry["Type"]][int(entry["HorseID"]) - 1] = entry["Odds"]
|
135
|
-
|
136
|
-
return {t: odds[t] for t in odds_type}
|
File without changes
|