UNPKG

tardis-dev

Version:

Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js

604 lines (508 loc) 16.9 kB
import { asNonZeroNumberOrUndefined, lowerCaseSymbols, upperCaseSymbols } from '../handy.ts' import { BookChange, BookTicker, OptionSummary, Trade } from '../types.ts' import { Mapper } from './mapper.ts' // https://binance-docs.github.io/apidocs/voptions/en/#websocket-market-streams export class BinanceEuropeanOptionsTradesMapper implements Mapper<'binance-european-options', Trade> { canHandle(message: BinanceResponse<any>) { if (message.stream === undefined) { return false } return message.stream.endsWith('@trade') } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'trade', symbols } as const ] } *map(binanceTradeResponse: BinanceResponse<BinanceOptionsTradeData>, localTimestamp: Date) { const trade: Trade = { type: 'trade', symbol: binanceTradeResponse.data.s, exchange: 'binance-european-options', id: binanceTradeResponse.data.t, price: Number(binanceTradeResponse.data.p), amount: Number(binanceTradeResponse.data.q), side: binanceTradeResponse.data.S === '-1' ? 'sell' : 'buy', timestamp: new Date(binanceTradeResponse.data.T), localTimestamp: localTimestamp } yield trade } } export class BinanceEuropeanOptionsTradesMapperV2 implements Mapper<'binance-european-options', Trade> { canHandle(message: BinanceResponse<any>) { if (message.stream === undefined) { return false } return message.stream.endsWith('@optionTrade') } getFilters(symbols?: string[]) { symbols = lowerCaseSymbols(symbols) return [ { channel: 'optionTrade', symbols } as const ] } *map(binanceTradeResponse: BinanceResponse<BinanceOptionsTradeDataV2>, localTimestamp: Date) { const trade: Trade = { type: 'trade', symbol: binanceTradeResponse.data.s, exchange: 'binance-european-options', id: String(binanceTradeResponse.data.t), price: Number(binanceTradeResponse.data.p), amount: Number(binanceTradeResponse.data.q), side: binanceTradeResponse.data.m ? 'sell' : 'buy', timestamp: new Date(binanceTradeResponse.data.T), localTimestamp: localTimestamp } yield trade } } export class BinanceEuropeanOptionsBookChangeMapper implements Mapper<'binance-european-options', BookChange> { canHandle(message: BinanceResponse<any>) { if (message.stream === undefined) { return false } return message.stream.includes('@depth100') } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'depth100', symbols } as const ] } *map(message: BinanceResponse<BinanceOptionsDepthData>, localTimestamp: Date) { const bookChange: BookChange = { type: 'book_change', symbol: message.data.s, exchange: 'binance-european-options', isSnapshot: true, bids: message.data.b.map(this.mapBookLevel), asks: message.data.a.map(this.mapBookLevel), timestamp: message.data.E !== undefined ? new Date(message.data.E) : new Date(message.data.T), localTimestamp } yield bookChange } protected mapBookLevel(level: BinanceBookLevel) { const price = Number(level[0]) const amount = Number(level[1]) return { price, amount } } } export class BinanceEuropeanOptionsBookChangeMapperV2 implements Mapper<'binance-european-options', BookChange> { canHandle(message: BinanceResponse<any>) { if (message.stream === undefined) { return false } return message.stream.includes('@depth20') } getFilters(symbols?: string[]) { symbols = lowerCaseSymbols(symbols) return [ { channel: 'depth20', symbols } as const ] } *map(message: BinanceResponse<BinanceOptionsDepthDataV2>, localTimestamp: Date) { const bookChange: BookChange = { type: 'book_change', symbol: message.data.s, exchange: 'binance-european-options', isSnapshot: true, bids: message.data.b.map(this.mapBookLevel), asks: message.data.a.map(this.mapBookLevel), timestamp: new Date(message.data.T), localTimestamp } yield bookChange } protected mapBookLevel(level: BinanceBookLevel) { const price = Number(level[0]) const amount = Number(level[1]) return { price, amount } } } export class BinanceEuropeanOptionsBookTickerMapper implements Mapper<'binance-european-options', BookTicker> { canHandle(message: BinanceResponse<any>) { if (message.stream === undefined) { return false } return message.stream.endsWith('@bookTicker') } getFilters(symbols?: string[]) { symbols = lowerCaseSymbols(symbols) return [ { channel: 'bookTicker', symbols } as const ] } *map(message: BinanceResponse<BinanceOptionsBookTickerData>, localTimestamp: Date) { const bestBidPrice = Number(message.data.b) const bestBidAmount = Number(message.data.B) const bestAskPrice = Number(message.data.a) const bestAskAmount = Number(message.data.A) const bookTicker: BookTicker = { type: 'book_ticker', symbol: message.data.s, exchange: 'binance-european-options', bidPrice: bestBidPrice > 0 ? bestBidPrice : undefined, bidAmount: bestBidAmount > 0 ? bestBidAmount : undefined, askPrice: bestAskPrice > 0 ? bestAskPrice : undefined, askAmount: bestAskAmount > 0 ? bestAskAmount : undefined, timestamp: new Date(message.data.T), localTimestamp } yield bookTicker } } export class BinanceEuropeanOptionSummaryMapper implements Mapper<'binance-european-options', OptionSummary> { private readonly _indexPrices = new Map<string, number>() private readonly _openInterests = new Map<string, number>() canHandle(message: BinanceResponse<any>) { if (message.stream === undefined) { return false } return message.stream.endsWith('@ticker') || message.stream.endsWith('@index') || message.stream.includes('@openInterest') } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) const indexes = symbols !== undefined ? symbols.map((s) => { const symbolParts = s.split('-') return `${symbolParts[0]}USDT` }) : undefined const underlyings = symbols !== undefined ? symbols.map((s) => { const symbolParts = s.split('-') return `${symbolParts[0]}` }) : undefined return [ { channel: 'ticker', symbols } as const, { channel: 'index', symbols: indexes } as const, { channel: 'openInterest', symbols: underlyings } as const ] } *map( message: BinanceResponse<BinanceOptionsTickerData | BinanceOptionsIndexData | BinanceOptionsOpenInterestData[]>, localTimestamp: Date ) { if (message.stream.endsWith('@index')) { const lastIndexPrice = Number((message.data as any).p) if (lastIndexPrice > 0) { this._indexPrices.set((message.data as any).s, lastIndexPrice) } return } if (message.stream.includes('@openInterest')) { for (let data of message.data as BinanceOptionsOpenInterestData[]) { const openInterest = Number(data.o) if (openInterest >= 0) { this._openInterests.set(data.s, openInterest) } } 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` const bestBidPrice = asNonZeroNumberOrUndefined(optionInfo.bo) const bestBidIV = bestBidPrice !== undefined ? asNonZeroNumberOrUndefined(optionInfo.b) : undefined const bestAskPrice = asNonZeroNumberOrUndefined(optionInfo.ao) const bestAskIV = bestAskPrice !== undefined ? asNonZeroNumberOrUndefined(optionInfo.a) : undefined const optionSummary: OptionSummary = { type: 'option_summary', symbol: optionInfo.s, exchange: 'binance-european-options', optionType: isPut ? 'put' : 'call', strikePrice: Number(strikePrice), expirationDate, bestBidPrice, bestBidAmount: asNonZeroNumberOrUndefined(optionInfo.bq), bestBidIV: bestBidIV === -1 ? undefined : bestBidIV, bestAskPrice, bestAskAmount: asNonZeroNumberOrUndefined(optionInfo.aq), bestAskIV: bestAskIV === -1 ? undefined : bestAskIV, lastPrice: asNonZeroNumberOrUndefined(optionInfo.c), openInterest: this._openInterests.get(optionInfo.s), markPrice: asNonZeroNumberOrUndefined(optionInfo.mp), markIV: undefined, delta: asNonZeroNumberOrUndefined(optionInfo.d), gamma: asNonZeroNumberOrUndefined(optionInfo.g), vega: asNonZeroNumberOrUndefined(optionInfo.v), theta: asNonZeroNumberOrUndefined(optionInfo.t), rho: undefined, underlyingPrice: this._indexPrices.get(underlyingIndex), underlyingIndex, timestamp: new Date(optionInfo.E), localTimestamp: localTimestamp } yield optionSummary } } export class BinanceEuropeanOptionSummaryMapperV2 implements Mapper<'binance-european-options', OptionSummary> { private readonly _lastPrices = new Map<string, number>() private readonly _openInterests = new Map<string, number>() canHandle(message: BinanceResponse<any>) { if (message.stream === undefined) { return false } return ( message.stream.endsWith('@optionMarkPrice') || message.stream.endsWith('@optionTicker') || message.stream.includes('@optionOpenInterest') ) } getFilters(symbols?: string[]) { symbols = lowerCaseSymbols(symbols) const underlyings = symbols !== undefined ? symbols.map((s) => { const symbolParts = s.split('-') return `${symbolParts[0]}usdt` }) : undefined return [ { channel: 'optionMarkPrice', symbols: underlyings } as const, { channel: 'optionTicker', symbols } as const, { channel: 'optionOpenInterest', symbols: underlyings } as const ] } *map( message: BinanceResponse<BinanceOptionsMarkPriceData[] | BinanceOptionsTickerData | BinanceOptionsOpenInterestDataV2[]>, localTimestamp: Date ) { // Handle optionTicker messages to track last prices if (message.stream.endsWith('@optionTicker')) { const tickerData = message.data as BinanceOptionsTickerData const lastPrice = Number(tickerData.c) if (lastPrice > 0) { this._lastPrices.set(tickerData.s, lastPrice) } return } // Handle optionOpenInterest messages to track open interest if (message.stream.includes('@optionOpenInterest')) { const openInterestArray = message.data as BinanceOptionsOpenInterestDataV2[] for (let oi of openInterestArray) { const openInterest = Number(oi.o) if (openInterest >= 0) { this._openInterests.set(oi.s, openInterest) } } return } // optionMarkPrice contains all data needed: greeks, IV, best bid/ask, mark price, and index price const markPriceArray = message.data as BinanceOptionsMarkPriceData[] for (let markData of markPriceArray) { const [base, expiryPart, strikePrice, optionType] = markData.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` const bestBidPrice = asNonZeroNumberOrUndefined(markData.bo) const bestBidIV = bestBidPrice !== undefined ? asNonZeroNumberOrUndefined(markData.b) : undefined const bestAskPrice = asNonZeroNumberOrUndefined(markData.ao) const bestAskIV = bestAskPrice !== undefined ? asNonZeroNumberOrUndefined(markData.a) : undefined const markPrice = asNonZeroNumberOrUndefined(markData.mp) const markIV = asNonZeroNumberOrUndefined(markData.vo) const delta = asNonZeroNumberOrUndefined(markData.d) const gamma = asNonZeroNumberOrUndefined(markData.g) const vega = asNonZeroNumberOrUndefined(markData.v) const theta = asNonZeroNumberOrUndefined(markData.t) const underlyingPrice = asNonZeroNumberOrUndefined(markData.i) // Index price is included in mark price data const optionSummary: OptionSummary = { type: 'option_summary', symbol: markData.s, exchange: 'binance-european-options', optionType: isPut ? 'put' : 'call', strikePrice: Number(strikePrice), expirationDate, bestBidPrice, bestBidAmount: asNonZeroNumberOrUndefined(markData.bq), bestBidIV: bestBidIV === -1 ? undefined : bestBidIV, bestAskPrice, bestAskAmount: asNonZeroNumberOrUndefined(markData.aq), bestAskIV: bestAskIV === -1 ? undefined : bestAskIV, lastPrice: this._lastPrices.get(markData.s), openInterest: this._openInterests.get(markData.s), markPrice, markIV, delta, gamma, vega, theta, rho: undefined, underlyingPrice, underlyingIndex, timestamp: new Date(markData.E), localTimestamp: localTimestamp } yield optionSummary } } } type BinanceResponse<T> = { stream: string data: T } type BinanceOptionsTradeData = { e: 'trade' E: 1696118408137 s: 'DOGE-231006-0.06-C' t: '15' p: '2.64' q: '0.01' b: '4647850284614262784' a: '4719907951072796672' T: 1696118408134 S: '-1' } type BinanceOptionsDepthData = { e: 'depth' E: 1696118400038 T: 1696118399082 s: 'BTC-231027-34000-C' u: 1925729 pu: 1925729 b: [['60', '7.31'], ['55', '2.5'], ['50', '15'], ['45', '15'], ['40', '34.04']] a: [['65', '8.28'], ['70', '38.88'], ['75', '15'], ['1200', '0.01'], ['4660', '0.42']] } type BinanceOptionsTickerData = { e: '24hrTicker' E: 1696118400043 T: 1696118400000 s: 'BNB-231013-200-P' o: '1' h: '1' l: '0.9' c: '0.9' V: '11.08' A: '9.97' P: '-0.1' p: '-0.1' Q: '11' F: '0' L: '8' n: 1 bo: '1' ao: '1.7' bq: '50' aq: '50' b: '0.35929501' a: '0.43317497' d: '-0.16872899' t: '-0.16779034' g: '0.0153237' v: '0.09935076' vo: '0.41658748' mp: '1.5' hl: '37.1' ll: '0.1' eep: '0' } type BinanceOptionsIndexData = { e: 'index'; E: 1696118400040; s: 'BNBUSDT'; p: '214.6133998' } type BinanceOptionsOpenInterestData = { e: 'openInterest'; E: 1696118400042; s: 'XRP-231006-0.46-P'; o: '39480.0'; h: '20326.64319' } type BinanceBookLevel = [string, string] // V2 Types for new format (Dec 17, 2025+) type BinanceOptionsTradeDataV2 = { e: 'trade' E: number // event time T: number // trade completed time s: string // option symbol t: number // trade ID p: string // price q: string // quantity X: 'MARKET' | 'BLOCK' // trade type S: 'BUY' | 'SELL' // direction m: boolean // is buyer market maker } type BinanceOptionsDepthDataV2 = { e: 'depthUpdate' E: number // event time T: number // transaction time s: string // symbol U: number // first update ID u: number // final update ID pu: number // previous final update ID b: [string, string][] // bids a: [string, string][] // asks } type BinanceOptionsBookTickerData = { e: 'bookTicker' u: number // order book update ID s: string // symbol b: string // best bid price B: string // best bid qty a: string // best ask price A: string // best ask qty T: number // transaction time E: number // event time } type BinanceOptionsOpenInterestDataV2 = { e: 'openInterest' E: number // event time s: string // symbol o: string // open interest (quantity) h: string // open interest in notional value (USD) } type BinanceOptionsMarkPriceData = { s: string // option symbol mp: string // mark price E: number // event time e: 'markPrice' i: string // index price P: string // premium bo: string // best bid price ao: string // best ask price bq: string // best bid quantity aq: string // best ask quantity b: string // bid IV a: string // ask IV hl: string // high limit price ll: string // low limit price vo: string // mark IV rf: string // risk free rate d: string // delta t: string // theta g: string // gamma v: string // vega }