Codex commited on
Commit ·
63b4caa
1
Parent(s): 8128600
Fix odds API event-level scanner requests
Browse files- src/market-scanner.js +35 -10
- test/market-scanner.test.js +62 -0
src/market-scanner.js
CHANGED
|
@@ -382,6 +382,14 @@ async function fetchArrayBuffer(url) {
|
|
| 382 |
return response.arrayBuffer();
|
| 383 |
}
|
| 384 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 385 |
async function extractPdfText(buffer) {
|
| 386 |
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(buffer), useWorkerFetch: false, isEvalSupported: false });
|
| 387 |
const pdf = await loadingTask.promise;
|
|
@@ -433,19 +441,36 @@ export async function fetchCircaEntries(config) {
|
|
| 433 |
}
|
| 434 |
|
| 435 |
export async function fetchOddsApiEntries(config) {
|
| 436 |
-
const
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
|
| 443 |
-
|
| 444 |
-
if (!response.ok) {
|
| 445 |
-
throw new Error(`Odds API request failed with ${response.status}`);
|
| 446 |
}
|
| 447 |
|
| 448 |
-
const payload = await response.json();
|
| 449 |
return normalizeOddsApiEntries(payload);
|
| 450 |
}
|
| 451 |
|
|
|
|
| 382 |
return response.arrayBuffer();
|
| 383 |
}
|
| 384 |
|
| 385 |
+
async function fetchJson(url) {
|
| 386 |
+
const response = await fetch(url);
|
| 387 |
+
if (!response.ok) {
|
| 388 |
+
throw new Error(`Request failed with ${response.status} for ${url}`);
|
| 389 |
+
}
|
| 390 |
+
return response.json();
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
async function extractPdfText(buffer) {
|
| 394 |
const loadingTask = pdfjsLib.getDocument({ data: new Uint8Array(buffer), useWorkerFetch: false, isEvalSupported: false });
|
| 395 |
const pdf = await loadingTask.promise;
|
|
|
|
| 441 |
}
|
| 442 |
|
| 443 |
export async function fetchOddsApiEntries(config) {
|
| 444 |
+
const baseUrl = config.oddsApiBaseUrl.replace(/\/$/, '');
|
| 445 |
+
const eventsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events`);
|
| 446 |
+
eventsUrl.searchParams.set('apiKey', config.oddsApiKey);
|
| 447 |
+
eventsUrl.searchParams.set('dateFormat', 'iso');
|
| 448 |
+
|
| 449 |
+
const events = await fetchJson(eventsUrl);
|
| 450 |
+
if (!Array.isArray(events) || events.length === 0) {
|
| 451 |
+
return [];
|
| 452 |
+
}
|
| 453 |
+
|
| 454 |
+
const payload = [];
|
| 455 |
+
for (const event of events) {
|
| 456 |
+
const oddsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events/${event.id}/odds`);
|
| 457 |
+
oddsUrl.searchParams.set('apiKey', config.oddsApiKey);
|
| 458 |
+
oddsUrl.searchParams.set('regions', config.oddsApiRegions);
|
| 459 |
+
oddsUrl.searchParams.set('markets', config.oddsApiMarkets.join(','));
|
| 460 |
+
oddsUrl.searchParams.set('oddsFormat', 'american');
|
| 461 |
+
oddsUrl.searchParams.set('dateFormat', 'iso');
|
| 462 |
+
|
| 463 |
+
const response = await fetch(oddsUrl);
|
| 464 |
+
if (response.status === 404 || response.status === 422) {
|
| 465 |
+
continue;
|
| 466 |
+
}
|
| 467 |
+
if (!response.ok) {
|
| 468 |
+
throw new Error(`Odds API request failed with ${response.status}`);
|
| 469 |
+
}
|
| 470 |
|
| 471 |
+
payload.push(await response.json());
|
|
|
|
|
|
|
| 472 |
}
|
| 473 |
|
|
|
|
| 474 |
return normalizeOddsApiEntries(payload);
|
| 475 |
}
|
| 476 |
|
test/market-scanner.test.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
| 4 |
americanToImpliedProbability,
|
| 5 |
analyzeMarkets,
|
| 6 |
buildMarketKey,
|
|
|
|
| 7 |
normalizeOddsApiEntries,
|
| 8 |
parseCircaOcrText,
|
| 9 |
} from '../src/market-scanner.js';
|
|
@@ -103,3 +104,64 @@ test('ranks discrepancy, width, and circa alerts', () => {
|
|
| 103 |
assert.equal(analysis.circaAlerts.length, 1);
|
| 104 |
assert.equal(analysis.circaAlerts[0].furthestBookName, 'FanDuel');
|
| 105 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
americanToImpliedProbability,
|
| 5 |
analyzeMarkets,
|
| 6 |
buildMarketKey,
|
| 7 |
+
fetchOddsApiEntries,
|
| 8 |
normalizeOddsApiEntries,
|
| 9 |
parseCircaOcrText,
|
| 10 |
} from '../src/market-scanner.js';
|
|
|
|
| 104 |
assert.equal(analysis.circaAlerts.length, 1);
|
| 105 |
assert.equal(analysis.circaAlerts[0].furthestBookName, 'FanDuel');
|
| 106 |
});
|
| 107 |
+
|
| 108 |
+
test('fetches odds api entries from event-level endpoints', async () => {
|
| 109 |
+
const originalFetch = global.fetch;
|
| 110 |
+
const calls = [];
|
| 111 |
+
|
| 112 |
+
global.fetch = async (url) => {
|
| 113 |
+
const asString = String(url);
|
| 114 |
+
calls.push(asString);
|
| 115 |
+
|
| 116 |
+
if (asString.includes('/events?')) {
|
| 117 |
+
return {
|
| 118 |
+
ok: true,
|
| 119 |
+
json: async () => [{ id: 'event-1' }],
|
| 120 |
+
};
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
if (asString.includes('/events/event-1/odds')) {
|
| 124 |
+
return {
|
| 125 |
+
ok: true,
|
| 126 |
+
status: 200,
|
| 127 |
+
json: async () => ({
|
| 128 |
+
id: 'event-1',
|
| 129 |
+
away_team: 'Yankees',
|
| 130 |
+
home_team: 'Red Sox',
|
| 131 |
+
bookmakers: [
|
| 132 |
+
{
|
| 133 |
+
title: 'FanDuel',
|
| 134 |
+
markets: [
|
| 135 |
+
{
|
| 136 |
+
key: 'batter_total_bases',
|
| 137 |
+
outcomes: [
|
| 138 |
+
{ name: 'Aaron Judge', description: 'Over', point: 1.5, price: -110 },
|
| 139 |
+
],
|
| 140 |
+
},
|
| 141 |
+
],
|
| 142 |
+
},
|
| 143 |
+
],
|
| 144 |
+
}),
|
| 145 |
+
};
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
throw new Error(`Unexpected fetch URL: ${asString}`);
|
| 149 |
+
};
|
| 150 |
+
|
| 151 |
+
try {
|
| 152 |
+
const entries = await fetchOddsApiEntries({
|
| 153 |
+
oddsApiBaseUrl: 'https://api.the-odds-api.com/v4',
|
| 154 |
+
oddsApiSportKey: 'baseball_mlb',
|
| 155 |
+
oddsApiKey: 'test-key',
|
| 156 |
+
oddsApiRegions: 'us',
|
| 157 |
+
oddsApiMarkets: ['batter_total_bases'],
|
| 158 |
+
});
|
| 159 |
+
|
| 160 |
+
assert.equal(entries.length, 1);
|
| 161 |
+
assert.equal(entries[0].playerName, 'Aaron Judge');
|
| 162 |
+
assert.ok(calls.some((url) => url.includes('/events?')));
|
| 163 |
+
assert.ok(calls.some((url) => url.includes('/events/event-1/odds')));
|
| 164 |
+
} finally {
|
| 165 |
+
global.fetch = originalFetch;
|
| 166 |
+
}
|
| 167 |
+
});
|