Codex commited on
Commit
c93121f
·
1 Parent(s): af8ceca

Update alert thresholds and fallback order

Browse files
src/config.js CHANGED
@@ -34,11 +34,11 @@ export function getConfig() {
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 dfsScanMinBooks = Number(process.env.DFS_SCAN_MIN_BOOKS || 2);
38
  const dfsEdgeAlertThreshold = Number(process.env.DFS_EDGE_ALERT_THRESHOLD || sharpEdgeAlertThreshold);
39
  const dfsReverseAlertThreshold = Number(process.env.DFS_REVERSE_ALERT_THRESHOLD || reverseAlertThreshold);
40
  const dfsStaleAlertThreshold = Number(process.env.DFS_STALE_ALERT_THRESHOLD || staleBookAlertThreshold);
41
- const exchangeScanMinBooks = Number(process.env.EXCHANGE_SCAN_MIN_BOOKS || 2);
42
  const exchangeEdgeAlertThreshold = Number(process.env.EXCHANGE_EDGE_ALERT_THRESHOLD || sharpEdgeAlertThreshold);
43
  const exchangeReverseAlertThreshold = Number(process.env.EXCHANGE_REVERSE_ALERT_THRESHOLD || reverseAlertThreshold);
44
  const exchangeStaleAlertThreshold = Number(process.env.EXCHANGE_STALE_ALERT_THRESHOLD || staleBookAlertThreshold);
 
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 dfsScanMinBooks = Number(process.env.DFS_SCAN_MIN_BOOKS || 3);
38
  const dfsEdgeAlertThreshold = Number(process.env.DFS_EDGE_ALERT_THRESHOLD || sharpEdgeAlertThreshold);
39
  const dfsReverseAlertThreshold = Number(process.env.DFS_REVERSE_ALERT_THRESHOLD || reverseAlertThreshold);
40
  const dfsStaleAlertThreshold = Number(process.env.DFS_STALE_ALERT_THRESHOLD || staleBookAlertThreshold);
41
+ const exchangeScanMinBooks = Number(process.env.EXCHANGE_SCAN_MIN_BOOKS || 3);
42
  const exchangeEdgeAlertThreshold = Number(process.env.EXCHANGE_EDGE_ALERT_THRESHOLD || sharpEdgeAlertThreshold);
43
  const exchangeReverseAlertThreshold = Number(process.env.EXCHANGE_REVERSE_ALERT_THRESHOLD || reverseAlertThreshold);
44
  const exchangeStaleAlertThreshold = Number(process.env.EXCHANGE_STALE_ALERT_THRESHOLD || staleBookAlertThreshold);
src/embeds.js CHANGED
@@ -650,7 +650,7 @@ export function buildMarketHealthEmbed(title, rows, filters = {}) {
650
  `Rows: ${row.rows} | Avg Books: ${row.averageBooksCompared.toFixed(2)}`,
651
  `Avg Width: ${formatPercent(row.averageWidthPct * 100)}`,
652
  row.lane === 'sportsbook'
653
- ? `Sharp Source: Circa ${row.circaRows} | MGM fallback ${row.mgmFallbackRows}`
654
  : `Reference: ${row.lane === 'dfs' ? 'DFS consensus baseline' : 'Exchange consensus baseline'} | Rows ${row.consensusRows}`,
655
  ].join('\n'),
656
  inline: false,
@@ -1462,7 +1462,13 @@ function describeSharpSource(row) {
1462
  if (row.lane === 'exchange') {
1463
  return 'Exchange consensus';
1464
  }
1465
- return row.sharpSourceMode === 'circa' ? 'Circa' : 'BetMGM fallback';
 
 
 
 
 
 
1466
  }
1467
 
1468
  function getReferenceLabel(row) {
 
650
  `Rows: ${row.rows} | Avg Books: ${row.averageBooksCompared.toFixed(2)}`,
651
  `Avg Width: ${formatPercent(row.averageWidthPct * 100)}`,
652
  row.lane === 'sportsbook'
653
+ ? `Sharp Source: Circa ${row.circaRows} | FanDuel fallback ${row.fanduelFallbackRows ?? 0} | MGM fallback ${row.mgmFallbackRows}`
654
  : `Reference: ${row.lane === 'dfs' ? 'DFS consensus baseline' : 'Exchange consensus baseline'} | Rows ${row.consensusRows}`,
655
  ].join('\n'),
656
  inline: false,
 
1462
  if (row.lane === 'exchange') {
1463
  return 'Exchange consensus';
1464
  }
1465
+ if (row.sharpSourceMode === 'circa') {
1466
+ return 'Circa';
1467
+ }
1468
+ if (row.sharpSourceMode === 'fanduel_fallback') {
1469
+ return 'FanDuel fallback';
1470
+ }
1471
+ return 'BetMGM fallback';
1472
  }
1473
 
1474
  function getReferenceLabel(row) {
src/market-scanner.js CHANGED
@@ -2215,6 +2215,7 @@ function pickLaneSharpEntry(uniqueEntries = [], lane = 'sportsbook') {
2215
  const normalizedLane = normalizeLaneFilter(lane);
2216
  if (normalizedLane === 'sportsbook') {
2217
  return uniqueEntries.find((entry) => normalizeBookFilter(entry.book) === 'Circa')
 
2218
  ?? uniqueEntries.find((entry) => normalizeBookFilter(entry.book) === 'BetMGM')
2219
  ?? null;
2220
  }
@@ -2232,7 +2233,13 @@ function getSharpSourceMode(entry, lane = 'sportsbook') {
2232
  const normalizedLane = normalizeLaneFilter(lane);
2233
  const normalizedBook = normalizeBookFilter(entry?.book) ?? entry?.book ?? 'Unknown';
2234
  if (normalizedLane === 'sportsbook') {
2235
- return normalizedBook === 'Circa' ? 'circa' : 'mgm_fallback';
 
 
 
 
 
 
2236
  }
2237
  if (normalizedLane === 'dfs') {
2238
  return 'dfs_consensus';
@@ -2372,6 +2379,7 @@ export function analyzeSharpMarkets(entries, config = {}, options = {}) {
2372
  widthTotal: 0,
2373
  booksTotal: 0,
2374
  circaRows: 0,
 
2375
  mgmFallbackRows: 0,
2376
  consensusRows: 0,
2377
  });
@@ -2383,6 +2391,8 @@ export function analyzeSharpMarkets(entries, config = {}, options = {}) {
2383
  current.booksTotal += row.booksCompared;
2384
  if (row.sharpSourceMode === 'circa') {
2385
  current.circaRows += 1;
 
 
2386
  } else if (row.sharpSourceMode === 'mgm_fallback') {
2387
  current.mgmFallbackRows += 1;
2388
  } else {
 
2215
  const normalizedLane = normalizeLaneFilter(lane);
2216
  if (normalizedLane === 'sportsbook') {
2217
  return uniqueEntries.find((entry) => normalizeBookFilter(entry.book) === 'Circa')
2218
+ ?? uniqueEntries.find((entry) => normalizeBookFilter(entry.book) === 'FanDuel')
2219
  ?? uniqueEntries.find((entry) => normalizeBookFilter(entry.book) === 'BetMGM')
2220
  ?? null;
2221
  }
 
2233
  const normalizedLane = normalizeLaneFilter(lane);
2234
  const normalizedBook = normalizeBookFilter(entry?.book) ?? entry?.book ?? 'Unknown';
2235
  if (normalizedLane === 'sportsbook') {
2236
+ if (normalizedBook === 'Circa') {
2237
+ return 'circa';
2238
+ }
2239
+ if (normalizedBook === 'FanDuel') {
2240
+ return 'fanduel_fallback';
2241
+ }
2242
+ return 'mgm_fallback';
2243
  }
2244
  if (normalizedLane === 'dfs') {
2245
  return 'dfs_consensus';
 
2379
  widthTotal: 0,
2380
  booksTotal: 0,
2381
  circaRows: 0,
2382
+ fanduelFallbackRows: 0,
2383
  mgmFallbackRows: 0,
2384
  consensusRows: 0,
2385
  });
 
2391
  current.booksTotal += row.booksCompared;
2392
  if (row.sharpSourceMode === 'circa') {
2393
  current.circaRows += 1;
2394
+ } else if (row.sharpSourceMode === 'fanduel_fallback') {
2395
+ current.fanduelFallbackRows += 1;
2396
  } else if (row.sharpSourceMode === 'mgm_fallback') {
2397
  current.mgmFallbackRows += 1;
2398
  } else {
test/alerts.test.js CHANGED
@@ -47,6 +47,22 @@ test('market health embed uses lane-appropriate reference language', () => {
47
  assert.doesNotMatch(embed.fields[0].value, /MGM fallback/i);
48
  });
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  test('builds the alerts embed with all analyst lines', () => {
51
  const embed = buildAlertsEmbed().toJSON();
52
 
 
47
  assert.doesNotMatch(embed.fields[0].value, /MGM fallback/i);
48
  });
49
 
50
+ test('sportsbook market health embed shows fanDuel before MGM fallback', () => {
51
+ const embed = buildMarketHealthEmbed('Market Health', [{
52
+ marketLabel: 'Home Runs',
53
+ lane: 'sportsbook',
54
+ rows: 4,
55
+ averageBooksCompared: 4,
56
+ averageWidthPct: 0.05,
57
+ circaRows: 2,
58
+ fanduelFallbackRows: 1,
59
+ mgmFallbackRows: 1,
60
+ }], { lane: 'sportsbook' }).toJSON();
61
+
62
+ assert.match(embed.fields[0].value, /FanDuel fallback 1/i);
63
+ assert.match(embed.fields[0].value, /MGM fallback 1/i);
64
+ });
65
+
66
  test('builds the alerts embed with all analyst lines', () => {
67
  const embed = buildAlertsEmbed().toJSON();
68
 
test/market-scanner.test.js CHANGED
@@ -728,7 +728,7 @@ test('sharp edge alerts require pregame rows and at least five books compared',
728
  assert.equal(sentPayloads.length, 1);
729
  });
730
 
731
- test('sharp analysis prefers Circa and falls back to BetMGM when Circa is missing', () => {
732
  const rows = analyzeSharpMarkets([
733
  {
734
  marketKey: 'judge-hr',
@@ -809,8 +809,8 @@ test('sharp analysis prefers Circa and falls back to BetMGM when Circa is missin
809
  assert.equal(judge.sharpSourceMode, 'circa');
810
 
811
  const soto = rows.sharpRows.find((row) => row.playerName === 'Juan Soto');
812
- assert.equal(soto.sharpBook, 'BetMGM');
813
- assert.equal(soto.sharpSourceMode, 'mgm_fallback');
814
 
815
  assert.equal(rows.sharpRows.some((row) => row.playerName === 'Ronald Acuna Jr.'), false);
816
  });
 
728
  assert.equal(sentPayloads.length, 1);
729
  });
730
 
731
+ test('sharp analysis prefers Circa, then FanDuel, then BetMGM for sportsbook reference', () => {
732
  const rows = analyzeSharpMarkets([
733
  {
734
  marketKey: 'judge-hr',
 
809
  assert.equal(judge.sharpSourceMode, 'circa');
810
 
811
  const soto = rows.sharpRows.find((row) => row.playerName === 'Juan Soto');
812
+ assert.equal(soto.sharpBook, 'FanDuel');
813
+ assert.equal(soto.sharpSourceMode, 'fanduel_fallback');
814
 
815
  assert.equal(rows.sharpRows.some((row) => row.playerName === 'Ronald Acuna Jr.'), false);
816
  });