hkjc 0.3.16__tar.gz → 0.3.18__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.3.18/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ backend/
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hkjc
3
- Version: 0.3.16
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: joblib>=1.5.2
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
hkjc-0.3.18/process.py ADDED
@@ -0,0 +1,4 @@
1
+ from hkjc import generate_historical_data
2
+
3
+ df = generate_historical_data('2024-09-01', '2025-10-06')
4
+ df.write_parquet('hkjc2425.parquet')
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hkjc"
3
- version = "0.3.16"
3
+ version = "0.3.18"
4
4
  description = "Library for scrapping HKJC data and perform basic analysis"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -8,7 +8,7 @@ dependencies = [
8
8
  "beautifulsoup4>=4.14.2",
9
9
  "cachetools>=6.2.0",
10
10
  "fastexcel>=0.16.0",
11
- "joblib>=1.5.2",
11
+ "flask>=3.1.2",
12
12
  "numba>=0.62.1",
13
13
  "numpy>=2.3.3",
14
14
  "polars>=1.33.1",
@@ -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__ = ["live_odds", "qpbanker",
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
@@ -12,6 +12,9 @@ from .utils import _parse_html_table
12
12
  HKJC_RACE_URL_TEMPLATE = "https://racing.hkjc.com/racing/information/English/Racing/LocalResults.aspx?RaceDate={date}&Racecourse={venue_code}&RaceNo={race_number}"
13
13
  HKJC_HORSE_URL_TEMPLATE = "https://racing.hkjc.com/racing/information/English/Horse/Horse.aspx?HorseNo={horse_no}"
14
14
 
15
+ incidents = ['DISQ', 'DNF', 'FE', 'ML', 'PU', 'TNP', 'TO',
16
+ 'UR', 'VOID', 'WR', 'WV', 'WV-A', 'WX', 'WX-A', 'WXNR']
17
+
15
18
 
16
19
  @ttl_cache(maxsize=100, ttl=3600)
17
20
  def _soupify(url: str) -> BeautifulSoup:
@@ -37,7 +40,6 @@ def _soupify_horse_page(horse_no: str) -> BeautifulSoup:
37
40
  return _soupify(url)
38
41
 
39
42
 
40
-
41
43
  def _classify_running_style(df: pl.DataFrame, running_pos_col="RunningPosition") -> pl.DataFrame:
42
44
  """Classify running style based on RunningPosition column
43
45
  """
@@ -50,14 +52,13 @@ def _classify_running_style(df: pl.DataFrame, running_pos_col="RunningPosition")
50
52
  .alias("split_data").cast(pl.Int64, strict=False)
51
53
  ).unnest("split_data")
52
54
 
53
- df.with_columns(pl.col('FinishPosition').fill_null(pl.col('Position3')))
55
+ df = df.with_columns(pl.col('FinishPosition').fill_null(pl.col('Position3')))
54
56
 
55
57
  df = df.with_columns([
56
58
  (pl.col("StartPosition")-pl.col("FinishPosition")).alias("PositionChange"),
57
- pl.mean_horizontal("StartPosition", "Position2",
58
- "Position3", "FinishPosition").alias("AvgPosition"),
59
+ pl.mean_horizontal("StartPosition", "Position2").alias("AvgStartPosition"),
59
60
  ]).with_columns(pl.when(pl.col("StartPosition").is_null()).then(pl.lit("--"))
60
- .when((pl.col("PositionChange") <= 0) & pl.col("StartPosition") <= 3).then(pl.lit("FrontRunner"))
61
+ .when((pl.col("AvgStartPosition") <= 3) & (pl.col("StartPosition") <= 3)).then(pl.lit("FrontRunner"))
61
62
  .when((pl.col("PositionChange") >= 1) & (pl.col("StartPosition") >= 6)).then(pl.lit("Closer"))
62
63
  .otherwise(pl.lit("Pacer")).alias("RunningStyle"))
63
64
 
@@ -67,7 +68,7 @@ def _classify_running_style(df: pl.DataFrame, running_pos_col="RunningPosition")
67
68
  return df
68
69
 
69
70
 
70
- def get_horse_data(horse_no: str) -> pl.DataFrame:
71
+ def _extract_horse_data(horse_no: str) -> pl.DataFrame:
71
72
  """Extract horse info and history from horse page
72
73
  """
73
74
  soup = _soupify_horse_page(horse_no)
@@ -82,13 +83,16 @@ def get_horse_data(horse_no: str) -> pl.DataFrame:
82
83
  profile_data = _parse_html_table(table[1], skip_header=True)
83
84
 
84
85
  try:
85
- current_rating = int(profile_data.filter(pl.col("column_0").str.starts_with("Current Rating"))['column_2'].item(0))
86
- season_start_rating = int(profile_data.filter(pl.col("column_0").str.starts_with("Start of Season Rating"))['column_2'].item(0))
86
+ current_rating = int(profile_data.filter(
87
+ pl.col("column_0").str.starts_with("Current Rating"))['column_2'].item(0))
88
+ season_start_rating = int(profile_data.filter(pl.col(
89
+ "column_0").str.starts_with("Start of Season Rating"))['column_2'].item(0))
87
90
  except:
88
91
  current_rating, season_start_rating = 0, 0
89
-
92
+
90
93
  try:
91
- last_rating = int(profile_data.filter(pl.col("column_0").str.starts_with("Last Rating"))['column_2'].item(0))
94
+ last_rating = int(profile_data.filter(
95
+ pl.col("column_0").str.starts_with("Last Rating"))['column_2'].item(0))
92
96
  except:
93
97
  last_rating = 0
94
98
 
@@ -96,47 +100,85 @@ def get_horse_data(horse_no: str) -> pl.DataFrame:
96
100
  'HorseID': horse_no,
97
101
  'CurrentRating': current_rating,
98
102
  'SeasonStartRating': season_start_rating,
99
- 'LastRating' : last_rating if current_rating==0 else current_rating
103
+ 'LastRating': last_rating if current_rating == 0 else current_rating
100
104
  }
101
105
  horse_data = (horse_data.with_columns([
102
106
  pl.lit(value).alias(key) for key, value in horse_info.items()
103
107
  ])
104
108
  )
105
109
 
106
- horse_data = horse_data.with_columns([
110
+ return horse_data
111
+
112
+
113
+ def _clean_horse_data(df: pl.DataFrame) -> pl.DataFrame:
114
+ """ Clean and convert horse data to suitable data types
115
+ """
116
+ df = df.with_columns(
117
+ pl.col('Pla').str.split(' ').list.first().alias('Pla')
118
+ ).filter(~pl.col('Pla').is_in(incidents))
119
+
120
+ df = df.with_columns([
107
121
  pl.col('Pla').cast(pl.Int64, strict=False),
108
- pl.col('WinOdds').cast(pl.Int64, strict=False),
109
122
  pl.col('ActWt').cast(pl.Int64, strict=False),
110
123
  pl.col('DeclarHorseWt').cast(pl.Int64, strict=False),
111
124
  pl.col('Dr').cast(pl.Int64, strict=False),
112
125
  pl.col('Rtg').cast(pl.Int64, strict=False),
113
- pl.col('RaceIndex').cast(pl.Int64, strict=False),
114
- pl.col('Dist').cast(pl.Int64, strict=False)
126
+ pl.col('Dist').cast(pl.Int64, strict=False),
127
+ pl.col('WinOdds').cast(pl.Float64, strict=False),
128
+ pl.col('RaceIndex').cast(pl.Int64, strict=False)
115
129
  ])
116
130
 
117
- horse_data = horse_data.with_columns(
131
+ df = df.with_columns(
118
132
  (
119
- pl.col("FinishTime").str.split(":").list.get(0).cast(pl.Int64) * 60 +
120
- pl.col("FinishTime").str.split(":").list.get(1).cast(pl.Float64)
133
+ pl.col("FinishTime").str.split_exact(".", 1).struct.field("field_0").cast(pl.Int64) * 60 +
134
+ pl.col("FinishTime").str.split_exact(".", 1).struct.field("field_1").cast(pl.Int64)
121
135
  ).cast(pl.Float64).alias("FinishTime")
122
136
  )
123
137
 
124
- horse_data = horse_data.with_columns(
138
+ df = df.with_columns(
125
139
  pl.col('RCTrackCourse').str.split_exact(' / ', 2)
126
140
  .struct.rename_fields(['Venue', 'Track', 'Course'])
127
141
  .alias('RCTrackCourse')
128
142
  ).unnest('RCTrackCourse')
129
143
 
130
- return horse_data
144
+ return df
131
145
 
146
+ def get_horse_data(horse_no: str) -> pl.DataFrame:
147
+ df = _extract_horse_data(horse_no)
148
+ return _clean_horse_data(df)
132
149
 
133
- def get_race_data(date: str, venue_code: str, race_number: int) -> pl.DataFrame:
150
+ def _clean_race_data(df: pl.DataFrame) -> pl.DataFrame:
151
+ """ Clean and convert horse data to suitable data types
152
+ """
153
+ df = df.with_columns(
154
+ pl.col('Pla').str.split(' ').list.first().alias('Pla')
155
+ ).filter(~pl.col('Pla').is_in(incidents))
156
+
157
+ df = df.with_columns([
158
+ pl.col('Pla').cast(pl.Int64, strict=False),
159
+ pl.col('HorseNo').cast(pl.Int64, strict=False),
160
+ pl.col('ActWt').cast(pl.Int64, strict=False),
161
+ pl.col('DeclarHorseWt').cast(pl.Int64, strict=False),
162
+ pl.col('Dr').cast(pl.Int64, strict=False),
163
+ pl.col('WinOdds').cast(pl.Float64, strict=False)
164
+ ])
165
+
166
+ df = df.with_columns(
167
+ (
168
+ pl.col("FinishTime").str.split_exact(":", 1).struct.field("field_0").cast(pl.Int64) * 60 +
169
+ pl.col("FinishTime").str.split_exact(":", 1).struct.field("field_1").cast(pl.Int64)
170
+ ).cast(pl.Float64).alias("FinishTime")
171
+ )
172
+
173
+ return df
174
+
175
+ def _extract_race_data(date: str, venue_code: str, race_number: int) -> pl.DataFrame:
134
176
  soup = _soupify_race_page(date, venue_code, race_number)
135
177
  table = soup.find('div', class_='race_tab').find('table')
136
178
  race_data = _parse_html_table(table)
137
179
 
138
180
  # Extract the relevant race information
139
- race_id = race_data.columns[0].replace(f'RACE{race_number}','')
181
+ race_id = race_data.columns[0].replace(f'RACE{race_number}', '')
140
182
  race_class = race_data.item(1, 0).split('-')[0].strip()
141
183
  race_dist = race_data.item(1, 0).split('-')[1].strip().rstrip('M')
142
184
  race_name = race_data.item(2, 0).strip()
@@ -162,7 +204,12 @@ def get_race_data(date: str, venue_code: str, race_number: int) -> pl.DataFrame:
162
204
  .with_columns(
163
205
  pl.col("Horse").str.extract(r"\((.*?)\)")
164
206
  .alias("HorseID")
165
- )
166
- )
207
+ )
208
+ )
209
+
210
+ return race_data
167
211
 
168
- return race_data
212
+
213
+ def get_race_data(date: str, venue_code: str, race_number: int) -> pl.DataFrame:
214
+ df = _extract_race_data(date,venue_code,race_number)
215
+ return _clean_race_data(df)
@@ -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}
@@ -3,10 +3,10 @@
3
3
  from __future__ import annotations
4
4
  from typing import Tuple, List, Union
5
5
 
6
- from .live_odds import live_odds
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
- from .historical import get_race_data, get_horse_data
9
+ from .historical import _extract_horse_data, _extract_race_data, _clean_horse_data
10
10
  from .utils import _validate_date
11
11
 
12
12
  import polars as pl
@@ -23,8 +23,7 @@ def _all_subsets(lst): return [list(x) for r in range(
23
23
  # ======================================
24
24
  # Historical data processing functions
25
25
  # ======================================
26
- incidents = ['DISQ', 'DNF', 'FE', 'ML', 'PU', 'TNP', 'TO',
27
- 'UR', 'VOID', 'WR', 'WV', 'WV-A', 'WX', 'WX-A', 'WXNR']
26
+
28
27
 
29
28
 
30
29
  def _historical_process_single_date_venue(date: str, venue_code: str) -> List[pl.DataFrame]:
@@ -33,7 +32,7 @@ def _historical_process_single_date_venue(date: str, venue_code: str) -> List[pl
33
32
  range(1, 12), desc=f"Processing {date} {venue_code} ...", leave=False)
34
33
  for race_number in iter_date:
35
34
  try:
36
- dfs.append(get_race_data(date.strftime('%Y/%m/%d'),
35
+ dfs.append(_extract_race_data(date.strftime('%Y/%m/%d'),
37
36
  venue_code, race_number))
38
37
  except:
39
38
  if race_number == 1:
@@ -51,7 +50,7 @@ def generate_historical_data(start_date: str, end_date: str) -> pl.DataFrame:
51
50
 
52
51
  dfs = []
53
52
 
54
- for date in tqdm(pl.date_range(start_dt, end_dt, interval='1d', eager=True)):
53
+ for date in tqdm(pl.date_range(start_dt, end_dt, interval='1d', eager=True), leave=False, desc='Scanning for horse IDs ...'):
55
54
  for venue_code in ['ST', 'HV']:
56
55
  dfs += _historical_process_single_date_venue(date, venue_code)
57
56
 
@@ -62,35 +61,9 @@ def generate_historical_data(start_date: str, end_date: str) -> pl.DataFrame:
62
61
  horse_ids = pl.concat(dfs)['HorseID'].unique()
63
62
 
64
63
  # Use horse track records
65
- dfs = [get_horse_data(horse_id) for horse_id in horse_ids]
66
- df = (
67
- pl.concat(dfs).with_columns(
68
- pl.col('Date').str.strptime(pl.Date, '%m/%d/%y')
69
- ).filter(pl.col('Date').is_between(start_dt, end_dt))
70
- .filter(~pl.col('Pla').is_in(incidents))
71
- .with_columns(
72
- pl.col('Pla').str.split(' ').list.first().alias('Pla')
73
- )
74
- )
75
-
76
- df = df.with_columns([
77
- pl.col('Pla').cast(pl.Int64, strict=False),
78
- pl.col('HorseNo').cast(pl.Int64, strict=False),
79
- pl.col('ActWt').cast(pl.Int64, strict=False),
80
- pl.col('DeclarHorseWt').cast(pl.Int64, strict=False),
81
- pl.col('Dr').cast(pl.Int64, strict=False),
82
- pl.col('RaceDistance').cast(pl.Int64, strict=False),
83
- pl.col('WinOdds').cast(pl.Float64, strict=False)
84
- ])
85
-
86
- df = df.with_columns(
87
- (
88
- pl.col("FinishTime").str.split(":").list.get(0).cast(pl.Int64) * 60 +
89
- pl.col("FinishTime").str.split(":").list.get(1).cast(pl.Float64)
90
- ).cast(pl.Float64).alias("FinishTime")
91
- )
92
-
93
- return df
64
+ dfs = [_extract_horse_data(horse_id) for horse_id in tqdm(horse_ids, desc='Processing horses ...', leave=False)]
65
+ df = pl.concat(dfs)
66
+ return _clean_horse_data(df)
94
67
 
95
68
 
96
69
  # ==========================
@@ -15,6 +15,15 @@ wheels = [
15
15
  { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
16
16
  ]
17
17
 
18
+ [[package]]
19
+ name = "blinker"
20
+ version = "1.9.0"
21
+ source = { registry = "https://pypi.org/simple" }
22
+ sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
23
+ wheels = [
24
+ { url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
25
+ ]
26
+
18
27
  [[package]]
19
28
  name = "cachetools"
20
29
  version = "6.2.0"
@@ -86,6 +95,18 @@ wheels = [
86
95
  { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" },
87
96
  ]
88
97
 
98
+ [[package]]
99
+ name = "click"
100
+ version = "8.3.0"
101
+ source = { registry = "https://pypi.org/simple" }
102
+ dependencies = [
103
+ { name = "colorama", marker = "sys_platform == 'win32'" },
104
+ ]
105
+ sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
106
+ wheels = [
107
+ { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
108
+ ]
109
+
89
110
  [[package]]
90
111
  name = "colorama"
91
112
  version = "0.4.6"
@@ -108,15 +129,32 @@ wheels = [
108
129
  { url = "https://files.pythonhosted.org/packages/9c/2e/805c2d0e799710e4937d084d9c37821bafa129eda1de62c3279a042ca56d/fastexcel-0.16.0-cp39-abi3-win_amd64.whl", hash = "sha256:04c2b6fea7292e26d76a458f9095f4ec260c864c90be7a7161d20ca81cf77fd8", size = 2819876, upload-time = "2025-09-22T12:34:38.716Z" },
109
130
  ]
110
131
 
132
+ [[package]]
133
+ name = "flask"
134
+ version = "3.1.2"
135
+ source = { registry = "https://pypi.org/simple" }
136
+ dependencies = [
137
+ { name = "blinker" },
138
+ { name = "click" },
139
+ { name = "itsdangerous" },
140
+ { name = "jinja2" },
141
+ { name = "markupsafe" },
142
+ { name = "werkzeug" },
143
+ ]
144
+ sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
145
+ wheels = [
146
+ { url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
147
+ ]
148
+
111
149
  [[package]]
112
150
  name = "hkjc"
113
- version = "0.3.16"
151
+ version = "0.3.18"
114
152
  source = { editable = "." }
115
153
  dependencies = [
116
154
  { name = "beautifulsoup4" },
117
155
  { name = "cachetools" },
118
156
  { name = "fastexcel" },
119
- { name = "joblib" },
157
+ { name = "flask" },
120
158
  { name = "numba" },
121
159
  { name = "numpy" },
122
160
  { name = "polars" },
@@ -131,7 +169,7 @@ requires-dist = [
131
169
  { name = "beautifulsoup4", specifier = ">=4.14.2" },
132
170
  { name = "cachetools", specifier = ">=6.2.0" },
133
171
  { name = "fastexcel", specifier = ">=0.16.0" },
134
- { name = "joblib", specifier = ">=1.5.2" },
172
+ { name = "flask", specifier = ">=3.1.2" },
135
173
  { name = "numba", specifier = ">=0.62.1" },
136
174
  { name = "numpy", specifier = ">=2.3.3" },
137
175
  { name = "polars", specifier = ">=1.33.1" },
@@ -151,12 +189,24 @@ wheels = [
151
189
  ]
152
190
 
153
191
  [[package]]
154
- name = "joblib"
155
- version = "1.5.2"
192
+ name = "itsdangerous"
193
+ version = "2.2.0"
156
194
  source = { registry = "https://pypi.org/simple" }
157
- sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" }
195
+ sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
158
196
  wheels = [
159
- { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" },
197
+ { url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
198
+ ]
199
+
200
+ [[package]]
201
+ name = "jinja2"
202
+ version = "3.1.6"
203
+ source = { registry = "https://pypi.org/simple" }
204
+ dependencies = [
205
+ { name = "markupsafe" },
206
+ ]
207
+ sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
208
+ wheels = [
209
+ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
160
210
  ]
161
211
 
162
212
  [[package]]
@@ -182,6 +232,80 @@ wheels = [
182
232
  { 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" },
183
233
  ]
184
234
 
235
+ [[package]]
236
+ name = "markupsafe"
237
+ version = "3.0.3"
238
+ source = { registry = "https://pypi.org/simple" }
239
+ sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
240
+ wheels = [
241
+ { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
242
+ { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
243
+ { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
244
+ { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
245
+ { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
246
+ { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
247
+ { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
248
+ { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
249
+ { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
250
+ { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
251
+ { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
252
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
253
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
254
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
255
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
256
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
257
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
258
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
259
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
260
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
261
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
262
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
263
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
264
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
265
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
266
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
267
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
268
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
269
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
270
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
271
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
272
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
273
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
274
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
275
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
276
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
277
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
278
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
279
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
280
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
281
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
282
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
283
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
284
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
285
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
286
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
287
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
288
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
289
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
290
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
291
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
292
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
293
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
294
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
295
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
296
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
297
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
298
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
299
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
300
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
301
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
302
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
303
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
304
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
305
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
306
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
307
+ ]
308
+
185
309
  [[package]]
186
310
  name = "numba"
187
311
  version = "0.62.1"
@@ -464,3 +588,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599
464
588
  wheels = [
465
589
  { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
466
590
  ]
591
+
592
+ [[package]]
593
+ name = "werkzeug"
594
+ version = "3.1.3"
595
+ source = { registry = "https://pypi.org/simple" }
596
+ dependencies = [
597
+ { name = "markupsafe" },
598
+ ]
599
+ sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" }
600
+ wheels = [
601
+ { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" },
602
+ ]
Binary file
hkjc-0.3.16/process.py DELETED
@@ -1,4 +0,0 @@
1
- from hkjc import generate_historical_data
2
-
3
- df = generate_historical_data('2024-09-08', '2025-10-06')
4
- df.write_parquet('2024-2025-hkjc.parquet')
@@ -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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes