import { asNumberIfValid , upperCaseSymbols } from '../handy' ; import { BookChange , BookPriceLevel , BookTicker , DerivativeTicker , FilterForExchange , Liquidation , Trade } from '../types' ; import { Mapper , PendingTickerInfoHelper } from './mapper' ; export const bitmexTradesMapper = { canHandle ( message ) { return message . table === 'trade' && message . action === 'insert' ; } , getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; return [ { channel : 'trade' , symbols } ] ; } , * map ( bitmexTradesMessage , localTimestamp ) { for ( const bitmexTrade of bitmexTradesMessage . data ) { const trade = { type : 'trade' , symbol : bitmexTrade . symbol , exchange : 'bitmex' , id : bitmexTrade . trdMatchID , price : bitmexTrade . price , amount : bitmexTrade . size , side : bitmexTrade . side !== undefined ? ( bitmexTrade . side === 'Buy' ? 'buy' : 'sell' ) : 'unknown' , timestamp : new Date ( bitmexTrade . timestamp ) , localTimestamp : localTimestamp } ; yield trade ; } } } ; export class BitmexBookChangeMapper implements Mapper < 'bitmex' , BookChange > { private readonly _idToPriceLevelMap : Map < number , number > = new Map ( ) ; canHandle ( message ) { return message . table === 'orderBookL2' ; } getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; return [ { channel : 'orderBookL2' , symbols } as const ] ; } * map ( bitmexOrderBookL2Message , localTimestamp ) { let bitmexBookMessagesGrouppedBySymbol ; if ( bitmexOrderBookL2Message . action === 'partial' ) { bitmexBookMessagesGrouppedBySymbol = bitmexOrderBookL2Message . data . reduce ( ( prev , current ) => { if ( prev [ current . symbol ] ) { prev [ current . symbol ] . push ( current ) ; } else { prev [ current . symbol ] = [ current ] ; } return prev ; } , { } as { [ key ] : typeof bitmexOrderBookL2Message . data ; } ) ; if ( bitmexOrderBookL2Message . data . length === 0 && bitmexOrderBookL2Message . filter ?. symbol !== undefined ) { const emptySnapshot = { type : 'book_change' , symbol : bitmexOrderBookL2Message . filter ?. symbol ! , exchange : 'bitmex' , isSnapshot : true , bids : [ ] , asks : [ ] , timestamp : localTimestamp , localTimestamp : localTimestamp } ; yield emptySnapshot ; } } else { bitmexBookMessagesGrouppedBySymbol = { [ bitmexOrderBookL2Message . data [ 0 ] . symbol ] : bitmexOrderBookL2Message . data } ; } for ( let symbol in bitmexBookMessagesGrouppedBySymbol ) { const bids = [ ] ; const asks = [ ] ; for ( const item of bitmexBookMessagesGrouppedBySymbol [ symbol ] ) { if ( item . price !== undefined ) { this . _idToPriceLevelMap . set ( item . id , item . price ) ; } const price = this . _idToPriceLevelMap . get ( item . id ) ; const amount = item . size || 0 ; if ( price === undefined ) { continue ; } if ( item . side === 'Buy' ) { bids . push ( { price , amount } ) ; } else { asks . push ( { price , amount } ) ; } if ( bitmexOrderBookL2Message . action === 'delete' ) { this . _idToPriceLevelMap . delete ( item . id ) ; } } const isSnapshot = bitmexOrderBookL2Message . action === 'partial' ; if ( bids . length > 0 || asks . length > 0 || isSnapshot ) { const bookChange = { type : 'book_change' , symbol , exchange : 'bitmex' , isSnapshot , bids , asks , timestamp : localTimestamp , localTimestamp : localTimestamp } ; yield bookChange ; } } } } export class BitmexDerivativeTickerMapper implements Mapper < 'bitmex' , DerivativeTicker > { private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper ( ) ; canHandle ( message ) { return message . table === 'instrument' ; } getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; return [ { channel : 'instrument' , symbols } as const ] ; } * map ( message , localTimestamp ) { for ( const bitmexInstrument of message . data ) { const isOpen = bitmexInstrument . state === undefined || bitmexInstrument . state === 'Open' || bitmexInstrument . state === 'Closed' ; const isPartial = message . action === 'partial' ; const hasPendingInfo = this . pendingTickerInfoHelper . hasPendingTickerInfo ( bitmexInstrument . symbol ) ; if ( ( isPartial || hasPendingInfo ) && isOpen ) { const pendingTickerInfo = this . pendingTickerInfoHelper . getPendingTickerInfo ( bitmexInstrument . symbol , 'bitmex' ) ; pendingTickerInfo . updateFundingRate ( bitmexInstrument . fundingRate ) ; pendingTickerInfo . updatePredictedFundingRate ( bitmexInstrument . indicativeFundingRate ) ; pendingTickerInfo . updateFundingTimestamp ( bitmexInstrument . fundingTimestamp ? new Date ( bitmexInstrument . fundingTimestamp ) : undefined ) ; pendingTickerInfo . updateIndexPrice ( bitmexInstrument . indicativeSettlePrice ) ; pendingTickerInfo . updateMarkPrice ( bitmexInstrument . markPrice ) ; pendingTickerInfo . updateOpenInterest ( bitmexInstrument . openInterest ) ; pendingTickerInfo . updateLastPrice ( bitmexInstrument . lastPrice ) ; if ( bitmexInstrument . timestamp !== undefined ) { pendingTickerInfo . updateTimestamp ( new Date ( bitmexInstrument . timestamp ) ) ; } if ( pendingTickerInfo . hasChanged ( ) ) { yield pendingTickerInfo . getSnapshot ( localTimestamp ) ; } } } } } export const bitmexLiquidationsMapper = { canHandle ( message ) { return message . table === 'liquidation' && message . action === 'insert' ; } , getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; return [ { channel : 'liquidation' , symbols } ] ; } , * map ( bitmexLiquiationsMessage , localTimestamp ) { for ( const bitmexLiquidation of bitmexLiquiationsMessage . data ) { const liquidation = { type : 'liquidation' , symbol : bitmexLiquidation . symbol , exchange : 'bitmex' , id : bitmexLiquidation . orderID , price : bitmexLiquidation . price , amount : bitmexLiquidation . leavesQty , side : bitmexLiquidation . side === 'Buy' ? 'buy' : 'sell' , timestamp : localTimestamp , localTimestamp : localTimestamp } ; yield liquidation ; } } } ; export const bitmexBookTickerMapper = { canHandle ( message ) { return message . table === 'quote' && message . action === 'insert' ; } , getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; return [ { channel : 'quote' , symbols } ] ; } , * map ( bitmexQuoteMessage , localTimestamp ) { for ( const bitmexQuote of bitmexQuoteMessage . data ) { const ticker = { type : 'book_ticker' , symbol : bitmexQuote . symbol , exchange : 'bitmex' , askAmount : asNumberIfValid ( bitmexQuote . askSize ) , askPrice : asNumberIfValid ( bitmexQuote . askPrice ) , bidPrice : asNumberIfValid ( bitmexQuote . bidPrice ) , bidAmount : asNumberIfValid ( bitmexQuote . bidSize ) , timestamp : new Date ( bitmexQuote . timestamp ) , localTimestamp : localTimestamp } ; yield ticker ; } } } ; type BitmexDataMessage = { table : FilterForExchange [ 'bitmex' ] [ 'channel' ] ; action : 'partial' | 'update' | 'insert' | 'delete' ; } ; type BitmexTradesMessage = BitmexDataMessage & { table : 'trade' ; action : 'insert' ; data : { symbol : string ; trdMatchID : string ; side ? : 'Buy' | 'Sell' ; size : number ; price : number ; timestamp : string ; } [ ] ; } ; type BitmexInstrument = { symbol : string ; state ? : 'Open' | 'Closed' | 'Unlisted' | 'Settled' ; openInterest ? : number | null ; fundingRate ? : number | null ; markPrice ? : number | null ; lastPrice ? : number | null ; indicativeSettlePrice ? : number | null ; indicativeFundingRate ? : number | null ; fundingTimestamp ? : string | null ; timestamp ? : string ; } ; type BitmexInstrumentsMessage = BitmexDataMessage & { table : 'instrument' ; data : BitmexInstrument [ ] ; } ; type BitmexOrderBookL2Message = BitmexDataMessage & { table : 'orderBookL2' ; filter ? : { symbol ? : string ; } ; data : { symbol : string ; id : number ; side : 'Buy' | 'Sell' ; size ? : number ; price ? : number ; } [ ] ; } ; type BitmexLiquidation = BitmexDataMessage & { table : 'liquidation' ; data : { orderID : string ; symbol : string ; side : 'Buy' | 'Sell' ; price : number ; leavesQty : number ; } [ ] ; } ; type BitmexQuote = BitmexDataMessage & { table : 'quote' ; action : 'insert' ; data : [ { timestamp : string ; symbol : string ; bidSize : number ; bidPrice : number ; askPrice : number ; askSize : number ; } ] ; } ;