Codex commited on
Commit
a2fc38f
·
1 Parent(s): 5736302

Tighten sharp alert pregame thresholds

Browse files
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
  {