@@ -107,11 +107,46 @@ interface ArbOpportunity {
107107 detected_at: string ;
108108}
109109
110+ interface LowHoldOpportunity {
111+ id: string ;
112+ event_id: string ;
113+ event_name: string ;
114+ sport: string ;
115+ league: string ;
116+ market_type: string ;
117+ line: number | null ;
118+ hold_percentage: number ;
119+ side1: LowHoldSide ;
120+ side2: LowHoldSide ;
121+ side3: LowHoldSide | null ; // 3-way markets (soccer, hockey)
122+ is_live: boolean ;
123+ is_alternate_line: boolean ;
124+ all_books: string [];
125+ confidence: number ;
126+ odds_age_seconds: number ;
127+ possibly_stale: boolean ;
128+ detected_at: string ;
129+ }
130+
131+ interface LowHoldSide {
132+ selection: string ;
133+ books: string [];
134+ line: number | null ;
135+ odds: {
136+ american: number ;
137+ decimal: number ;
138+ implied_probability: number ;
139+ fair_probability: number ;
140+ };
141+ deep_links: Record <string , string >;
142+ }
143+
110144// ─── State Management ─────────────────────────────────────────────────────
111145// Keyed by odds line ID (e.g. "draftkings_33483153_moneyline_PHO")
112146const oddsMap = new Map <string , OddsLine >();
113147const evMap = new Map <string , EVOpportunity >();
114148const arbMap = new Map <string , ArbOpportunity >();
149+ const lowHoldMap = new Map <string , LowHoldOpportunity >();
115150let isReady = false ;
116151
117152// ─── Connect ──────────────────────────────────────────────────────────────
@@ -132,6 +167,7 @@ eventSource.addEventListener('connected', (e) => {
132167 oddsMap .clear ();
133168 evMap .clear ();
134169 arbMap .clear ();
170+ lowHoldMap .clear ();
135171 isReady = false ;
136172 }
137173});
@@ -143,8 +179,7 @@ eventSource.addEventListener('snapshot', (e) => {
143179 // Odds snapshots: keyed by sportsbook name
144180 // e.g. { "draftkings": [OddsLine, ...], "fanduel": [OddsLine, ...] }
145181 for (const [key, value] of Object .entries (data )) {
146- if (Array .isArray (value ) && key !== ' ev' && key !== ' arbitrage'
147- && key !== ' middles' && key !== ' low_hold' ) {
182+ if (Array .isArray (value ) && ! [' ev' , ' arbitrage' , ' middles' , ' low_hold' ].includes (key )) {
148183 // Odds data — key is sportsbook name
149184 for (const odds of value as OddsLine []) {
150185 oddsMap .set (odds .id , odds );
@@ -163,13 +198,18 @@ eventSource.addEventListener('snapshot', (e) => {
163198 arbMap .set (arb .id , arb );
164199 }
165200 }
201+ if (data .low_hold ) {
202+ for (const lh of data .low_hold as LowHoldOpportunity []) {
203+ lowHoldMap .set (lh .id , lh );
204+ }
205+ }
166206});
167207
168208eventSource .addEventListener (' snapshot:complete' , (e ) => {
169209 const { books, total_odds } = JSON .parse (e .data );
170210 isReady = true ;
171211 console .log (` Ready: ${total_odds } odds from ${books .join (' , ' )} ` );
172- console .log (` ${evMap .size } EV opportunities , ${arbMap .size } arb opportunities ` );
212+ console .log (` ${evMap .size } EV, ${arbMap .size } arb, ${ lowHoldMap . size } low-hold opportunities ` );
173213});
174214
175215// ─── Real-time odds updates (DELTAS — merge into local state) ─────────────
@@ -225,6 +265,22 @@ eventSource.addEventListener('arb:expired', (e) => {
225265 }
226266});
227267
268+ eventSource .addEventListener (' low_hold:detected' , (e ) => {
269+ const opps = JSON .parse (e .data ) as LowHoldOpportunity [];
270+ for (const opp of opps ) {
271+ if (opp .possibly_stale ) continue ;
272+ lowHoldMap .set (opp .id , opp );
273+ console .log (` Low Hold: ${opp .hold_percentage }% — ${opp .event_name } (${opp .market_type }) ` );
274+ }
275+ });
276+
277+ eventSource .addEventListener (' low_hold:expired' , (e ) => {
278+ const { expired } = JSON .parse (e .data ) as { expired: string [] };
279+ for (const id of expired ) {
280+ lowHoldMap .delete (id );
281+ }
282+ });
283+
228284// ─── Health monitoring ────────────────────────────────────────────────────
229285let lastHeartbeat = Date .now ();
230286
@@ -306,7 +362,7 @@ function americanToProbability(american: number): number {
306362
307363## Staleness Metadata
308364
309- EV and arbitrage opportunity responses include staleness information to help you filter out opportunities based on stale odds:
365+ EV, arbitrage, and low-hold opportunity responses include staleness information to help you filter out opportunities based on stale odds:
310366
311367``` typescript
312368interface EVOpportunity {
@@ -345,6 +401,7 @@ eventSource.addEventListener('connected', (e) => {
345401 oddsMap .clear ();
346402 evMap .clear ();
347403 arbMap .clear ();
404+ lowHoldMap .clear ();
348405 isReady = false ;
349406 }
350407});
0 commit comments