@@ -9,11 +9,13 @@ const RECONNECT_MAX_DELAY = 60000;
99const MAX_MESSAGE_SIZE = 1024 * 1024 ; // 1MB guard against oversized payloads
1010// Close codes that indicate we should not reconnect
1111const NO_RECONNECT_CODES = new Set ( [ 1008 , 1011 , 4000 , 4001 , 4003 ] ) ;
12+ const PING_INTERVAL = 30000 ; // 30s keepalive for exchanges that need it
1213
1314interface WebSocketConfig {
1415 exchange : Exchange ;
1516 url : string ;
1617 subscribe ?: object ;
18+ ping ?: object ; // Keepalive message to send periodically
1719 parse : ( data : unknown , threshold : number ) => Liquidation [ ] ;
1820}
1921
@@ -142,46 +144,53 @@ const EXCHANGES: WebSocketConfig[] = [
142144 method : 'subscribe' ,
143145 subscription : { type : 'trades' , coin : 'BTC' } ,
144146 } ,
147+ ping : { method : 'ping' } ,
145148 parse : ( data : unknown , threshold : number ) => {
146149 const msg = data as {
147150 channel ?: string ;
148- data ?: {
151+ data ?: Array < {
149152 coin ?: string ;
150153 side ?: string ;
151154 px ?: string ;
152155 sz ?: string ;
153156 time ?: number ;
157+ tid ?: number ;
154158 liquidation ?: boolean ;
155159 startPosition ?: boolean ;
156160 dir ?: string ;
157161 closedPnl ?: string ;
158- } ;
162+ } > ;
159163 } ;
160164
161- // Check if it's a fill with liquidation
162- if ( ! msg . data || ! msg . data . liquidation ) return [ ] ;
163- if ( ! msg . data . coin ?. toUpperCase ( ) . includes ( 'BTC' ) ) return [ ] ;
165+ // Hyperliquid trades channel sends data as an array
166+ if ( ! Array . isArray ( msg . data ) ) return [ ] ;
164167
165- const quantity = parseFloat ( msg . data . sz || '0' ) ;
166- const price = parseFloat ( msg . data . px || '0' ) ;
167- if ( ! isFinite ( quantity ) || ! isFinite ( price ) ) return [ ] ;
168- const valueUsd = quantity * price ;
168+ const results : Liquidation [ ] = [ ] ;
169+ for ( const trade of msg . data ) {
170+ if ( ! trade . liquidation ) continue ;
171+ if ( ! trade . coin ?. toUpperCase ( ) . includes ( 'BTC' ) ) continue ;
169172
170- if ( valueUsd < threshold ) return [ ] ;
173+ const quantity = parseFloat ( trade . sz || '0' ) ;
174+ const price = parseFloat ( trade . px || '0' ) ;
175+ if ( ! isFinite ( quantity ) || ! isFinite ( price ) ) continue ;
176+ const valueUsd = quantity * price ;
171177
172- // Determine side based on direction
173- const isLong = msg . data . side === 'A' || msg . data . dir ?. includes ( 'Long' ) ;
178+ if ( valueUsd < threshold ) continue ;
174179
175- return [ {
176- id : `hl-${ msg . data . time } -${ msg . data . side } ` ,
177- exchange : 'Hyperliquid' ,
178- symbol : msg . data . coin || 'BTC' ,
179- side : isLong ? 'Long' : 'Short' ,
180- quantity,
181- price,
182- valueUsd,
183- timestamp : new Date ( msg . data . time || Date . now ( ) ) ,
184- } ] ;
180+ const isLong = trade . side === 'A' || trade . dir ?. includes ( 'Long' ) ;
181+
182+ results . push ( {
183+ id : `hl-${ trade . tid ?? trade . time } -${ trade . side } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 6 ) } ` ,
184+ exchange : 'Hyperliquid' ,
185+ symbol : trade . coin || 'BTC' ,
186+ side : isLong ? 'Long' : 'Short' ,
187+ quantity,
188+ price,
189+ valueUsd,
190+ timestamp : new Date ( trade . time || Date . now ( ) ) ,
191+ } ) ;
192+ }
193+ return results ;
185194 } ,
186195 } ,
187196 // Aevo - Subscribe to trades and filter liquidations
@@ -190,7 +199,7 @@ const EXCHANGES: WebSocketConfig[] = [
190199 url : 'wss://ws.aevo.xyz' ,
191200 subscribe : {
192201 op : 'subscribe' ,
193- data : [ 'orderbook :BTC-PERP' ] ,
202+ data : [ 'trades :BTC-PERP' ] ,
194203 } ,
195204 parse : ( data : unknown , threshold : number ) => {
196205 const msg = data as {
@@ -247,6 +256,7 @@ export function useMultiExchangeWebSocket(threshold: number = 10000) {
247256 const wsRefs = useRef < Map < Exchange , WebSocket > > ( new Map ( ) ) ;
248257 const reconnectTimeouts = useRef < Map < Exchange , NodeJS . Timeout > > ( new Map ( ) ) ;
249258 const reconnectAttempts = useRef < Map < Exchange , number > > ( new Map ( ) ) ;
259+ const pingIntervals = useRef < Map < Exchange , NodeJS . Timeout > > ( new Map ( ) ) ;
250260 const thresholdRef = useRef ( threshold ) ;
251261
252262 // Keep threshold ref updated
@@ -276,6 +286,19 @@ export function useMultiExchangeWebSocket(threshold: number = 10000) {
276286 if ( config . subscribe ) {
277287 ws . send ( JSON . stringify ( config . subscribe ) ) ;
278288 }
289+
290+ // Start keepalive ping if configured
291+ if ( config . ping ) {
292+ const existingPing = pingIntervals . current . get ( config . exchange ) ;
293+ if ( existingPing ) clearInterval ( existingPing ) ;
294+
295+ const interval = setInterval ( ( ) => {
296+ if ( ws . readyState === WebSocket . OPEN ) {
297+ ws . send ( JSON . stringify ( config . ping ) ) ;
298+ }
299+ } , PING_INTERVAL ) ;
300+ pingIntervals . current . set ( config . exchange , interval ) ;
301+ }
279302 } ;
280303
281304 ws . onmessage = ( event ) => {
@@ -292,8 +315,8 @@ export function useMultiExchangeWebSocket(threshold: number = 10000) {
292315 return [ ...deduped , ...prev ] . slice ( 0 , MAX_LIQUIDATIONS ) ;
293316 } ) ;
294317 }
295- } catch {
296- // Silent parse errors
318+ } catch ( e ) {
319+ console . debug ( `[ ${ config . exchange } ] parse error:` , e ) ;
297320 }
298321 } ;
299322
@@ -304,6 +327,13 @@ export function useMultiExchangeWebSocket(threshold: number = 10000) {
304327 ws . onclose = ( event ) => {
305328 updateConnection ( config . exchange , { isConnected : false } ) ;
306329
330+ // Clear ping interval on close
331+ const pingInterval = pingIntervals . current . get ( config . exchange ) ;
332+ if ( pingInterval ) {
333+ clearInterval ( pingInterval ) ;
334+ pingIntervals . current . delete ( config . exchange ) ;
335+ }
336+
307337 if ( NO_RECONNECT_CODES . has ( event . code ) ) {
308338 console . log ( `${ config . exchange } closed with code ${ event . code } , not reconnecting` ) ;
309339 updateConnection ( config . exchange , { error : `Rejected (code ${ event . code } )` } ) ;
@@ -340,6 +370,9 @@ export function useMultiExchangeWebSocket(threshold: number = 10000) {
340370 reconnectTimeouts . current . clear ( ) ;
341371 reconnectAttempts . current . clear ( ) ;
342372
373+ pingIntervals . current . forEach ( ( interval ) => clearInterval ( interval ) ) ;
374+ pingIntervals . current . clear ( ) ;
375+
343376 wsRefs . current . forEach ( ( ws ) => ws . close ( ) ) ;
344377 wsRefs . current . clear ( ) ;
345378 } , [ ] ) ;
0 commit comments