Codex commited on
Commit ·
c93121f
1
Parent(s): af8ceca
Update alert thresholds and fallback order
Browse files- src/config.js +2 -2
- src/embeds.js +8 -2
- src/market-scanner.js +11 -1
- test/alerts.test.js +16 -0
- test/market-scanner.test.js +3 -3
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 ||
|
| 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 ||
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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, '
|
| 813 |
-
assert.equal(soto.sharpSourceMode, '
|
| 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 |
});
|