hkjc 0.3.18__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.18 → hkjc-0.3.19}/.gitignore +1 -2
- {hkjc-0.3.18 → hkjc-0.3.19}/PKG-INFO +1 -1
- hkjc-0.3.19/dashboard/app.py +87 -0
- hkjc-0.3.19/dashboard/hkjc2425.parquet +0 -0
- hkjc-0.3.19/dashboard/static/script.js +345 -0
- hkjc-0.3.19/dashboard/static/styles.css +336 -0
- hkjc-0.3.19/dashboard/templates/horse-info.html +40 -0
- hkjc-0.3.19/dashboard/templates/index.html +129 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/pyproject.toml +1 -1
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/__init__.py +4 -4
- hkjc-0.3.19/src/hkjc/features.py +6 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/historical.py +28 -43
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/live.py +22 -18
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/processing.py +6 -1
- {hkjc-0.3.18 → hkjc-0.3.19}/uv.lock +24 -12
- hkjc-0.3.18/src/hkjc/analysis.py +0 -3
- {hkjc-0.3.18 → hkjc-0.3.19}/.python-version +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/README.md +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19/dashboard}/process.py +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/harville_model.py +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/py.typed +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/speedpro.py +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/strategy/place_only.py +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/strategy/qpbanker.py +0 -0
- {hkjc-0.3.18 → hkjc-0.3.19}/src/hkjc/utils.py +0 -0
@@ -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,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
|
+
|