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.
Files changed (28) hide show
  1. {hkjc-0.4.0 → hkjc-0.4.1}/PKG-INFO +1 -1
  2. {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/app.py +5 -5
  3. hkjc-0.4.1/dashboard/static/app.js +43 -0
  4. hkjc-0.4.1/dashboard/static/core.js +201 -0
  5. hkjc-0.4.1/dashboard/static/race-data.js +481 -0
  6. hkjc-0.4.1/dashboard/static/race-ui.js +379 -0
  7. hkjc-0.4.1/dashboard/static/script.js +1469 -0
  8. {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/static/styles.css +273 -4
  9. hkjc-0.4.1/dashboard/static/trade-calculator.js +370 -0
  10. {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/templates/index.html +89 -8
  11. {hkjc-0.4.0 → hkjc-0.4.1}/pyproject.toml +1 -1
  12. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/processing.py +1 -1
  13. {hkjc-0.4.0 → hkjc-0.4.1}/uv.lock +1 -1
  14. hkjc-0.4.0/dashboard/static/script.js +0 -933
  15. {hkjc-0.4.0 → hkjc-0.4.1}/.gitignore +0 -0
  16. {hkjc-0.4.0 → hkjc-0.4.1}/.python-version +0 -0
  17. {hkjc-0.4.0 → hkjc-0.4.1}/README.md +0 -0
  18. {hkjc-0.4.0 → hkjc-0.4.1}/dashboard/templates/horse-info.html +0 -0
  19. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/__init__.py +0 -0
  20. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/features.py +0 -0
  21. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/harville_model.py +0 -0
  22. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/historical.py +0 -0
  23. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/live.py +0 -0
  24. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/py.typed +0 -0
  25. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/speedpro.py +0 -0
  26. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/strategy/place_only.py +0 -0
  27. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/strategy/qpbanker.py +0 -0
  28. {hkjc-0.4.0 → hkjc-0.4.1}/src/hkjc/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hkjc
3
- Version: 0.4.0
3
+ Version: 0.4.1
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
@@ -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>', methods=['GET'])
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
+