Codex commited on
Commit ·
45751a3
1
Parent(s): 262b7bd
Reduce odds API load and preserve scan sources
Browse files- src/config.js +6 -0
- src/db.js +4 -0
- src/market-scanner.js +14 -3
- test/market-scanner.test.js +75 -0
src/config.js
CHANGED
|
@@ -9,6 +9,11 @@ export function getConfig() {
|
|
| 9 |
const oddsApiBaseUrl = process.env.ODDS_API_BASE_URL?.trim() || 'https://api.the-odds-api.com/v4';
|
| 10 |
const oddsApiSportKey = process.env.ODDS_API_SPORT_KEY?.trim() || 'baseball_mlb';
|
| 11 |
const oddsApiRegions = process.env.ODDS_API_REGIONS?.trim() || 'us';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
const oddsApiMarkets = (process.env.ODDS_API_MARKETS?.trim()
|
| 13 |
|| 'batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts')
|
| 14 |
.split(',')
|
|
@@ -63,6 +68,7 @@ export function getConfig() {
|
|
| 63 |
oddsApiBaseUrl,
|
| 64 |
oddsApiSportKey,
|
| 65 |
oddsApiRegions,
|
|
|
|
| 66 |
oddsApiMarkets,
|
| 67 |
circaDropboxUrl,
|
| 68 |
scanReportChannelId,
|
|
|
|
| 9 |
const oddsApiBaseUrl = process.env.ODDS_API_BASE_URL?.trim() || 'https://api.the-odds-api.com/v4';
|
| 10 |
const oddsApiSportKey = process.env.ODDS_API_SPORT_KEY?.trim() || 'baseball_mlb';
|
| 11 |
const oddsApiRegions = process.env.ODDS_API_REGIONS?.trim() || 'us';
|
| 12 |
+
const oddsApiBookmakers = (process.env.ODDS_API_BOOKMAKERS?.trim()
|
| 13 |
+
|| 'fanduel,draftkings,betmgm,williamhill_us')
|
| 14 |
+
.split(',')
|
| 15 |
+
.map((value) => value.trim())
|
| 16 |
+
.filter(Boolean);
|
| 17 |
const oddsApiMarkets = (process.env.ODDS_API_MARKETS?.trim()
|
| 18 |
|| 'batter_home_runs,batter_hits,batter_total_bases,batter_rbis,batter_runs_scored,batter_hits_runs_rbis,pitcher_strikeouts')
|
| 19 |
.split(',')
|
|
|
|
| 68 |
oddsApiBaseUrl,
|
| 69 |
oddsApiSportKey,
|
| 70 |
oddsApiRegions,
|
| 71 |
+
oddsApiBookmakers,
|
| 72 |
oddsApiMarkets,
|
| 73 |
circaDropboxUrl,
|
| 74 |
scanReportChannelId,
|
src/db.js
CHANGED
|
@@ -920,6 +920,10 @@ export class BetStore {
|
|
| 920 |
entryCount: Number(snapshotRow.entry_count),
|
| 921 |
entries: entryResult.rows.map((entry) => ({
|
| 922 |
marketKey: entry.market_key,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
playerName: entry.player_name,
|
| 924 |
team: entry.team,
|
| 925 |
marketType: entry.market_type,
|
|
|
|
| 920 |
entryCount: Number(snapshotRow.entry_count),
|
| 921 |
entries: entryResult.rows.map((entry) => ({
|
| 922 |
marketKey: entry.market_key,
|
| 923 |
+
source: 'circa',
|
| 924 |
+
book: 'Circa',
|
| 925 |
+
eventName: 'Circa MLB',
|
| 926 |
+
eventId: null,
|
| 927 |
playerName: entry.player_name,
|
| 928 |
team: entry.team,
|
| 929 |
marketType: entry.market_type,
|
src/market-scanner.js
CHANGED
|
@@ -491,9 +491,10 @@ export function normalizeOddsApiEntries(payload = []) {
|
|
| 491 |
point: outcome.point,
|
| 492 |
});
|
| 493 |
const lineValue = normalizeLine(outcome.point, side, market.key);
|
|
|
|
| 494 |
const entry = {
|
| 495 |
-
source:
|
| 496 |
-
book:
|
| 497 |
eventName: `${event.away_team} @ ${event.home_team}`,
|
| 498 |
eventId: event.id,
|
| 499 |
team: null,
|
|
@@ -2631,7 +2632,11 @@ export async function fetchOddsApiEntries(config) {
|
|
| 2631 |
for (const event of events) {
|
| 2632 |
const oddsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events/${event.id}/odds`);
|
| 2633 |
oddsUrl.searchParams.set('apiKey', config.oddsApiKey);
|
| 2634 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2635 |
oddsUrl.searchParams.set('markets', config.oddsApiMarkets.join(','));
|
| 2636 |
oddsUrl.searchParams.set('oddsFormat', 'american');
|
| 2637 |
oddsUrl.searchParams.set('dateFormat', 'iso');
|
|
@@ -2640,6 +2645,12 @@ export async function fetchOddsApiEntries(config) {
|
|
| 2640 |
if (response.status === 404 || response.status === 422) {
|
| 2641 |
continue;
|
| 2642 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2643 |
if (!response.ok) {
|
| 2644 |
throw new Error(`Odds API request failed with ${response.status}`);
|
| 2645 |
}
|
|
|
|
| 491 |
point: outcome.point,
|
| 492 |
});
|
| 493 |
const lineValue = normalizeLine(outcome.point, side, market.key);
|
| 494 |
+
const bookName = normalizeWhitespace(bookmaker.title || bookmaker.key || 'Unknown book');
|
| 495 |
const entry = {
|
| 496 |
+
source: 'odds_api',
|
| 497 |
+
book: bookName,
|
| 498 |
eventName: `${event.away_team} @ ${event.home_team}`,
|
| 499 |
eventId: event.id,
|
| 500 |
team: null,
|
|
|
|
| 2632 |
for (const event of events) {
|
| 2633 |
const oddsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events/${event.id}/odds`);
|
| 2634 |
oddsUrl.searchParams.set('apiKey', config.oddsApiKey);
|
| 2635 |
+
if (Array.isArray(config.oddsApiBookmakers) && config.oddsApiBookmakers.length > 0) {
|
| 2636 |
+
oddsUrl.searchParams.set('bookmakers', config.oddsApiBookmakers.join(','));
|
| 2637 |
+
} else {
|
| 2638 |
+
oddsUrl.searchParams.set('regions', config.oddsApiRegions);
|
| 2639 |
+
}
|
| 2640 |
oddsUrl.searchParams.set('markets', config.oddsApiMarkets.join(','));
|
| 2641 |
oddsUrl.searchParams.set('oddsFormat', 'american');
|
| 2642 |
oddsUrl.searchParams.set('dateFormat', 'iso');
|
|
|
|
| 2645 |
if (response.status === 404 || response.status === 422) {
|
| 2646 |
continue;
|
| 2647 |
}
|
| 2648 |
+
if (response.status === 429) {
|
| 2649 |
+
if (payload.length > 0) {
|
| 2650 |
+
break;
|
| 2651 |
+
}
|
| 2652 |
+
throw new Error('Odds API request failed with 429. Narrow ODDS_API_BOOKMAKERS or reduce scan frequency.');
|
| 2653 |
+
}
|
| 2654 |
if (!response.ok) {
|
| 2655 |
throw new Error(`Odds API request failed with ${response.status}`);
|
| 2656 |
}
|
test/market-scanner.test.js
CHANGED
|
@@ -44,6 +44,8 @@ test('normalizes odds api entries into shared market keys', () => {
|
|
| 44 |
assert.equal(entries.length, 1);
|
| 45 |
assert.equal(entries[0].marketKey, buildMarketKey(entries[0]));
|
| 46 |
assert.equal(entries[0].side, 'over');
|
|
|
|
|
|
|
| 47 |
});
|
| 48 |
|
| 49 |
test('parses circa OCR lines into normalized entries', () => {
|
|
@@ -394,6 +396,7 @@ test('fetches odds api entries from event-level endpoints', async () => {
|
|
| 394 |
oddsApiSportKey: 'baseball_mlb',
|
| 395 |
oddsApiKey: 'test-key',
|
| 396 |
oddsApiRegions: 'us',
|
|
|
|
| 397 |
oddsApiMarkets: ['batter_total_bases'],
|
| 398 |
});
|
| 399 |
|
|
@@ -401,6 +404,78 @@ test('fetches odds api entries from event-level endpoints', async () => {
|
|
| 401 |
assert.equal(entries[0].playerName, 'Aaron Judge');
|
| 402 |
assert.ok(calls.some((url) => url.includes('/events?')));
|
| 403 |
assert.ok(calls.some((url) => url.includes('/events/event-1/odds')));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 404 |
} finally {
|
| 405 |
global.fetch = originalFetch;
|
| 406 |
}
|
|
|
|
| 44 |
assert.equal(entries.length, 1);
|
| 45 |
assert.equal(entries[0].marketKey, buildMarketKey(entries[0]));
|
| 46 |
assert.equal(entries[0].side, 'over');
|
| 47 |
+
assert.equal(entries[0].source, 'odds_api');
|
| 48 |
+
assert.equal(entries[0].book, 'FanDuel');
|
| 49 |
});
|
| 50 |
|
| 51 |
test('parses circa OCR lines into normalized entries', () => {
|
|
|
|
| 396 |
oddsApiSportKey: 'baseball_mlb',
|
| 397 |
oddsApiKey: 'test-key',
|
| 398 |
oddsApiRegions: 'us',
|
| 399 |
+
oddsApiBookmakers: ['fanduel', 'draftkings', 'betmgm', 'williamhill_us'],
|
| 400 |
oddsApiMarkets: ['batter_total_bases'],
|
| 401 |
});
|
| 402 |
|
|
|
|
| 404 |
assert.equal(entries[0].playerName, 'Aaron Judge');
|
| 405 |
assert.ok(calls.some((url) => url.includes('/events?')));
|
| 406 |
assert.ok(calls.some((url) => url.includes('/events/event-1/odds')));
|
| 407 |
+
assert.ok(calls.some((url) => url.includes('bookmakers=fanduel%2Cdraftkings%2Cbetmgm%2Cwilliamhill_us')));
|
| 408 |
+
} finally {
|
| 409 |
+
global.fetch = originalFetch;
|
| 410 |
+
}
|
| 411 |
+
});
|
| 412 |
+
|
| 413 |
+
test('returns partial odds results when later event requests hit 429', async () => {
|
| 414 |
+
const originalFetch = global.fetch;
|
| 415 |
+
const calls = [];
|
| 416 |
+
global.fetch = async (input) => {
|
| 417 |
+
const asString = String(input);
|
| 418 |
+
calls.push(asString);
|
| 419 |
+
|
| 420 |
+
if (asString.includes('/events?')) {
|
| 421 |
+
return {
|
| 422 |
+
ok: true,
|
| 423 |
+
status: 200,
|
| 424 |
+
json: async () => ([
|
| 425 |
+
{ id: 'event-1', away_team: 'Yankees', home_team: 'Red Sox' },
|
| 426 |
+
{ id: 'event-2', away_team: 'Dodgers', home_team: 'Padres' },
|
| 427 |
+
]),
|
| 428 |
+
};
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
if (asString.includes('/events/event-1/odds')) {
|
| 432 |
+
return {
|
| 433 |
+
ok: true,
|
| 434 |
+
status: 200,
|
| 435 |
+
json: async () => ({
|
| 436 |
+
id: 'event-1',
|
| 437 |
+
away_team: 'Yankees',
|
| 438 |
+
home_team: 'Red Sox',
|
| 439 |
+
bookmakers: [
|
| 440 |
+
{
|
| 441 |
+
title: 'FanDuel',
|
| 442 |
+
markets: [
|
| 443 |
+
{
|
| 444 |
+
key: 'batter_total_bases',
|
| 445 |
+
outcomes: [
|
| 446 |
+
{ name: 'Aaron Judge', description: 'Over', point: 1.5, price: -110 },
|
| 447 |
+
],
|
| 448 |
+
},
|
| 449 |
+
],
|
| 450 |
+
},
|
| 451 |
+
],
|
| 452 |
+
}),
|
| 453 |
+
};
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
if (asString.includes('/events/event-2/odds')) {
|
| 457 |
+
return {
|
| 458 |
+
ok: false,
|
| 459 |
+
status: 429,
|
| 460 |
+
};
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
throw new Error(`Unexpected fetch URL: ${asString}`);
|
| 464 |
+
};
|
| 465 |
+
|
| 466 |
+
try {
|
| 467 |
+
const entries = await fetchOddsApiEntries({
|
| 468 |
+
oddsApiBaseUrl: 'https://api.the-odds-api.com/v4',
|
| 469 |
+
oddsApiSportKey: 'baseball_mlb',
|
| 470 |
+
oddsApiKey: 'test-key',
|
| 471 |
+
oddsApiRegions: 'us',
|
| 472 |
+
oddsApiBookmakers: ['fanduel'],
|
| 473 |
+
oddsApiMarkets: ['batter_total_bases'],
|
| 474 |
+
});
|
| 475 |
+
|
| 476 |
+
assert.equal(entries.length, 1);
|
| 477 |
+
assert.equal(entries[0].playerName, 'Aaron Judge');
|
| 478 |
+
assert.ok(calls.some((url) => url.includes('/events/event-2/odds')));
|
| 479 |
} finally {
|
| 480 |
global.fetch = originalFetch;
|
| 481 |
}
|