import { asNumberIfValid , upperCaseSymbols } from '../handy' ; import { BookChange , OptionSummary , Trade } from '../types' ; import { Mapper } from './mapper' ; export class BinanceOptionsTradesMapper implements Mapper < 'binance-options' , Trade > { private readonly _lastTradeId = new Map < string , number > ( ) ; canHandle ( message ) { if ( message . stream === undefined ) { return false ; } return message . stream . endsWith ( '@TRADE' ) ; } getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; return [ { channel : 'TRADE' , symbols } as const ] ; } * map ( binanceTradeResponse , localTimestamp ) { const symbol = binanceTradeResponse . data . s ; for ( const optionsTrade of binanceTradeResponse . data . t ) { const timestamp = new Date ( optionsTrade . T ) ; const tradeIdNumeric = Number ( optionsTrade . t ) ; const lastProcessedTradeId = this . _lastTradeId . get ( symbol ) ; const isAlreadyProcessed = lastProcessedTradeId !== undefined && lastProcessedTradeId >= tradeIdNumeric ; const isStaleTrade = localTimestamp . valueOf ( ) - timestamp . valueOf ( ) > 10000 ; if ( isAlreadyProcessed || isStaleTrade ) { continue ; } this . _lastTradeId . set ( symbol , Number ( optionsTrade . t ) ) ; const trade = { type : 'trade' , symbol , exchange : 'binance-options' , id : optionsTrade . t , price : Number ( optionsTrade . p ) , amount : Number ( optionsTrade . q ) , side : optionsTrade . s === '-1' ? 'sell' : 'buy' , timestamp , localTimestamp : localTimestamp } ; yield trade ; } } } export class BinanceOptionsBookChangeMapper implements Mapper < 'binance-options' , BookChange > { canHandle ( message ) { if ( message . stream === undefined ) { return false ; } return message . stream . endsWith ( '@DEPTH100' ) ; } getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; return [ { channel : 'DEPTH100' , symbols } as const ] ; } * map ( message , localTimestamp ) { const bookChange = { type : 'book_change' , symbol : message . data . s , exchange : 'binance-options' , isSnapshot : true , bids : message . data . b . map ( this . mapBookLevel ) , asks : message . data . a . map ( this . mapBookLevel ) , timestamp : new Date ( message . data . E ) , localTimestamp } ; yield bookChange ; } protected mapBookLevel ( level ) { const price = Number ( level [ 0 ] ) ; const amount = Number ( level [ 1 ] ) ; return { price , amount } ; } } export class BinanceOptionSummaryMapper implements Mapper < 'binance-options' , OptionSummary > { private readonly _indexPrices = new Map < string , number > ( ) ; canHandle ( message ) { if ( message . stream === undefined ) { return false ; } return message . stream . endsWith ( '@TICKER' ) || message . stream . endsWith ( '@INDEX' ) ; } getFilters ( symbols ? ) { symbols = upperCaseSymbols ( symbols ) ; const indexes = symbols !== undefined ? symbols . map ( ( s ) => { const symbolParts = s . split ( '-' ) ; return `${ symbolParts [ 0 ] }USDT` ; } ) : undefined ; return [ { channel : 'TICKER' , symbols } as const , { channel : 'INDEX' , symbols : indexes } as const ] ; } * map ( message , localTimestamp ) { if ( message . stream . endsWith ( '@INDEX' ) ) { const lastIndexPrice = Number ( message . data . p ) ; if ( lastIndexPrice > 0 ) { this . _indexPrices . set ( message . data . s , lastIndexPrice ) ; } return ; } const optionInfo = message . data as BinanceOptionsTickerData ; const [ base , expiryPart , strikePrice , optionType ] = optionInfo . s . split ( '-' ) ; const expirationDate = new Date ( `20${ expiryPart . slice ( 0 , 2 ) }-${ expiryPart . slice ( 2 , 4 ) }-${ expiryPart . slice ( 4 , 6 ) }Z` ) ; expirationDate . setUTCHours ( 8 ) ; const isPut = optionType === 'P' ; const underlyingIndex = `${ base }USDT` ; let bestBidPrice = asNumberIfValid ( optionInfo . bo ) ; if ( bestBidPrice === 0 ) { bestBidPrice = undefined ; } let bestAskPrice = asNumberIfValid ( optionInfo . ao ) ; if ( bestAskPrice === 0 ) { bestAskPrice = undefined ; } let bestBidIV = bestBidPrice !== undefined ? asNumberIfValid ( optionInfo . b ) : undefined ; if ( bestBidIV === - 1 ) { bestBidIV = undefined ; } let bestAskIV = bestAskPrice !== undefined ? asNumberIfValid ( optionInfo . a ) : undefined ; if ( bestAskIV === - 1 ) { bestAskIV = undefined ; } const optionSummary = { type : 'option_summary' , symbol : optionInfo . s , exchange : 'binance-options' , optionType : isPut ? 'put' : 'call' , strikePrice : Number ( strikePrice ) , expirationDate , bestBidPrice , bestBidAmount : undefined , bestBidIV , bestAskPrice , bestAskAmount : undefined , bestAskIV , lastPrice : asNumberIfValid ( optionInfo . c ) , openInterest : undefined , markPrice : asNumberIfValid ( optionInfo . mp ) , markIV : undefined , delta : asNumberIfValid ( optionInfo . d ) , gamma : asNumberIfValid ( optionInfo . g ) , vega : asNumberIfValid ( optionInfo . v ) , theta : asNumberIfValid ( optionInfo . t ) , rho : undefined , underlyingPrice : this . _indexPrices . get ( underlyingIndex ) , underlyingIndex , timestamp : new Date ( optionInfo . E ) , localTimestamp : localTimestamp } ; yield optionSummary ; } } type BinanceResponse < T > = { stream : string ; data : T ; } ; type BinanceOptionsTradeData = { e : 'trade' ; E : number ; s : string ; t : { t : string ; p : string ; q : string ; T : number ; s : '-1' | '1' ; } [ ] ; } ; type BinanceOptionsDepthData = { e : 'depth' ; E : number ; s : string ; b : BinanceBookLevel [ ] ; a : BinanceBookLevel [ ] ; } ; type BinanceOptionsTickerData = { e : 'ticker' ; E : 1591677962357 ; s : 'BTC-200630-9000-P' ; o : '1000' ; h : '1000' ; l : '1000' ; c : '1000' ; V : '2' ; A : '0' ; p : '0' ; Q : '2000' ; F : 1 ; L : 1 ; n : 1 ; b : '0' ; a : '0' ; d : '0' ; t : '0' ; g : '0' ; v : '0' ; bo : '3.1' ; ao : '90.36' ; mp : '6.96' ; } ; type BinanceOptionsIndexData = { e : 'index' ; E : 1614556800182 ; s : 'BTCUSDT' ; p : '45160.127864' ; } ; type BinanceBookLevel = [ string , string ] ;