Codex commited on
Commit ·
9331364
1
Parent(s): e1eb4d1
Fix Caesars odds API home run lookups
Browse files- src/constants.js +1 -1
- src/market-scanner.js +18 -7
- test/market-scanner.test.js +78 -1
src/constants.js
CHANGED
|
@@ -2,7 +2,7 @@ export const BOOK_CHOICES = [
|
|
| 2 |
{ name: 'FanDuel', value: 'FanDuel' },
|
| 3 |
{ name: 'DraftKings', value: 'DraftKings' },
|
| 4 |
{ name: 'BetMGM', value: 'BetMGM' },
|
| 5 |
-
{ name: '
|
| 6 |
{ name: 'Bet365', value: 'Bet365' },
|
| 7 |
];
|
| 8 |
|
|
|
|
| 2 |
{ name: 'FanDuel', value: 'FanDuel' },
|
| 3 |
{ name: 'DraftKings', value: 'DraftKings' },
|
| 4 |
{ name: 'BetMGM', value: 'BetMGM' },
|
| 5 |
+
{ name: 'Caesars', value: 'Caesars' },
|
| 6 |
{ name: 'Bet365', value: 'Bet365' },
|
| 7 |
];
|
| 8 |
|
src/market-scanner.js
CHANGED
|
@@ -521,6 +521,7 @@ export function normalizeOddsApiEntries(payload = []) {
|
|
| 521 |
const entry = {
|
| 522 |
source: 'odds_api',
|
| 523 |
book: bookName,
|
|
|
|
| 524 |
eventName: `${event.away_team} @ ${event.home_team}`,
|
| 525 |
eventId: event.id,
|
| 526 |
team: null,
|
|
@@ -2676,11 +2677,7 @@ export async function fetchOddsApiEntries(config) {
|
|
| 2676 |
for (const event of events) {
|
| 2677 |
const oddsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events/${event.id}/odds`);
|
| 2678 |
oddsUrl.searchParams.set('apiKey', config.oddsApiKey);
|
| 2679 |
-
|
| 2680 |
-
oddsUrl.searchParams.set('bookmakers', config.oddsApiBookmakers.join(','));
|
| 2681 |
-
} else {
|
| 2682 |
-
oddsUrl.searchParams.set('regions', config.oddsApiRegions);
|
| 2683 |
-
}
|
| 2684 |
oddsUrl.searchParams.set('markets', config.oddsApiMarkets.join(','));
|
| 2685 |
oddsUrl.searchParams.set('oddsFormat', 'american');
|
| 2686 |
oddsUrl.searchParams.set('dateFormat', 'iso');
|
|
@@ -2702,7 +2699,21 @@ export async function fetchOddsApiEntries(config) {
|
|
| 2702 |
payload.push(await response.json());
|
| 2703 |
}
|
| 2704 |
|
| 2705 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2706 |
}
|
| 2707 |
|
| 2708 |
function formatZonedParts(date, timeZone) {
|
|
@@ -2777,7 +2788,7 @@ function normalizeBookFilter(book) {
|
|
| 2777 |
if (normalized === 'betmgm') {
|
| 2778 |
return 'BetMGM';
|
| 2779 |
}
|
| 2780 |
-
if (normalized === 'caesars' || normalized === 'williamhill_us') {
|
| 2781 |
return 'Caesars';
|
| 2782 |
}
|
| 2783 |
|
|
|
|
| 521 |
const entry = {
|
| 522 |
source: 'odds_api',
|
| 523 |
book: bookName,
|
| 524 |
+
bookKey: String(bookmaker.key || '').trim(),
|
| 525 |
eventName: `${event.away_team} @ ${event.home_team}`,
|
| 526 |
eventId: event.id,
|
| 527 |
team: null,
|
|
|
|
| 2677 |
for (const event of events) {
|
| 2678 |
const oddsUrl = new URL(`${baseUrl}/sports/${config.oddsApiSportKey}/events/${event.id}/odds`);
|
| 2679 |
oddsUrl.searchParams.set('apiKey', config.oddsApiKey);
|
| 2680 |
+
oddsUrl.searchParams.set('regions', config.oddsApiRegions);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2681 |
oddsUrl.searchParams.set('markets', config.oddsApiMarkets.join(','));
|
| 2682 |
oddsUrl.searchParams.set('oddsFormat', 'american');
|
| 2683 |
oddsUrl.searchParams.set('dateFormat', 'iso');
|
|
|
|
| 2699 |
payload.push(await response.json());
|
| 2700 |
}
|
| 2701 |
|
| 2702 |
+
const normalizedEntries = normalizeOddsApiEntries(payload);
|
| 2703 |
+
if (!Array.isArray(config.oddsApiBookmakers) || config.oddsApiBookmakers.length === 0) {
|
| 2704 |
+
return normalizedEntries;
|
| 2705 |
+
}
|
| 2706 |
+
|
| 2707 |
+
const allowedBooks = new Set(
|
| 2708 |
+
config.oddsApiBookmakers
|
| 2709 |
+
.map((book) => normalizeBookFilter(book))
|
| 2710 |
+
.filter(Boolean),
|
| 2711 |
+
);
|
| 2712 |
+
|
| 2713 |
+
return normalizedEntries.filter((entry) =>
|
| 2714 |
+
allowedBooks.has(normalizeBookFilter(entry.book))
|
| 2715 |
+
|| allowedBooks.has(normalizeBookFilter(entry.bookKey))
|
| 2716 |
+
);
|
| 2717 |
}
|
| 2718 |
|
| 2719 |
function formatZonedParts(date, timeZone) {
|
|
|
|
| 2788 |
if (normalized === 'betmgm') {
|
| 2789 |
return 'BetMGM';
|
| 2790 |
}
|
| 2791 |
+
if (normalized === 'caesars' || normalized === 'williamhill_us' || normalized === 'williamhill us') {
|
| 2792 |
return 'Caesars';
|
| 2793 |
}
|
| 2794 |
|
test/market-scanner.test.js
CHANGED
|
@@ -373,6 +373,7 @@ test('fetches odds api entries from event-level endpoints', async () => {
|
|
| 373 |
home_team: 'Red Sox',
|
| 374 |
bookmakers: [
|
| 375 |
{
|
|
|
|
| 376 |
title: 'FanDuel',
|
| 377 |
markets: [
|
| 378 |
{
|
|
@@ -405,7 +406,8 @@ test('fetches odds api entries from event-level endpoints', async () => {
|
|
| 405 |
assert.equal(entries[0].playerName, 'Aaron Judge');
|
| 406 |
assert.ok(calls.some((url) => url.includes('/events?')));
|
| 407 |
assert.ok(calls.some((url) => url.includes('/events/event-1/odds')));
|
| 408 |
-
assert.ok(calls.some((url) => url.includes('
|
|
|
|
| 409 |
} finally {
|
| 410 |
global.fetch = originalFetch;
|
| 411 |
}
|
|
@@ -439,6 +441,7 @@ test('returns partial odds results when later event requests hit 429', async ()
|
|
| 439 |
home_team: 'Red Sox',
|
| 440 |
bookmakers: [
|
| 441 |
{
|
|
|
|
| 442 |
title: 'FanDuel',
|
| 443 |
markets: [
|
| 444 |
{
|
|
@@ -482,6 +485,80 @@ test('returns partial odds results when later event requests hit 429', async ()
|
|
| 482 |
}
|
| 483 |
});
|
| 484 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
test('matches expected circa mlb filenames', () => {
|
| 486 |
assert.equal(matchesCircaMlbFilename('MLB Props - 2026-4-3.pdf'), true);
|
| 487 |
assert.equal(matchesCircaMlbFilename('MLB Props - 2026-04-03.pdf'), true);
|
|
|
|
| 373 |
home_team: 'Red Sox',
|
| 374 |
bookmakers: [
|
| 375 |
{
|
| 376 |
+
key: 'fanduel',
|
| 377 |
title: 'FanDuel',
|
| 378 |
markets: [
|
| 379 |
{
|
|
|
|
| 406 |
assert.equal(entries[0].playerName, 'Aaron Judge');
|
| 407 |
assert.ok(calls.some((url) => url.includes('/events?')));
|
| 408 |
assert.ok(calls.some((url) => url.includes('/events/event-1/odds')));
|
| 409 |
+
assert.ok(calls.some((url) => url.includes('regions=us')));
|
| 410 |
+
assert.ok(calls.every((url) => !url.includes('bookmakers=')));
|
| 411 |
} finally {
|
| 412 |
global.fetch = originalFetch;
|
| 413 |
}
|
|
|
|
| 441 |
home_team: 'Red Sox',
|
| 442 |
bookmakers: [
|
| 443 |
{
|
| 444 |
+
key: 'fanduel',
|
| 445 |
title: 'FanDuel',
|
| 446 |
markets: [
|
| 447 |
{
|
|
|
|
| 485 |
}
|
| 486 |
});
|
| 487 |
|
| 488 |
+
test('filters event-level odds to configured books after regional fetch', async () => {
|
| 489 |
+
const originalFetch = global.fetch;
|
| 490 |
+
|
| 491 |
+
global.fetch = async (input) => {
|
| 492 |
+
const asString = String(input);
|
| 493 |
+
|
| 494 |
+
if (asString.includes('/events?')) {
|
| 495 |
+
return {
|
| 496 |
+
ok: true,
|
| 497 |
+
status: 200,
|
| 498 |
+
json: async () => [{ id: 'event-1', away_team: 'Royals', home_team: 'Guardians' }],
|
| 499 |
+
};
|
| 500 |
+
}
|
| 501 |
+
|
| 502 |
+
if (asString.includes('/events/event-1/odds')) {
|
| 503 |
+
return {
|
| 504 |
+
ok: true,
|
| 505 |
+
status: 200,
|
| 506 |
+
json: async () => ({
|
| 507 |
+
id: 'event-1',
|
| 508 |
+
away_team: 'Royals',
|
| 509 |
+
home_team: 'Guardians',
|
| 510 |
+
bookmakers: [
|
| 511 |
+
{
|
| 512 |
+
key: 'williamhill_us',
|
| 513 |
+
title: 'Caesars',
|
| 514 |
+
markets: [
|
| 515 |
+
{
|
| 516 |
+
key: 'batter_home_runs',
|
| 517 |
+
outcomes: [
|
| 518 |
+
{ name: 'Yes', description: 'Salvador Perez', price: 475 },
|
| 519 |
+
],
|
| 520 |
+
},
|
| 521 |
+
],
|
| 522 |
+
},
|
| 523 |
+
{
|
| 524 |
+
key: 'espnbet',
|
| 525 |
+
title: 'ESPN BET',
|
| 526 |
+
markets: [
|
| 527 |
+
{
|
| 528 |
+
key: 'batter_home_runs',
|
| 529 |
+
outcomes: [
|
| 530 |
+
{ name: 'Yes', description: 'Salvador Perez', price: 500 },
|
| 531 |
+
],
|
| 532 |
+
},
|
| 533 |
+
],
|
| 534 |
+
},
|
| 535 |
+
],
|
| 536 |
+
}),
|
| 537 |
+
};
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
throw new Error(`Unexpected fetch URL: ${asString}`);
|
| 541 |
+
};
|
| 542 |
+
|
| 543 |
+
try {
|
| 544 |
+
const entries = await fetchOddsApiEntries({
|
| 545 |
+
oddsApiBaseUrl: 'https://api.the-odds-api.com/v4',
|
| 546 |
+
oddsApiSportKey: 'baseball_mlb',
|
| 547 |
+
oddsApiKey: 'test-key',
|
| 548 |
+
oddsApiRegions: 'us',
|
| 549 |
+
oddsApiBookmakers: ['fanduel', 'draftkings', 'betmgm', 'williamhill_us'],
|
| 550 |
+
oddsApiMarkets: ['batter_home_runs'],
|
| 551 |
+
});
|
| 552 |
+
|
| 553 |
+
assert.equal(entries.length, 1);
|
| 554 |
+
assert.equal(entries[0].playerName, 'Salvador Perez');
|
| 555 |
+
assert.equal(entries[0].book, 'Caesars');
|
| 556 |
+
assert.equal(entries[0].oddsInput, '475');
|
| 557 |
+
} finally {
|
| 558 |
+
global.fetch = originalFetch;
|
| 559 |
+
}
|
| 560 |
+
});
|
| 561 |
+
|
| 562 |
test('matches expected circa mlb filenames', () => {
|
| 563 |
assert.equal(matchesCircaMlbFilename('MLB Props - 2026-4-3.pdf'), true);
|
| 564 |
assert.equal(matchesCircaMlbFilename('MLB Props - 2026-04-03.pdf'), true);
|