Codex commited on
Commit ·
a2fc38f
1
Parent(s): 5736302
Tighten sharp alert pregame thresholds
Browse files- src/config.js +2 -0
- src/market-scanner.js +7 -0
- test/market-scanner.test.js +98 -0
src/config.js
CHANGED
|
@@ -31,6 +31,7 @@ export function getConfig() {
|
|
| 31 |
const scanHttpTimeoutMs = Number(process.env.SCAN_HTTP_TIMEOUT_MS || 15000);
|
| 32 |
const directOddsCacheTtlMs = Number(process.env.DIRECT_ODDS_CACHE_TTL_MS || 60000);
|
| 33 |
const sharpEdgeAlertThreshold = Number(process.env.SHARP_EDGE_ALERT_THRESHOLD || 0.04);
|
|
|
|
| 34 |
const staleBookAlertThreshold = Number(process.env.STALE_BOOK_ALERT_THRESHOLD || 0.025);
|
| 35 |
const reverseAlertThreshold = Number(process.env.REVERSE_ALERT_THRESHOLD || 0.03);
|
| 36 |
const marketBoardDefaultLimit = Number(process.env.MARKET_BOARD_DEFAULT_LIMIT || 10);
|
|
@@ -103,6 +104,7 @@ export function getConfig() {
|
|
| 103 |
scanHttpTimeoutMs,
|
| 104 |
directOddsCacheTtlMs,
|
| 105 |
sharpEdgeAlertThreshold,
|
|
|
|
| 106 |
staleBookAlertThreshold,
|
| 107 |
reverseAlertThreshold,
|
| 108 |
marketBoardDefaultLimit,
|
|
|
|
| 31 |
const scanHttpTimeoutMs = Number(process.env.SCAN_HTTP_TIMEOUT_MS || 15000);
|
| 32 |
const directOddsCacheTtlMs = Number(process.env.DIRECT_ODDS_CACHE_TTL_MS || 60000);
|
| 33 |
const sharpEdgeAlertThreshold = Number(process.env.SHARP_EDGE_ALERT_THRESHOLD || 0.04);
|
| 34 |
+
const sharpEdgeAlertMinBooks = Number(process.env.SHARP_EDGE_ALERT_MIN_BOOKS || 5);
|
| 35 |
const staleBookAlertThreshold = Number(process.env.STALE_BOOK_ALERT_THRESHOLD || 0.025);
|
| 36 |
const reverseAlertThreshold = Number(process.env.REVERSE_ALERT_THRESHOLD || 0.03);
|
| 37 |
const marketBoardDefaultLimit = Number(process.env.MARKET_BOARD_DEFAULT_LIMIT || 10);
|
|
|
|
| 104 |
scanHttpTimeoutMs,
|
| 105 |
directOddsCacheTtlMs,
|
| 106 |
sharpEdgeAlertThreshold,
|
| 107 |
+
sharpEdgeAlertMinBooks,
|
| 108 |
staleBookAlertThreshold,
|
| 109 |
reverseAlertThreshold,
|
| 110 |
marketBoardDefaultLimit,
|
src/market-scanner.js
CHANGED
|
@@ -4035,7 +4035,14 @@ export class MarketScanner {
|
|
| 4035 |
}
|
| 4036 |
|
| 4037 |
if (this.config.sharpEdgeAlertsEnabled) {
|
|
|
|
| 4038 |
for (const row of analysis.edgeRows) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4039 |
if (row.edgeImpliedPct < this.config.sharpEdgeAlertThreshold) {
|
| 4040 |
continue;
|
| 4041 |
}
|
|
|
|
| 4035 |
}
|
| 4036 |
|
| 4037 |
if (this.config.sharpEdgeAlertsEnabled) {
|
| 4038 |
+
const now = new Date();
|
| 4039 |
for (const row of analysis.edgeRows) {
|
| 4040 |
+
if (!isPregameSharpRow(row, now)) {
|
| 4041 |
+
continue;
|
| 4042 |
+
}
|
| 4043 |
+
if (row.booksCompared < (this.config.sharpEdgeAlertMinBooks ?? 5)) {
|
| 4044 |
+
continue;
|
| 4045 |
+
}
|
| 4046 |
if (row.edgeImpliedPct < this.config.sharpEdgeAlertThreshold) {
|
| 4047 |
continue;
|
| 4048 |
}
|
test/market-scanner.test.js
CHANGED
|
@@ -577,6 +577,104 @@ test('movement alerts skip games that have already started', async () => {
|
|
| 577 |
assert.equal(sentPayloads.length, 0);
|
| 578 |
});
|
| 579 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
test('sharp analysis prefers Circa and falls back to BetMGM when Circa is missing', () => {
|
| 581 |
const rows = analyzeSharpMarkets([
|
| 582 |
{
|
|
|
|
| 577 |
assert.equal(sentPayloads.length, 0);
|
| 578 |
});
|
| 579 |
|
| 580 |
+
test('sharp edge alerts require pregame rows and at least five books compared', async () => {
|
| 581 |
+
const sentPayloads = [];
|
| 582 |
+
const scanner = new MarketScanner({
|
| 583 |
+
client: {
|
| 584 |
+
channels: {
|
| 585 |
+
fetch: async () => ({
|
| 586 |
+
isTextBased: () => true,
|
| 587 |
+
send: async (payload) => {
|
| 588 |
+
sentPayloads.push(payload);
|
| 589 |
+
return { id: `message-${sentPayloads.length}` };
|
| 590 |
+
},
|
| 591 |
+
}),
|
| 592 |
+
},
|
| 593 |
+
},
|
| 594 |
+
store: {
|
| 595 |
+
canSendScanAlert: async () => true,
|
| 596 |
+
recordScanAlert: async () => {},
|
| 597 |
+
getScanRunById: async () => null,
|
| 598 |
+
getPreviousScanRun: async () => null,
|
| 599 |
+
},
|
| 600 |
+
embeds: {
|
| 601 |
+
buildSharpAlertEmbed: () => ({ data: { title: 'sharp' } }),
|
| 602 |
+
},
|
| 603 |
+
config: {
|
| 604 |
+
enabled: true,
|
| 605 |
+
oddsWorkflowEnabled: true,
|
| 606 |
+
circaWorkflowEnabled: false,
|
| 607 |
+
scanAlertChannelId: 'alerts',
|
| 608 |
+
sharpEdgeAlertsEnabled: true,
|
| 609 |
+
sharpEdgeAlertThreshold: 0.04,
|
| 610 |
+
sharpEdgeAlertMinBooks: 5,
|
| 611 |
+
scanAlertCooldownMinutes: 0,
|
| 612 |
+
staleBookAlertsEnabled: false,
|
| 613 |
+
reverseAlertsEnabled: false,
|
| 614 |
+
},
|
| 615 |
+
logger: {
|
| 616 |
+
log: () => {},
|
| 617 |
+
error: () => {},
|
| 618 |
+
},
|
| 619 |
+
});
|
| 620 |
+
|
| 621 |
+
scanner.collectMarketAnalysis = async () => ({
|
| 622 |
+
circaAlerts: [],
|
| 623 |
+
scanRunId: null,
|
| 624 |
+
edgeRows: [
|
| 625 |
+
{
|
| 626 |
+
marketKey: 'live-row',
|
| 627 |
+
playerName: 'Bryce Harper',
|
| 628 |
+
marketLabel: 'Runs',
|
| 629 |
+
side: 'under',
|
| 630 |
+
lineValue: 0.5,
|
| 631 |
+
sharpBook: 'BetMGM',
|
| 632 |
+
bestSoftBook: 'DraftKings',
|
| 633 |
+
sharpOddsInput: '-275',
|
| 634 |
+
bestSoftOddsInput: '-225',
|
| 635 |
+
edgeImpliedPct: 0.041,
|
| 636 |
+
booksCompared: 5,
|
| 637 |
+
eventCommenceTime: '2026-04-07T00:00:00.000Z',
|
| 638 |
+
entries: [],
|
| 639 |
+
},
|
| 640 |
+
{
|
| 641 |
+
marketKey: 'thin-row',
|
| 642 |
+
playerName: 'Aaron Judge',
|
| 643 |
+
marketLabel: 'Home Runs',
|
| 644 |
+
side: 'yes',
|
| 645 |
+
lineValue: 0.5,
|
| 646 |
+
sharpBook: 'Circa',
|
| 647 |
+
bestSoftBook: 'Caesars',
|
| 648 |
+
sharpOddsInput: '+400',
|
| 649 |
+
bestSoftOddsInput: '+475',
|
| 650 |
+
edgeImpliedPct: 0.052,
|
| 651 |
+
booksCompared: 2,
|
| 652 |
+
eventCommenceTime: '2099-04-08T00:00:00.000Z',
|
| 653 |
+
entries: [],
|
| 654 |
+
},
|
| 655 |
+
{
|
| 656 |
+
marketKey: 'eligible-row',
|
| 657 |
+
playerName: 'Kyle Schwarber',
|
| 658 |
+
marketLabel: 'Home Runs',
|
| 659 |
+
side: 'yes',
|
| 660 |
+
lineValue: 0.5,
|
| 661 |
+
sharpBook: 'Circa',
|
| 662 |
+
bestSoftBook: 'DraftKings',
|
| 663 |
+
sharpOddsInput: '+390',
|
| 664 |
+
bestSoftOddsInput: '+500',
|
| 665 |
+
edgeImpliedPct: 0.061,
|
| 666 |
+
booksCompared: 5,
|
| 667 |
+
eventCommenceTime: '2099-04-08T00:00:00.000Z',
|
| 668 |
+
entries: [],
|
| 669 |
+
},
|
| 670 |
+
],
|
| 671 |
+
});
|
| 672 |
+
|
| 673 |
+
await scanner.runDisagreementScan();
|
| 674 |
+
|
| 675 |
+
assert.equal(sentPayloads.length, 1);
|
| 676 |
+
});
|
| 677 |
+
|
| 678 |
test('sharp analysis prefers Circa and falls back to BetMGM when Circa is missing', () => {
|
| 679 |
const rows = analyzeSharpMarkets([
|
| 680 |
{
|