Codex commited on
Commit
b8c237d
·
1 Parent(s): b833e28

Clarify odds scanner alert displays

Browse files
Files changed (2) hide show
  1. src/embeds.js +48 -8
  2. test/alerts.test.js +33 -0
src/embeds.js CHANGED
@@ -668,8 +668,9 @@ export function buildSteamEmbed(title, rows, filters = {}) {
668
  rows.map((row, index) => ({
669
  name: `${index + 1}. ${row.playerName} - ${row.marketLabel}`,
670
  value: [
671
- `Moved Book: ${row.movedBook} | ${row.oldOddsInput} -> ${row.newOddsInput}`,
672
- `Sharp: ${row.sharpBook} @ ${row.sharpOddsInput} | Delta: ${formatPercent(row.impliedDeltaPct * 100)}`,
 
673
  ].join('\n'),
674
  inline: false,
675
  }))
@@ -684,8 +685,9 @@ export function buildSharpAlertEmbed(row) {
684
  .setTitle('Sharp Edge Alert')
685
  .setDescription(`${row.playerName} - ${row.marketLabel}`)
686
  .addFields(
687
- { name: 'Sharp', value: `${row.sharpBook} @ ${row.sharpOddsInput}`, inline: true },
688
- { name: 'Best Soft', value: `${row.bestSoftBook} @ ${row.bestSoftOddsInput}`, inline: true },
 
689
  { name: 'Edge', value: formatPercent(row.edgeImpliedPct * 100), inline: true },
690
  { name: 'Width', value: formatPercent(row.marketWidthPct * 100), inline: true },
691
  { name: 'Books Compared', value: String(row.booksCompared), inline: true },
@@ -699,8 +701,9 @@ export function buildStaleBookAlertEmbed(row, staleEntry) {
699
  .setTitle('Stale Book Alert')
700
  .setDescription(`${row.playerName} - ${row.marketLabel}`)
701
  .addFields(
702
- { name: 'Sharp', value: `${row.sharpBook} @ ${row.sharpOddsInput}`, inline: true },
703
- { name: 'Stale Book', value: `${staleEntry.book} @ ${staleEntry.oddsInput}`, inline: true },
 
704
  { name: 'Current Edge', value: formatPercent(((row.sharpImpliedProbability ?? 0) - (staleEntry.impliedProbability ?? 0)) * 100), inline: true },
705
  )
706
  .setFooter({ text: 'Sharp and at least one other book moved, but this book did not.' });
@@ -712,8 +715,13 @@ export function buildReverseAlertEmbed(row, movement) {
712
  .setTitle('Reverse Alert')
713
  .setDescription(`${row.playerName} - ${row.marketLabel}`)
714
  .addFields(
715
- { name: 'Sharp', value: `${row.sharpBook} @ ${row.sharpOddsInput}`, inline: true },
716
- { name: 'Soft Move', value: `${movement.book} ${movement.previousEntry.oddsInput} -> ${movement.currentEntry.oddsInput}`, inline: true },
 
 
 
 
 
717
  { name: 'Move Size', value: formatPercent(movement.impliedDeltaPct * 100), inline: true },
718
  )
719
  .setFooter({ text: 'A soft book moved materially while the sharp book stayed flat.' });
@@ -989,6 +997,24 @@ function formatSignedCurrency(value) {
989
  return `${value >= 0 ? '+' : '-'}${formatCurrency(Math.abs(value))}`;
990
  }
991
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
992
  function formatPercent(value) {
993
  return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`;
994
  }
@@ -997,6 +1023,20 @@ function formatUnits(value) {
997
  return `${(value ?? 0).toFixed(2)}u`;
998
  }
999
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1000
  function truncate(value, maxLength) {
1001
  if (value.length <= maxLength) {
1002
  return value;
 
668
  rows.map((row, index) => ({
669
  name: `${index + 1}. ${row.playerName} - ${row.marketLabel}`,
670
  value: [
671
+ `Selection: ${formatSelection(row.side, row.lineValue)}`,
672
+ `Moved Book: ${row.movedBook} | ${formatAmericanOdds(row.oldOddsInput)} -> ${formatAmericanOdds(row.newOddsInput)}`,
673
+ `Sharp: ${row.sharpBook} @ ${formatAmericanOdds(row.sharpOddsInput)} | Delta: ${formatPercent(row.impliedDeltaPct * 100)}`,
674
  ].join('\n'),
675
  inline: false,
676
  }))
 
685
  .setTitle('Sharp Edge Alert')
686
  .setDescription(`${row.playerName} - ${row.marketLabel}`)
687
  .addFields(
688
+ { name: 'Selection', value: formatSelection(row.side, row.lineValue), inline: true },
689
+ { name: 'Sharp', value: `${row.sharpBook} @ ${formatAmericanOdds(row.sharpOddsInput)}`, inline: true },
690
+ { name: 'Best Soft', value: `${row.bestSoftBook} @ ${formatAmericanOdds(row.bestSoftOddsInput)}`, inline: true },
691
  { name: 'Edge', value: formatPercent(row.edgeImpliedPct * 100), inline: true },
692
  { name: 'Width', value: formatPercent(row.marketWidthPct * 100), inline: true },
693
  { name: 'Books Compared', value: String(row.booksCompared), inline: true },
 
701
  .setTitle('Stale Book Alert')
702
  .setDescription(`${row.playerName} - ${row.marketLabel}`)
703
  .addFields(
704
+ { name: 'Selection', value: formatSelection(row.side, row.lineValue), inline: true },
705
+ { name: 'Sharp', value: `${row.sharpBook} @ ${formatAmericanOdds(row.sharpOddsInput)}`, inline: true },
706
+ { name: 'Stale Book', value: `${staleEntry.book} @ ${formatAmericanOdds(staleEntry.oddsInput)}`, inline: true },
707
  { name: 'Current Edge', value: formatPercent(((row.sharpImpliedProbability ?? 0) - (staleEntry.impliedProbability ?? 0)) * 100), inline: true },
708
  )
709
  .setFooter({ text: 'Sharp and at least one other book moved, but this book did not.' });
 
715
  .setTitle('Reverse Alert')
716
  .setDescription(`${row.playerName} - ${row.marketLabel}`)
717
  .addFields(
718
+ { name: 'Selection', value: formatSelection(row.side, row.lineValue), inline: true },
719
+ { name: 'Sharp', value: `${row.sharpBook} @ ${formatAmericanOdds(row.sharpOddsInput)}`, inline: true },
720
+ {
721
+ name: 'Soft Move',
722
+ value: `${movement.book} ${formatSelection(movement.currentEntry.side, movement.currentEntry.lineValue)} ${formatAmericanOdds(movement.previousEntry.oddsInput)} -> ${formatAmericanOdds(movement.currentEntry.oddsInput)}`,
723
+ inline: true,
724
+ },
725
  { name: 'Move Size', value: formatPercent(movement.impliedDeltaPct * 100), inline: true },
726
  )
727
  .setFooter({ text: 'A soft book moved materially while the sharp book stayed flat.' });
 
997
  return `${value >= 0 ? '+' : '-'}${formatCurrency(Math.abs(value))}`;
998
  }
999
 
1000
+ function formatAmericanOdds(value) {
1001
+ const stringValue = String(value ?? '').trim();
1002
+ if (!stringValue) {
1003
+ return 'N/A';
1004
+ }
1005
+
1006
+ const numeric = Number(stringValue);
1007
+ if (!Number.isFinite(numeric)) {
1008
+ return stringValue;
1009
+ }
1010
+
1011
+ if (numeric > 0) {
1012
+ return `+${numeric}`;
1013
+ }
1014
+
1015
+ return `${numeric}`;
1016
+ }
1017
+
1018
  function formatPercent(value) {
1019
  return `${value >= 0 ? '+' : ''}${value.toFixed(2)}%`;
1020
  }
 
1023
  return `${(value ?? 0).toFixed(2)}u`;
1024
  }
1025
 
1026
+ function formatSelection(side, lineValue) {
1027
+ const normalizedSide = String(side ?? '').trim().toLowerCase();
1028
+ const sideLabel = {
1029
+ over: 'Over',
1030
+ under: 'Under',
1031
+ yes: 'Yes',
1032
+ no: 'No',
1033
+ }[normalizedSide] ?? (normalizedSide ? normalizedSide.toUpperCase() : 'Unknown');
1034
+
1035
+ return lineValue !== null && lineValue !== undefined
1036
+ ? `${sideLabel} ${lineValue}`
1037
+ : sideLabel;
1038
+ }
1039
+
1040
  function truncate(value, maxLength) {
1041
  if (value.length <= maxLength) {
1042
  return value;
test/alerts.test.js CHANGED
@@ -6,6 +6,7 @@ import {
6
  buildAlertsEmbed,
7
  buildCommandsEmbed,
8
  buildAlertRoleButtonId,
 
9
  parseAlertRoleButtonId,
10
  } from '../src/embeds.js';
11
 
@@ -41,3 +42,35 @@ test('builds two rows of alert role buttons and round-trips custom ids', () => {
41
  const customId = buildAlertRoleButtonId('RIIPAlerts');
42
  assert.equal(parseAlertRoleButtonId(customId), 'RIIPAlerts');
43
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  buildAlertsEmbed,
7
  buildCommandsEmbed,
8
  buildAlertRoleButtonId,
9
+ buildReverseAlertEmbed,
10
  parseAlertRoleButtonId,
11
  } from '../src/embeds.js';
12
 
 
42
  const customId = buildAlertRoleButtonId('RIIPAlerts');
43
  assert.equal(parseAlertRoleButtonId(customId), 'RIIPAlerts');
44
  });
45
+
46
+ test('builds reverse alerts with explicit selection and signed odds', () => {
47
+ const embed = buildReverseAlertEmbed(
48
+ {
49
+ playerName: 'Shane Smith',
50
+ marketLabel: 'Pitcher Strikeouts',
51
+ side: 'over',
52
+ lineValue: 5.5,
53
+ sharpBook: 'BetMGM',
54
+ sharpOddsInput: '100',
55
+ },
56
+ {
57
+ book: 'DraftKings',
58
+ impliedDeltaPct: 0.0327,
59
+ previousEntry: {
60
+ side: 'over',
61
+ lineValue: 5.5,
62
+ oddsInput: '114',
63
+ },
64
+ currentEntry: {
65
+ side: 'over',
66
+ lineValue: 5.5,
67
+ oddsInput: '100',
68
+ },
69
+ },
70
+ ).toJSON();
71
+
72
+ assert.equal(embed.fields[0].name, 'Selection');
73
+ assert.equal(embed.fields[0].value, 'Over 5.5');
74
+ assert.match(embed.fields[1].value, /BetMGM @ \+100/);
75
+ assert.match(embed.fields[2].value, /DraftKings Over 5\.5 \+114 -> \+100/);
76
+ });