hkjc 0.4.0__tar.gz → 0.4.1__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.4.0 → hkjc-0.4.1}/PKG-INFO +1 -1
- {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/app.py +5 -5
- hkjc-0.4.1/dashboard/static/app.js +43 -0
- hkjc-0.4.1/dashboard/static/core.js +201 -0
- hkjc-0.4.1/dashboard/static/race-data.js +481 -0
- hkjc-0.4.1/dashboard/static/race-ui.js +379 -0
- hkjc-0.4.1/dashboard/static/script.js +1469 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/static/styles.css +273 -4
- hkjc-0.4.1/dashboard/static/trade-calculator.js +370 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/templates/index.html +89 -8
- {hkjc-0.4.0 → hkjc-0.4.1}/pyproject.toml +1 -1
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/processing.py +1 -1
- {hkjc-0.4.0 → hkjc-0.4.1}/uv.lock +1 -1
- hkjc-0.4.0/dashboard/static/script.js +0 -933
- {hkjc-0.4.0 → hkjc-0.4.1}/.gitignore +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/.python-version +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/README.md +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/templates/horse-info.html +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/__init__.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/features.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/harville_model.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/historical.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/live.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/py.typed +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/speedpro.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/strategy/place_only.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/strategy/qpbanker.py +0 -0
- {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/utils.py +0 -0
@@ -144,15 +144,15 @@ def disp_qp_metrics(race_no=1, banker=1, cover='2'):
|
|
144
144
|
if use_filter:
|
145
145
|
res = fit_harville_to_odds(odds)
|
146
146
|
if res['success']:
|
147
|
-
odds = res['odds_fit']
|
148
|
-
covered = [int(v) for v in cover.split(',')]
|
147
|
+
odds['PLA'] = res['odds_fit']['PLA']
|
148
|
+
covered = [int(v) for v in cover.split(',') if int(v) != banker]
|
149
149
|
ev = qpbanker.expected_value(odds['PLA'], odds['QPL'], banker, covered)
|
150
150
|
win = qpbanker.win_probability(odds['PLA'], banker, covered)
|
151
151
|
avg_odds = qpbanker.average_odds(odds['QPL'], banker, covered)
|
152
152
|
|
153
153
|
return {'Banker': banker, 'Covered': covered, 'ExpValue': round(ev, 2), 'WinProb': round(win, 2), 'AvgOdds': round(avg_odds, 2)}
|
154
154
|
|
155
|
-
@app.route('/pla/<int:race_no>/<cover>'
|
155
|
+
@app.route('/pla/<int:race_no>/<cover>')
|
156
156
|
def disp_pla_metrics(race_no=1, cover=[]):
|
157
157
|
odds = live_odds('', '', race_number=race_no)
|
158
158
|
|
@@ -198,7 +198,7 @@ def disp_qp_recs(race_no=1, banker=1, exclude=[], maxC=None):
|
|
198
198
|
df_trades = df_trades.filter(elimination(excluded))
|
199
199
|
if maxC:
|
200
200
|
df_trades = df_trades.filter(pl.col('NumCovered') <= maxC)
|
201
|
-
pareto_trades = pareto_filter(df_trades, [], ['WinProb', 'ExpValue']).sort(
|
201
|
+
pareto_trades = pareto_filter(df_trades, ['NumCovered'], ['WinProb', 'ExpValue']).sort(
|
202
202
|
'WinProb', descending=True)
|
203
203
|
|
204
204
|
return [format_trade(t) for t in pareto_trades.iter_rows(named=True)]
|
@@ -219,7 +219,7 @@ def disp_pla_recs(race_no=1, exclude=[], maxC=None):
|
|
219
219
|
df_trades = df_trades.filter(elimination(excluded))
|
220
220
|
if maxC:
|
221
221
|
df_trades = df_trades.filter(pl.col('NumCovered') <= maxC)
|
222
|
-
pareto_trades = pareto_filter(df_trades, [], ['WinProb', 'ExpValue']).sort(
|
222
|
+
pareto_trades = pareto_filter(df_trades, ['NumCovered'], ['WinProb', 'ExpValue']).sort(
|
223
223
|
'WinProb', descending=True)
|
224
224
|
|
225
225
|
return [format_trade(t) for t in pareto_trades.iter_rows(named=True)]
|
@@ -0,0 +1,43 @@
|
|
1
|
+
// HKJC Race Info - Application Bootstrap
|
2
|
+
const App = {
|
3
|
+
init() {
|
4
|
+
console.log('Initializing HKJC Race Info...');
|
5
|
+
const savedRaceTab = sessionStorage.getItem('activeRaceTab');
|
6
|
+
if (savedRaceTab && Utils.$(`#tab-${savedRaceTab}`)) UI.showRace(savedRaceTab);
|
7
|
+
|
8
|
+
const activeRaceNum = Utils.$('.race-content.active')?.id.replace('race-', '');
|
9
|
+
Speedmap.loadAll();
|
10
|
+
FavStyleChart.initAll();
|
11
|
+
PreferredHighlight.initAll();
|
12
|
+
TradeCalculator.initAll();
|
13
|
+
Odds.startPolling();
|
14
|
+
RunnerDetails.prefetchAll(activeRaceNum).catch(error =>
|
15
|
+
console.error('Error in background prefetch:', error));
|
16
|
+
console.log('Application initialized successfully');
|
17
|
+
},
|
18
|
+
|
19
|
+
cleanup() {
|
20
|
+
Odds.stopPolling();
|
21
|
+
RequestQueue.clear();
|
22
|
+
Cache.clear();
|
23
|
+
}
|
24
|
+
};
|
25
|
+
|
26
|
+
window.showRace = (raceNum) => UI.showRace(raceNum);
|
27
|
+
window.toggleRunnerHistory = (row, raceNum, horseNo, going, track, dist) =>
|
28
|
+
RunnerDetails.toggle(row, raceNum, horseNo, going, track, dist);
|
29
|
+
window.toggleAllRunners = (raceNum, btn) => RunnerDetails.toggleAll(raceNum, btn);
|
30
|
+
window.sortTable = (raceNum, colIdx, type) => UI.sortTable(raceNum, colIdx, type);
|
31
|
+
window.openTrackWorkoutVideo = (date, raceNum) => UI.openTrackWorkoutVideo(date, raceNum);
|
32
|
+
window.openTipsIndex = (raceNum) => UI.openTipsIndex(raceNum);
|
33
|
+
window.showTradeTab = (raceNum, tabName) => TradeCalculator.showTab(raceNum, tabName);
|
34
|
+
window.toggleParetoExclude = (raceNum, type, runnerNo) => TradeCalculator.toggleParetoExclude(raceNum, type, runnerNo);
|
35
|
+
window.toggleCover = (raceNum, type, runnerNo) => TradeCalculator.toggleCover(raceNum, type, runnerNo);
|
36
|
+
window.selectBanker = (raceNum, runnerNo) => TradeCalculator.selectBanker(raceNum, runnerNo);
|
37
|
+
window.toggleFilter = (raceNum) => TradeCalculator.toggleFilter(raceNum);
|
38
|
+
window.calculatePLA = (raceNum) => TradeCalculator.calculatePLA(raceNum);
|
39
|
+
window.calculateQPL = (raceNum) => TradeCalculator.calculateQPL(raceNum);
|
40
|
+
|
41
|
+
document.addEventListener('DOMContentLoaded', () => App.init());
|
42
|
+
window.addEventListener('beforeunload', () => App.cleanup());
|
43
|
+
|
@@ -0,0 +1,201 @@
|
|
1
|
+
// HKJC Race Info - Core Infrastructure
|
2
|
+
const CONFIG = {
|
3
|
+
ODDS_POLL_INTERVAL: 90000,
|
4
|
+
POLL_JITTER: 10000,
|
5
|
+
MIN_REQUEST_DELAY: 80,
|
6
|
+
MAX_REQUEST_DELAY: 250,
|
7
|
+
MAX_CONCURRENT_REQUESTS: 3,
|
8
|
+
REQUEST_TIMEOUT: 10000,
|
9
|
+
HIGHLIGHT_DEBOUNCE_DELAY: 100,
|
10
|
+
DEBUG: false
|
11
|
+
};
|
12
|
+
|
13
|
+
const Utils = {
|
14
|
+
getRunnerData(row) {
|
15
|
+
return {
|
16
|
+
raceNum: row.dataset.race,
|
17
|
+
horseNo: row.dataset.horse,
|
18
|
+
going: row.dataset.going || '',
|
19
|
+
track: row.dataset.track || '',
|
20
|
+
distance: row.dataset.dist || '',
|
21
|
+
venue: row.dataset.venue || '',
|
22
|
+
runnerNo: row.querySelector('.odds-win')?.dataset.horse || row.dataset.horse
|
23
|
+
};
|
24
|
+
},
|
25
|
+
|
26
|
+
log(...args) { if (CONFIG.DEBUG) console.log(...args); },
|
27
|
+
|
28
|
+
$(selector, context = document) { return context.querySelector(selector); },
|
29
|
+
$$(selector, context = document) { return Array.from(context.querySelectorAll(selector)); },
|
30
|
+
|
31
|
+
addClass(elements, className) {
|
32
|
+
const classes = Array.isArray(className) ? className : [className];
|
33
|
+
const els = Array.isArray(elements) ? elements : [elements];
|
34
|
+
els.forEach(el => el && classes.forEach(cls => el.classList.add(cls)));
|
35
|
+
},
|
36
|
+
|
37
|
+
removeClass(elements, className) {
|
38
|
+
const classes = Array.isArray(className) ? className : [className];
|
39
|
+
const els = Array.isArray(elements) ? elements : [elements];
|
40
|
+
els.forEach(el => el && classes.forEach(cls => el.classList.remove(cls)));
|
41
|
+
},
|
42
|
+
|
43
|
+
toggleClass(elements, className, force) {
|
44
|
+
const els = Array.isArray(elements) ? elements : [elements];
|
45
|
+
els.forEach(el => el?.classList.toggle(className, force));
|
46
|
+
},
|
47
|
+
|
48
|
+
parseNum(value, fallback = 0) {
|
49
|
+
const num = parseFloat(value);
|
50
|
+
return isNaN(num) ? fallback : num;
|
51
|
+
},
|
52
|
+
|
53
|
+
makeCacheKey(...parts) {
|
54
|
+
return parts.join('-');
|
55
|
+
},
|
56
|
+
|
57
|
+
validateRaceNum(raceNum) {
|
58
|
+
const num = parseInt(raceNum);
|
59
|
+
if (isNaN(num) || num < 1 || num > 99) throw new Error(`Invalid race number: ${raceNum}`);
|
60
|
+
return num.toString();
|
61
|
+
},
|
62
|
+
|
63
|
+
validateHorseNo(horseNo) {
|
64
|
+
if (!horseNo || typeof horseNo !== 'string' || !horseNo.trim())
|
65
|
+
throw new Error(`Invalid horse number: ${horseNo}`);
|
66
|
+
return horseNo.trim();
|
67
|
+
},
|
68
|
+
|
69
|
+
validateDate(dateStr) {
|
70
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr))
|
71
|
+
throw new Error(`Invalid date format: ${dateStr}. Expected YYYY-MM-DD`);
|
72
|
+
return dateStr;
|
73
|
+
}
|
74
|
+
};
|
75
|
+
|
76
|
+
const State = {
|
77
|
+
caches: { runnerDetails: new Map(), speedmaps: new Map(), odds: new Map() },
|
78
|
+
polling: { oddsIntervalId: null },
|
79
|
+
raceReadiness: new Map()
|
80
|
+
};
|
81
|
+
|
82
|
+
const RequestQueue = {
|
83
|
+
queue: [],
|
84
|
+
activeCount: 0,
|
85
|
+
|
86
|
+
enqueue(requestFn, priority = 0) {
|
87
|
+
return new Promise((resolve, reject) => {
|
88
|
+
this.queue.push({ requestFn, resolve, reject, priority });
|
89
|
+
this.queue.sort((a, b) => b.priority - a.priority);
|
90
|
+
this.process();
|
91
|
+
});
|
92
|
+
},
|
93
|
+
|
94
|
+
async process() {
|
95
|
+
if (this.activeCount >= CONFIG.MAX_CONCURRENT_REQUESTS || !this.queue.length) return;
|
96
|
+
|
97
|
+
const { requestFn, resolve, reject } = this.queue.shift();
|
98
|
+
this.activeCount++;
|
99
|
+
|
100
|
+
try {
|
101
|
+
resolve(await requestFn());
|
102
|
+
} catch (error) {
|
103
|
+
reject(error);
|
104
|
+
} finally {
|
105
|
+
this.activeCount--;
|
106
|
+
await new Promise(r => setTimeout(r,
|
107
|
+
Math.random() * (CONFIG.MAX_REQUEST_DELAY - CONFIG.MIN_REQUEST_DELAY) + CONFIG.MIN_REQUEST_DELAY));
|
108
|
+
this.process();
|
109
|
+
}
|
110
|
+
},
|
111
|
+
|
112
|
+
clear() { this.queue = []; }
|
113
|
+
};
|
114
|
+
|
115
|
+
const API = {
|
116
|
+
async fetch(url, options = {}) {
|
117
|
+
const controller = new AbortController();
|
118
|
+
const timeoutId = setTimeout(() => controller.abort(), CONFIG.REQUEST_TIMEOUT);
|
119
|
+
|
120
|
+
try {
|
121
|
+
const response = await fetch(url, { ...options, signal: controller.signal });
|
122
|
+
clearTimeout(timeoutId);
|
123
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
124
|
+
return response;
|
125
|
+
} catch (error) {
|
126
|
+
clearTimeout(timeoutId);
|
127
|
+
if (error.name === 'AbortError') throw new Error('Request timeout');
|
128
|
+
throw error;
|
129
|
+
}
|
130
|
+
},
|
131
|
+
|
132
|
+
fetchOdds(raceNum) {
|
133
|
+
return RequestQueue.enqueue(async () =>
|
134
|
+
(await this.fetch(`/live_odds/${Utils.validateRaceNum(raceNum)}`)).json(), 1);
|
135
|
+
},
|
136
|
+
|
137
|
+
fetchHorseHistory(horseNo, going, track, distance, venue) {
|
138
|
+
return RequestQueue.enqueue(async () => {
|
139
|
+
const params = new URLSearchParams({ going: going || '', track: track || '', dist: distance || '', venue: venue || '' });
|
140
|
+
return (await this.fetch(`/horse_info/${Utils.validateHorseNo(horseNo)}?${params}`)).text();
|
141
|
+
}, 0);
|
142
|
+
},
|
143
|
+
|
144
|
+
fetchSpeedmap(raceNum) {
|
145
|
+
return RequestQueue.enqueue(async () =>
|
146
|
+
(await this.fetch(`/speedmap/${Utils.validateRaceNum(raceNum)}`)).text(), 0);
|
147
|
+
},
|
148
|
+
|
149
|
+
fetchParetoData(raceNum, exclude = '', abortSignal = null) {
|
150
|
+
return RequestQueue.enqueue(async () => {
|
151
|
+
const url = `/plarec/${Utils.validateRaceNum(raceNum)}${exclude ? `?exclude=${exclude}` : ''}`;
|
152
|
+
return (await this.fetch(url, { signal: abortSignal })).json();
|
153
|
+
}, 1);
|
154
|
+
},
|
155
|
+
|
156
|
+
fetchQParetoData(raceNum, banker, exclude = '', filter = false, abortSignal = null) {
|
157
|
+
return RequestQueue.enqueue(async () => {
|
158
|
+
const url = `/qprec/${Utils.validateRaceNum(raceNum)}/${banker}?exclude=${exclude}&filter=${filter}`;
|
159
|
+
return (await this.fetch(url, { signal: abortSignal })).json();
|
160
|
+
}, 1);
|
161
|
+
},
|
162
|
+
|
163
|
+
fetchPLAData(raceNum, cover) {
|
164
|
+
return RequestQueue.enqueue(async () =>
|
165
|
+
(await this.fetch(`/pla/${Utils.validateRaceNum(raceNum)}/${cover}`)).json(), 0);
|
166
|
+
},
|
167
|
+
|
168
|
+
fetchQPLData(raceNum, banker, cover, filter) {
|
169
|
+
return RequestQueue.enqueue(async () => {
|
170
|
+
const url = `/qp/${Utils.validateRaceNum(raceNum)}/${banker}/${cover}?filter=${filter}`;
|
171
|
+
return (await this.fetch(url)).json();
|
172
|
+
}, 0);
|
173
|
+
}
|
174
|
+
};
|
175
|
+
|
176
|
+
const Cache = {
|
177
|
+
get(cacheName, key) { return State.caches[cacheName]?.get(key); },
|
178
|
+
set(cacheName, key, value) {
|
179
|
+
if (!State.caches[cacheName]) State.caches[cacheName] = new Map();
|
180
|
+
State.caches[cacheName].set(key, value);
|
181
|
+
},
|
182
|
+
has(cacheName, key) { return State.caches[cacheName]?.has(key) || false; },
|
183
|
+
clear(cacheName) {
|
184
|
+
cacheName ? State.caches[cacheName]?.clear() : Object.values(State.caches).forEach(c => c.clear());
|
185
|
+
}
|
186
|
+
};
|
187
|
+
|
188
|
+
const RaceReadiness = {
|
189
|
+
isReady(raceNum) {
|
190
|
+
if (State.raceReadiness.has(raceNum)) return State.raceReadiness.get(raceNum);
|
191
|
+
const runnerRows = Utils.$$(`#race-${raceNum} .runner-row`);
|
192
|
+
const ready = runnerRows.length > 0 && runnerRows.some(row => {
|
193
|
+
const fitness = Utils.parseNum(row.cells[10]?.textContent.trim());
|
194
|
+
const energy = Utils.parseNum(row.cells[11]?.textContent.trim());
|
195
|
+
return fitness !== 0 || energy !== 0;
|
196
|
+
});
|
197
|
+
State.raceReadiness.set(raceNum, ready);
|
198
|
+
return ready;
|
199
|
+
}
|
200
|
+
};
|
201
|
+
|