hkjc 0.3.17__tar.gz → 0.3.19__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.19/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hkjc
3
- Version: 0.3.17
3
+ Version: 0.3.19
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
@@ -0,0 +1,87 @@
1
+ from flask import Flask, jsonify, render_template, request
2
+
3
+ import polars as pl
4
+
5
+ from hkjc.live import live_odds, _fetch_live_races
6
+ from hkjc.harville_model import fit_harville_to_odds
7
+ from hkjc.historical import get_horse_data
8
+ from hkjc.speedpro import speedmap, speedpro_energy
9
+
10
+ app = Flask(__name__)
11
+
12
+
13
+ @app.route("/")
14
+ def disp_race_info():
15
+ race_info = _fetch_live_races('', '')
16
+
17
+ df_speedpro = speedpro_energy(race_info['Date'])
18
+ for race_num, race in race_info['Races'].items():
19
+ for i, runner in enumerate(race['Runners']):
20
+ df = (df_speedpro
21
+ .filter(pl.col('RaceNo')==race_num)
22
+ .filter(pl.col('RunnerNumber')==int(runner['No']))
23
+ )
24
+ race_info['Races'][race_num]['Runners'][i]['SPEnergy'] = df['SpeedPRO_Energy_Difference'].item(0)
25
+ race_info['Races'][race_num]['Runners'][i]['Fitness'] = df['FitnessRatings'].item(0)
26
+
27
+ # TODO: add horse running style favorite from horse info
28
+ return render_template('index.html',
29
+ race_info=race_info)
30
+
31
+
32
+ turf_going_dict = {'FIRM': 'F',
33
+ 'GOOD TO FIRM': 'GF',
34
+ 'GOOD': 'G',
35
+ 'GOOD TO YIELDING': 'GY',
36
+ 'YIELDING': 'Y',
37
+ 'YIELDING TO SOFT': 'YS',
38
+ 'SOFT': 'S',
39
+ 'HEAVY': 'H'}
40
+ aw_going_dict = {'WET FAST': 'WF',
41
+ 'FAST': 'FT',
42
+ 'GOOD': 'GD',
43
+ 'SLOW': 'SL',
44
+ 'WET SLOW': 'WS',
45
+ 'RAIN AFFECTED': 'RA',
46
+ 'NORMAL WATERING': 'NW'}
47
+ going_dict = {'TURF': turf_going_dict, 'ALL WEATHER TRACK': aw_going_dict}
48
+
49
+ @app.route("/horse_info/<horse_no>", methods=['GET'])
50
+ def disp_horse_info(horse_no):
51
+ # read optional filters
52
+ dist = request.args.get('dist', type=int)
53
+ track = request.args.get('track')
54
+ going = request.args.get('going')
55
+
56
+ if track not in going_dict.keys():
57
+ track = None
58
+ if (going is not None) and (track is not None) and (going in going_dict[track].keys()):
59
+ going = going_dict[track][going] # translate going to code
60
+ else:
61
+ going = None
62
+
63
+ df = get_horse_data(horse_no)
64
+ if dist is not None:
65
+ df = df.filter(pl.col('Dist')==dist)
66
+ if track and track.upper() == 'TURF':
67
+ df = df.filter(pl.col('Track')=='Turf')
68
+ elif track and track.upper() == 'ALL WEATHER TRACK':
69
+ df = df.filter(pl.col('Track')=='AWT')
70
+ if going is not None:
71
+ df = df.filter(pl.col('G')==going)
72
+
73
+ return render_template('horse-info.html', df=df)
74
+
75
+ @app.route('/live_odds/<race_no>')
76
+ def disp_live_odds(race_no=1):
77
+ odds_dict = live_odds('','',int(race_no))
78
+ fitted_odds = fit_harville_to_odds(odds_dict)
79
+
80
+ # TODO: repackage odds into json
81
+ return fitted_odds.__repr__()
82
+
83
+ @app.route('/speedmap/<race_no>')
84
+ def disp_speedmap(race_no=1):
85
+ return speedmap(int(race_no))
86
+
87
+ # TODO: trades
Binary file
@@ -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')
@@ -0,0 +1,345 @@
1
+ /**
2
+ * HKJC Race Info - Main JavaScript
3
+ * Handles race tab navigation and horse history loading
4
+ */
5
+
6
+ // Cache to store loaded history data
7
+ const historyCache = {};
8
+
9
+ // Cache to store speedmap images
10
+ const speedmapCache = {};
11
+
12
+ /**
13
+ * Open track workout video in a new window
14
+ * @param {string} raceDate - The race date in format YYYY-MM-DD (e.g., 2025-10-08)
15
+ * @param {string} raceNum - The race number
16
+ */
17
+ function openTrackWorkoutVideo(raceDate, raceNum) {
18
+ // Parse the date
19
+ const dateParts = raceDate.split('-');
20
+ const year = dateParts[0];
21
+ const dateFormatted = dateParts.join(''); // YYYYMMDD format
22
+
23
+ // Pad race number to 2 digits
24
+ const raceNumPadded = raceNum.toString().padStart(2, '0');
25
+
26
+ // Construct the URL
27
+ const url = `https://streaminghkjc-a.akamaihd.net/hdflash/twstarter/${year}/${dateFormatted}/${raceNumPadded}/novo/twstarter_${dateFormatted}_${raceNumPadded}_novo_2500kbps.mp4`;
28
+
29
+ // Open in new window
30
+ window.open(url, '_blank');
31
+ }
32
+
33
+ /**
34
+ * Show a specific race tab and its content
35
+ * @param {string} raceNum - The race number to display
36
+ */
37
+ function showRace(raceNum) {
38
+ // Update active tab
39
+ document.querySelectorAll('.tab-btn').forEach(btn => {
40
+ btn.classList.remove('active');
41
+ });
42
+ document.getElementById(`tab-${raceNum}`).classList.add('active');
43
+
44
+ // Update active content
45
+ document.querySelectorAll('.race-content').forEach(content => {
46
+ content.classList.remove('active');
47
+ });
48
+ document.getElementById(`race-${raceNum}`).classList.add('active');
49
+ }
50
+
51
+ /**
52
+ * Toggle all runner histories in a race (expand or collapse all)
53
+ * @param {string} raceNum - The race number
54
+ * @param {HTMLElement} buttonElement - The expand/collapse all button element
55
+ */
56
+ function toggleAllRunners(raceNum, buttonElement) {
57
+ const raceContent = document.getElementById(`race-${raceNum}`);
58
+ const runnerRows = raceContent.querySelectorAll('.runner-row');
59
+ const isExpanding = !buttonElement.classList.contains('all-expanded');
60
+
61
+ runnerRows.forEach(row => {
62
+ const expandIcon = row.querySelector('.expand-icon');
63
+ const horseNo = row.onclick.toString().match(/'([^']+)'/g)[1].replace(/'/g, '');
64
+ const historyRow = document.getElementById(`history-${raceNum}-${horseNo}`);
65
+ const historyContent = document.getElementById(`history-content-${raceNum}-${horseNo}`);
66
+ const cacheKey = `${raceNum}-${horseNo}`;
67
+
68
+ if (isExpanding) {
69
+ // Expand
70
+ historyRow.classList.add('expanded');
71
+ expandIcon.classList.add('expanded');
72
+
73
+ // Load data if not already loaded
74
+ if (!historyCache[cacheKey]) {
75
+ historyContent.innerHTML = '<div class="history-loading">Loading history...</div>';
76
+
77
+ // Extract parameters from onclick
78
+ const onclickStr = row.getAttribute('onclick');
79
+ const params = onclickStr.match(/'([^']+)'/g).map(s => s.replace(/'/g, ''));
80
+ const going = params[2];
81
+ const track = params[3];
82
+ const distance = params[4];
83
+
84
+ const url = `/horse_info/${horseNo}?going=${encodeURIComponent(going)}&track=${encodeURIComponent(track)}&dist=${encodeURIComponent(distance)}`;
85
+
86
+ fetch(url)
87
+ .then(response => {
88
+ if (!response.ok) {
89
+ throw new Error(`HTTP error! status: ${response.status}`);
90
+ }
91
+ return response.text();
92
+ })
93
+ .then(html => {
94
+ historyCache[cacheKey] = html;
95
+ historyContent.innerHTML = html;
96
+ })
97
+ .catch(error => {
98
+ console.error('Error loading horse history:', error);
99
+ historyContent.innerHTML = `<div class="history-loading" style="color: #ff4444;">Error loading history: ${error.message}</div>`;
100
+ });
101
+ } else {
102
+ historyContent.innerHTML = historyCache[cacheKey];
103
+ }
104
+ } else {
105
+ // Collapse
106
+ historyRow.classList.remove('expanded');
107
+ expandIcon.classList.remove('expanded');
108
+ }
109
+ });
110
+
111
+ // Update button state
112
+ if (isExpanding) {
113
+ buttonElement.classList.add('all-expanded');
114
+ buttonElement.querySelector('.expand-all-text').textContent = 'Collapse All';
115
+ } else {
116
+ buttonElement.classList.remove('all-expanded');
117
+ buttonElement.querySelector('.expand-all-text').textContent = 'Expand All';
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Toggle individual runner history (expand or collapse)
123
+ * @param {HTMLElement} rowElement - The runner row element
124
+ * @param {string} raceNum - The race number
125
+ * @param {string} horseNo - The horse number
126
+ * @param {string} going - Track going conditions
127
+ * @param {string} track - Track type
128
+ * @param {string} distance - Race distance
129
+ */
130
+ function toggleRunnerHistory(rowElement, raceNum, horseNo, going, track, distance) {
131
+ const expandIcon = rowElement.querySelector('.expand-icon');
132
+ const historyRow = document.getElementById(`history-${raceNum}-${horseNo}`);
133
+ const historyContent = document.getElementById(`history-content-${raceNum}-${horseNo}`);
134
+ const cacheKey = `${raceNum}-${horseNo}`;
135
+
136
+ // Toggle expanded state
137
+ const isExpanded = historyRow.classList.contains('expanded');
138
+
139
+ if (isExpanded) {
140
+ // Collapse
141
+ historyRow.classList.remove('expanded');
142
+ expandIcon.classList.remove('expanded');
143
+ } else {
144
+ // Expand
145
+ historyRow.classList.add('expanded');
146
+ expandIcon.classList.add('expanded');
147
+
148
+ // Load data if not already loaded
149
+ if (!historyCache[cacheKey]) {
150
+ // Show loading state
151
+ historyContent.innerHTML = '<div class="history-loading">Loading history...</div>';
152
+
153
+ // Build URL with query parameters
154
+ const url = `/horse_info/${horseNo}?going=${encodeURIComponent(going)}&track=${encodeURIComponent(track)}&dist=${encodeURIComponent(distance)}`;
155
+
156
+ // Fetch history data
157
+ fetch(url)
158
+ .then(response => {
159
+ if (!response.ok) {
160
+ throw new Error(`HTTP error! status: ${response.status}`);
161
+ }
162
+ return response.text();
163
+ })
164
+ .then(html => {
165
+ // Cache the result
166
+ historyCache[cacheKey] = html;
167
+
168
+ // Display the history table
169
+ historyContent.innerHTML = html;
170
+ })
171
+ .catch(error => {
172
+ console.error('Error loading horse history:', error);
173
+ historyContent.innerHTML = `<div class="history-loading" style="color: #ff4444;">Error loading history: ${error.message}</div>`;
174
+ });
175
+ } else {
176
+ // Use cached data
177
+ historyContent.innerHTML = historyCache[cacheKey];
178
+ }
179
+ }
180
+
181
+ // Update "Expand All" button state
182
+ updateExpandAllButtonState(raceNum);
183
+ }
184
+
185
+ /**
186
+ * Update the state of the "Expand All" button based on current expanded rows
187
+ * @param {string} raceNum - The race number
188
+ */
189
+ function updateExpandAllButtonState(raceNum) {
190
+ const raceContent = document.getElementById(`race-${raceNum}`);
191
+ const expandAllBtn = raceContent.querySelector('.expand-all-btn');
192
+ const runnerRows = raceContent.querySelectorAll('.runner-row');
193
+ const historyRows = raceContent.querySelectorAll('.history-row');
194
+
195
+ // Check if all rows are expanded
196
+ let allExpanded = true;
197
+ historyRows.forEach(row => {
198
+ if (!row.classList.contains('expanded')) {
199
+ allExpanded = false;
200
+ }
201
+ });
202
+
203
+ // Update button state
204
+ if (allExpanded && runnerRows.length > 0) {
205
+ expandAllBtn.classList.add('all-expanded');
206
+ expandAllBtn.querySelector('.expand-all-text').textContent = 'Collapse All';
207
+ } else {
208
+ expandAllBtn.classList.remove('all-expanded');
209
+ expandAllBtn.querySelector('.expand-all-text').textContent = 'Expand All';
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Load speedmap image for a specific race
215
+ * @param {string} raceNum - The race number
216
+ */
217
+ function loadSpeedmap(raceNum) {
218
+ const speedmapImg = document.getElementById(`speedmap-${raceNum}`);
219
+
220
+ if (!speedmapImg) {
221
+ return;
222
+ }
223
+
224
+ // Check if already cached
225
+ if (speedmapCache[raceNum]) {
226
+ speedmapImg.src = speedmapCache[raceNum];
227
+ speedmapImg.style.display = 'block';
228
+ return;
229
+ }
230
+
231
+ // Show loading state
232
+ speedmapImg.style.display = 'none';
233
+
234
+ // Fetch speedmap from server
235
+ fetch(`/speedmap/${raceNum}`)
236
+ .then(response => {
237
+ if (!response.ok) {
238
+ throw new Error(`HTTP error! status: ${response.status}`);
239
+ }
240
+ return response.text();
241
+ })
242
+ .then(base64String => {
243
+ // Create data URL from base64 string
244
+ const dataUrl = base64String;
245
+
246
+ // Cache the image
247
+ speedmapCache[raceNum] = dataUrl;
248
+
249
+ // Set image source
250
+ speedmapImg.src = dataUrl;
251
+ speedmapImg.style.display = 'block';
252
+ })
253
+ .catch(error => {
254
+ console.error(`Error loading speedmap for race ${raceNum}:`, error);
255
+ // Hide image on error
256
+ speedmapImg.style.display = 'none';
257
+ });
258
+ }
259
+
260
+ /**
261
+ * Load all speedmaps on page load
262
+ */
263
+ function loadAllSpeedmaps() {
264
+ const speedmapImages = document.querySelectorAll('.speedmap-image');
265
+ speedmapImages.forEach(img => {
266
+ const raceNum = img.id.replace('speedmap-', '');
267
+ loadSpeedmap(raceNum);
268
+ });
269
+ }
270
+
271
+ // Load speedmaps when page is ready
272
+ document.addEventListener('DOMContentLoaded', loadAllSpeedmaps);
273
+
274
+ /**
275
+ * Sort table by column
276
+ * @param {string} raceNum - The race number
277
+ * @param {number} columnIndex - The column index to sort by
278
+ * @param {string} type - The data type ('number' or 'string')
279
+ */
280
+ function sortTable(raceNum, columnIndex, type) {
281
+ const table = document.querySelector(`#race-${raceNum} .runners-table`);
282
+ const tbody = table.querySelector('tbody');
283
+ const header = table.querySelectorAll('thead th')[columnIndex];
284
+ const allHeaders = table.querySelectorAll('thead th.sortable');
285
+
286
+ // Get all runner rows (every other row, excluding history rows)
287
+ const rows = Array.from(tbody.querySelectorAll('tr')).filter((row, index) => index % 2 === 0);
288
+
289
+ // Determine sort direction
290
+ let isAscending = true;
291
+ if (header.classList.contains('sort-asc')) {
292
+ isAscending = false;
293
+ }
294
+
295
+ // Clear all sort indicators
296
+ allHeaders.forEach(h => {
297
+ h.classList.remove('sort-asc', 'sort-desc');
298
+ const arrow = h.querySelector('.sort-arrow');
299
+ if (arrow) arrow.textContent = '⇅';
300
+ });
301
+
302
+ // Sort rows
303
+ rows.sort((a, b) => {
304
+ const aValue = a.cells[columnIndex].textContent.trim();
305
+ const bValue = b.cells[columnIndex].textContent.trim();
306
+
307
+ let aComp, bComp;
308
+
309
+ if (type === 'number') {
310
+ // Parse numeric values, treat non-numeric as -Infinity
311
+ aComp = parseFloat(aValue);
312
+ bComp = parseFloat(bValue);
313
+
314
+ // Only treat actual non-numeric values (NaN) as -Infinity, not 0
315
+ if (isNaN(aComp)) aComp = -Infinity;
316
+ if (isNaN(bComp)) bComp = -Infinity;
317
+ } else {
318
+ aComp = aValue.toLowerCase();
319
+ bComp = bValue.toLowerCase();
320
+ }
321
+
322
+ if (isAscending) {
323
+ return aComp > bComp ? 1 : aComp < bComp ? -1 : 0;
324
+ } else {
325
+ return aComp < bComp ? 1 : aComp > bComp ? -1 : 0;
326
+ }
327
+ });
328
+
329
+ // Update sort indicator
330
+ header.classList.add(isAscending ? 'sort-asc' : 'sort-desc');
331
+ const arrow = header.querySelector('.sort-arrow');
332
+ if (arrow) {
333
+ arrow.textContent = isAscending ? '▲' : '▼';
334
+ }
335
+
336
+ // Reorder rows in the table
337
+ rows.forEach((row, index) => {
338
+ const historyRow = row.nextElementSibling;
339
+ tbody.appendChild(row);
340
+ if (historyRow) {
341
+ tbody.appendChild(historyRow);
342
+ }
343
+ });
344
+ }
345
+