UNPKG

tardis-dev

Version:

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

795 lines 31 kB
import { asNonZeroNumberOrUndefined, upperCaseSymbols } from "../handy.js"; import { PendingTickerInfoHelper } from "./mapper.js"; // V5 Okex API mappers // https://www.okex.com/docs-v5/en/#websocket-api-public-channel-trades-channel export class OkexV5TradesMapper { _exchange; _useTradesAll; constructor(_exchange, _useTradesAll) { this._exchange = _exchange; this._useTradesAll = _useTradesAll; } canHandle(message) { if (message.event !== undefined || message.arg === undefined) { return false; } return message.arg.channel === 'trades' || message.arg.channel === 'trades-all'; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); if (this._useTradesAll) { return [ { channel: `trades-all`, symbols } ]; } return [ { channel: `trades`, symbols } ]; } *map(okexTradesMessage, localTimestamp) { for (const okexTrade of okexTradesMessage.data) { yield { type: 'trade', symbol: okexTrade.instId, exchange: this._exchange, id: okexTrade.tradeId, price: Number(okexTrade.px), amount: Number(okexTrade.sz), side: okexTrade.side === 'buy' ? 'buy' : 'sell', timestamp: new Date(Number(okexTrade.ts)), localTimestamp: localTimestamp }; } } } const mapV5BookLevel = (level) => { const price = Number(level[0]); const amount = Number(level[1]); return { price, amount }; }; export class OkexV5BookChangeMapper { _exchange; _channelName; constructor(_exchange, usePublicBooksChannel) { this._exchange = _exchange; this._channelName = this._getBooksChannelName(usePublicBooksChannel); } canHandle(message) { if (message.event !== undefined || message.arg === undefined) { return false; } return message.arg.channel === this._channelName; } _hasCredentials = process.env.OKX_API_KEY !== undefined; _hasVip5Access = process.env.OKX_API_VIP_5 !== undefined; _hasColoAccess = process.env.OKX_API_COLO !== undefined; _getBooksChannelName(usePublicBooksChannel) { if (usePublicBooksChannel === false) { // historical data always uses books-l2-tbt return 'books-l2-tbt'; } if (this._hasCredentials && this._hasVip5Access) { return 'books-l2-tbt'; } if (this._hasColoAccess) { return 'books-l2-tbt'; } if (this._hasCredentials) { return 'books50-l2-tbt'; } return 'books'; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); return [ { channel: this._channelName, symbols } ]; } *map(okexDepthDataMessage, localTimestamp) { for (const message of okexDepthDataMessage.data) { if (okexDepthDataMessage.action === 'update' && message.bids.length === 0 && message.asks.length === 0) { continue; } const timestamp = new Date(Number(message.ts)); if (timestamp.valueOf() === 0) { continue; } yield { type: 'book_change', symbol: okexDepthDataMessage.arg.instId, exchange: this._exchange, isSnapshot: okexDepthDataMessage.action === 'snapshot', bids: message.bids.map(mapV5BookLevel), asks: message.asks.map(mapV5BookLevel), timestamp, localTimestamp: localTimestamp }; } } } export class OkexV5BookTickerMapper { _exchange; _useTbtTickerChannel; constructor(_exchange, _useTbtTickerChannel) { this._exchange = _exchange; this._useTbtTickerChannel = _useTbtTickerChannel; } canHandle(message) { if (message.event !== undefined || message.arg === undefined) { return false; } if (this._useTbtTickerChannel) { return message.arg.channel === 'bbo-tbt'; } return message.arg.channel === 'tickers'; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); if (this._useTbtTickerChannel) { return [ { channel: `bbo-tbt`, symbols } ]; } return [ { channel: `tickers`, symbols } ]; } map(message, localTimestamp) { if (message.arg.channel === 'bbo-tbt') { return this._mapFromTbtTicker(message, localTimestamp); } else { return this._mapFromTicker(message, localTimestamp); } } *_mapFromTbtTicker(message, localTimestamp) { if (!message.data) { return; } for (const tbtTicker of message.data) { const bestAsk = tbtTicker.asks !== undefined && tbtTicker.asks[0] ? mapBookLevel(tbtTicker.asks[0]) : undefined; const bestBid = tbtTicker.bids !== undefined && tbtTicker.bids[0] ? mapBookLevel(tbtTicker.bids[0]) : undefined; const ticker = { type: 'book_ticker', symbol: message.arg.instId, exchange: this._exchange, askAmount: bestAsk?.amount, askPrice: bestAsk?.price, bidPrice: bestBid?.price, bidAmount: bestBid?.amount, timestamp: new Date(Number(tbtTicker.ts)), localTimestamp: localTimestamp }; yield ticker; } } *_mapFromTicker(message, localTimestamp) { for (const okexTicker of message.data) { const ticker = { type: 'book_ticker', symbol: okexTicker.instId, exchange: this._exchange, askAmount: asNonZeroNumberOrUndefined(okexTicker.askSz), askPrice: asNonZeroNumberOrUndefined(okexTicker.askPx), bidPrice: asNonZeroNumberOrUndefined(okexTicker.bidPx), bidAmount: asNonZeroNumberOrUndefined(okexTicker.bidSz), timestamp: new Date(Number(okexTicker.ts)), localTimestamp: localTimestamp }; yield ticker; } } } export class OkexV5DerivativeTickerMapper { _exchange; pendingTickerInfoHelper = new PendingTickerInfoHelper(); _indexPrices = new Map(); _futuresChannels = ['tickers', 'open-interest', 'mark-price', 'index-tickers']; _swapChannels = ['tickers', 'open-interest', 'mark-price', 'index-tickers', 'funding-rate']; constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels; if (message.event !== undefined || message.arg === undefined) { return false; } return channels.includes(message.arg.channel); } getFilters(symbols) { symbols = upperCaseSymbols(symbols); const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels; return channels.map((channel) => { if (channel === 'index-tickers') { const indexes = symbols !== undefined ? symbols.map((s) => { const symbolParts = s.split('-'); const quotePart = symbolParts[1] === 'USDC' ? 'USD' : symbolParts[1]; return `${symbolParts[0]}-${quotePart}`; }) : undefined; return { channel, symbols: indexes }; } return { channel, symbols }; }); } *map(message, localTimestamp) { if (message.arg.channel === 'index-tickers') { for (const dataMessage of message.data) { const indexTickerMessage = dataMessage; const lastIndexPrice = Number(indexTickerMessage.idxPx); if (lastIndexPrice > 0) { this._indexPrices.set(indexTickerMessage.instId, lastIndexPrice); } } return; } for (const dataMessage of message.data) { const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(dataMessage.instId, this._exchange); const symbolParts = dataMessage.instId.split('-'); const quotePart = symbolParts[1] === 'USDC' ? 'USD' : symbolParts[1]; const indexSymbol = `${symbolParts[0]}-${quotePart}`; const indexPrice = this._indexPrices.get(indexSymbol); if (indexPrice !== undefined) { pendingTickerInfo.updateIndexPrice(indexPrice); } if (message.arg.channel === 'mark-price') { const markPriceMessage = dataMessage; const markPrice = Number(markPriceMessage.markPx); if (markPrice > 0) { pendingTickerInfo.updateMarkPrice(markPrice); pendingTickerInfo.updateTimestamp(new Date(Number(markPriceMessage.ts))); } } if (message.arg.channel === 'open-interest') { const openInterestMessage = dataMessage; const openInterest = Number(openInterestMessage.oi); if (openInterest > 0) { pendingTickerInfo.updateOpenInterest(openInterest); pendingTickerInfo.updateTimestamp(new Date(Number(openInterestMessage.ts))); } } if (message.arg.channel === 'funding-rate') { const fundingRateMessage = dataMessage; if (fundingRateMessage.fundingRate !== undefined) { pendingTickerInfo.updateFundingRate(Number(fundingRateMessage.fundingRate)); } if (fundingRateMessage.fundingTime !== undefined) { pendingTickerInfo.updateFundingTimestamp(new Date(Number(fundingRateMessage.fundingTime))); } if (fundingRateMessage.nextFundingRate !== undefined && fundingRateMessage.nextFundingRate !== '') { pendingTickerInfo.updatePredictedFundingRate(Number(fundingRateMessage.nextFundingRate)); } } if (message.arg.channel === 'tickers') { const tickerMessage = dataMessage; const lastPrice = Number(tickerMessage.last); if (lastPrice > 0) { pendingTickerInfo.updateLastPrice(lastPrice); pendingTickerInfo.updateTimestamp(new Date(Number(tickerMessage.ts))); } } if (pendingTickerInfo.hasChanged()) { yield pendingTickerInfo.getSnapshot(localTimestamp); } } } } export class OkexV5LiquidationsMapper { _exchange; _isFirstMessage = true; constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { if (message.event !== undefined || message.arg === undefined) { return false; } return message.arg.channel === 'liquidations' || message.arg.channel === 'liquidation-orders'; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); return [ { channel: 'liquidations', symbols }, { channel: 'liquidation-orders', symbols } ]; } *map(okexLiquidationMessage, localTimestamp) { if (okexLiquidationMessage.arg.channel === 'liquidation-orders') { if (this._isFirstMessage) { this._isFirstMessage = false; return; } for (const okexLiquidation of okexLiquidationMessage.data) { for (const detail of okexLiquidation.details) { const liquidation = { type: 'liquidation', symbol: okexLiquidation.instId, exchange: this._exchange, id: undefined, price: Number(detail.bkPx), amount: Number(detail.sz), side: detail.side === 'buy' ? 'buy' : 'sell', timestamp: new Date(Number(detail.ts)), localTimestamp: localTimestamp }; yield liquidation; } } } else { for (const okexLiquidation of okexLiquidationMessage.data) { const liquidation = { type: 'liquidation', symbol: okexLiquidationMessage.arg.instId, exchange: this._exchange, id: undefined, price: Number(okexLiquidation.bkPx), amount: Number(okexLiquidation.sz), side: okexLiquidation.side === 'buy' ? 'buy' : 'sell', timestamp: new Date(Number(okexLiquidation.ts)), localTimestamp: localTimestamp }; yield liquidation; } } } } export class OkexV5OptionSummaryMapper { _indexPrices = new Map(); _openInterests = new Map(); _markPrices = new Map(); _tickers = new Map(); expiration_regex = /(\d{2})(\d{2})(\d{2})/; canHandle(message) { if (message.event !== undefined || message.arg === undefined) { return false; } return (message.arg.channel === 'opt-summary' || message.arg.channel === 'index-tickers' || message.arg.channel === 'tickers' || message.arg.channel === 'open-interest' || message.arg.channel === 'mark-price'); } getFilters(symbols) { symbols = upperCaseSymbols(symbols); const indexes = symbols !== undefined ? symbols.map((s) => { const symbolParts = s.split('-'); return `${symbolParts[0]}-${symbolParts[1]}`; }) : undefined; return [ { channel: `opt-summary`, symbols: [] }, { channel: `index-tickers`, symbols: indexes }, { channel: `tickers`, symbols: symbols }, { channel: `open-interest`, symbols: symbols }, { channel: `mark-price`, symbols: symbols } ]; } *map(message, localTimestamp) { if (message.arg.channel === 'index-tickers') { for (const dataMessage of message.data) { const indexTickerMessage = dataMessage; const lastIndexPrice = asNonZeroNumberOrUndefined(indexTickerMessage.idxPx); if (lastIndexPrice !== undefined) { this._indexPrices.set(indexTickerMessage.instId, lastIndexPrice); } } return; } if (message.arg.channel === 'open-interest') { for (const dataMessage of message.data) { const openInterestMessage = dataMessage; const openInterestValue = asNonZeroNumberOrUndefined(openInterestMessage.oi); if (openInterestValue !== undefined) { this._openInterests.set(openInterestMessage.instId, openInterestValue); } } return; } if (message.arg.channel === 'mark-price') { for (const dataMessage of message.data) { const markPriceMessage = dataMessage; const markPrice = asNonZeroNumberOrUndefined(markPriceMessage.markPx); if (markPrice !== undefined) { this._markPrices.set(markPriceMessage.instId, markPrice); } } return; } if (message.arg.channel === 'tickers') { for (const dataMessage of message.data) { const tickerMessage = dataMessage; this._tickers.set(tickerMessage.instId, tickerMessage); } return; } if (message.arg.channel === 'opt-summary') { for (const dataMessage of message.data) { const summary = dataMessage; const symbolParts = summary.instId.split('-'); const isPut = symbolParts[4] === 'P'; const strikePrice = Number(symbolParts[3]); var dateArray = this.expiration_regex.exec(symbolParts[2]); const expirationDate = new Date(Date.UTC(+('20' + dateArray[1]), +dateArray[2] - 1, +dateArray[3], 8, 0, 0, 0)); const lastUnderlyingPrice = this._indexPrices.get(summary.uly); const lastOpenInterest = this._openInterests.get(summary.instId); const lastMarkPrice = this._markPrices.get(summary.instId); const lastTickerInfo = this._tickers.get(summary.instId); const optionSummary = { type: 'option_summary', symbol: summary.instId, exchange: 'okex-options', optionType: isPut ? 'put' : 'call', strikePrice, expirationDate, bestBidPrice: asNonZeroNumberOrUndefined(lastTickerInfo?.bidPx), bestBidAmount: asNonZeroNumberOrUndefined(lastTickerInfo?.bidSz), bestBidIV: asNonZeroNumberOrUndefined(summary.bidVol), bestAskPrice: asNonZeroNumberOrUndefined(lastTickerInfo?.askPx), bestAskAmount: asNonZeroNumberOrUndefined(lastTickerInfo?.askSz), bestAskIV: asNonZeroNumberOrUndefined(summary.askVol), lastPrice: asNonZeroNumberOrUndefined(lastTickerInfo?.last), openInterest: lastOpenInterest, markPrice: lastMarkPrice, markIV: asNonZeroNumberOrUndefined(summary.markVol), delta: asNonZeroNumberOrUndefined(summary.delta), gamma: asNonZeroNumberOrUndefined(summary.gamma), vega: asNonZeroNumberOrUndefined(summary.vega), theta: asNonZeroNumberOrUndefined(summary.theta), rho: undefined, underlyingPrice: lastUnderlyingPrice, underlyingIndex: summary.uly, timestamp: new Date(Number(summary.ts)), localTimestamp: localTimestamp }; yield optionSummary; } } } } //--- //V3 Okex API mappers // https://www.okex.com/docs/en/#ws_swap-README export class OkexTradesMapper { _exchange; _market; constructor(_exchange, _market) { this._exchange = _exchange; this._market = _market; } canHandle(message) { return message.table === `${this._market}/trade`; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); return [ { channel: `${this._market}/trade`, symbols } ]; } *map(okexTradesMessage, localTimestamp) { for (const okexTrade of okexTradesMessage.data) { const symbol = okexTrade.instrument_id; yield { type: 'trade', symbol, exchange: this._exchange, id: typeof okexTrade.trade_id === 'string' ? okexTrade.trade_id : undefined, price: Number(okexTrade.price), amount: okexTrade.qty !== undefined ? Number(okexTrade.qty) : Number(okexTrade.size), side: okexTrade.side, timestamp: new Date(okexTrade.timestamp), localTimestamp: localTimestamp }; } } } const mapBookLevel = (level) => { const price = Number(level[0]); const amount = Number(level[1]); return { price, amount }; }; export class OkexBookChangeMapper { _exchange; _market; _canUseTickByTickChannel; constructor(_exchange, _market, _canUseTickByTickChannel) { this._exchange = _exchange; this._market = _market; this._canUseTickByTickChannel = _canUseTickByTickChannel; } canHandle(message) { const channelSuffix = this._canUseTickByTickChannel ? 'depth_l2_tbt' : 'depth'; return message.table === `${this._market}/${channelSuffix}`; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); if (this._canUseTickByTickChannel) { return [ { channel: `${this._market}/depth_l2_tbt`, symbols } ]; } // subscribe to both book channels and in canHandle decide which one to use // as one can subscribe to date range period that overlaps both when only depth channel has been available // and when both were available (both depth and depth_l2_tbt) return [ { channel: `${this._market}/depth_l2_tbt`, symbols }, { channel: `${this._market}/depth`, symbols } ]; } *map(okexDepthDataMessage, localTimestamp) { for (const message of okexDepthDataMessage.data) { if (message.bids.length === 0 && message.asks.length === 0 && okexDepthDataMessage.action !== 'partial') { continue; } const timestamp = new Date(message.timestamp); if (timestamp.valueOf() === 0) { continue; } yield { type: 'book_change', symbol: message.instrument_id, exchange: this._exchange, isSnapshot: okexDepthDataMessage.action === 'partial', bids: message.bids.map(mapBookLevel), asks: message.asks.map(mapBookLevel), timestamp, localTimestamp: localTimestamp }; } } } export class OkexDerivativeTickerMapper { _exchange; pendingTickerInfoHelper = new PendingTickerInfoHelper(); _futuresChannels = ['futures/ticker', 'futures/mark_price']; _swapChannels = ['swap/ticker', 'swap/mark_price', 'swap/funding_rate']; constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels; return channels.includes(message.table); } getFilters(symbols) { symbols = upperCaseSymbols(symbols); const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels; return channels.map((channel) => { return { channel, symbols }; }); } *map(message, localTimestamp) { for (const okexMessage of message.data) { const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(okexMessage.instrument_id, this._exchange); if ('funding_rate' in okexMessage) { pendingTickerInfo.updateFundingRate(Number(okexMessage.funding_rate)); pendingTickerInfo.updateFundingTimestamp(new Date(okexMessage.funding_time)); if (okexMessage.estimated_rate !== undefined) { pendingTickerInfo.updatePredictedFundingRate(Number(okexMessage.estimated_rate)); } } if ('mark_price' in okexMessage) { pendingTickerInfo.updateMarkPrice(Number(okexMessage.mark_price)); } if ('open_interest' in okexMessage) { const openInterest = Number(okexMessage.open_interest); if (openInterest > 0) { pendingTickerInfo.updateOpenInterest(Number(okexMessage.open_interest)); } } if ('last' in okexMessage) { pendingTickerInfo.updateLastPrice(Number(okexMessage.last)); } if (okexMessage.timestamp !== undefined) { pendingTickerInfo.updateTimestamp(new Date(okexMessage.timestamp)); } if (pendingTickerInfo.hasChanged()) { yield pendingTickerInfo.getSnapshot(localTimestamp); } } } } export class OkexOptionSummaryMapper { _indexPrices = new Map(); expiration_regex = /(\d{2})(\d{2})(\d{2})/; canHandle(message) { return message.table === 'index/ticker' || message.table === 'option/summary'; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); const indexes = symbols !== undefined ? symbols.map((s) => { const symbolParts = s.split('-'); return `${symbolParts[0]}-${symbolParts[1]}`; }) : undefined; return [ { channel: `option/summary`, symbols }, { channel: `index/ticker`, symbols: indexes } ]; } *map(message, localTimestamp) { if (message.table === 'index/ticker') { for (const index of message.data) { const lastIndexPrice = Number(index.last); if (lastIndexPrice > 0) { this._indexPrices.set(index.instrument_id, lastIndexPrice); } } return; } for (const summary of message.data) { const symbolParts = summary.instrument_id.split('-'); const isPut = symbolParts[4] === 'P'; const strikePrice = Number(symbolParts[3]); var dateArray = this.expiration_regex.exec(symbolParts[2]); const expirationDate = new Date(Date.UTC(+('20' + dateArray[1]), +dateArray[2] - 1, +dateArray[3], 8, 0, 0, 0)); const lastUnderlyingPrice = this._indexPrices.get(summary.underlying); const optionSummary = { type: 'option_summary', symbol: summary.instrument_id, exchange: 'okex-options', optionType: isPut ? 'put' : 'call', strikePrice, expirationDate, bestBidPrice: asNonZeroNumberOrUndefined(summary.best_bid), bestBidAmount: asNonZeroNumberOrUndefined(summary.best_bid_size), bestBidIV: asNonZeroNumberOrUndefined(summary.bid_vol), bestAskPrice: asNonZeroNumberOrUndefined(summary.best_ask), bestAskAmount: asNonZeroNumberOrUndefined(summary.best_ask_size), bestAskIV: asNonZeroNumberOrUndefined(summary.ask_vol), lastPrice: asNonZeroNumberOrUndefined(summary.last), openInterest: asNonZeroNumberOrUndefined(summary.open_interest), markPrice: asNonZeroNumberOrUndefined(summary.mark_price), markIV: asNonZeroNumberOrUndefined(summary.mark_vol), delta: asNonZeroNumberOrUndefined(summary.delta), gamma: asNonZeroNumberOrUndefined(summary.gamma), vega: asNonZeroNumberOrUndefined(summary.vega), theta: asNonZeroNumberOrUndefined(summary.theta), rho: undefined, underlyingPrice: lastUnderlyingPrice, underlyingIndex: summary.underlying, timestamp: new Date(summary.timestamp), localTimestamp: localTimestamp }; yield optionSummary; } } } export class OkexLiquidationsMapper { _exchange; _market; constructor(_exchange, _market) { this._exchange = _exchange; this._market = _market; } canHandle(message) { return message.table === `${this._market}/liquidation`; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); return [ { channel: `${this._market}/liquidation`, symbols } ]; } *map(okexLiquidationDataMessage, localTimestamp) { for (const okexLiquidation of okexLiquidationDataMessage.data) { const liquidation = { type: 'liquidation', symbol: okexLiquidation.instrument_id, exchange: this._exchange, id: undefined, price: Number(okexLiquidation.price), amount: Number(okexLiquidation.size), side: okexLiquidation.type === '3' ? 'sell' : 'buy', timestamp: new Date(okexLiquidation.created_at), localTimestamp: localTimestamp }; yield liquidation; } } } export class OkexBookTickerMapper { _exchange; _market; constructor(_exchange, _market) { this._exchange = _exchange; this._market = _market; } canHandle(message) { return message.table === `${this._market}/ticker`; } getFilters(symbols) { symbols = upperCaseSymbols(symbols); return [ { channel: `${this._market}/ticker`, symbols } ]; } *map(message, localTimestamp) { for (const okexTicker of message.data) { const ticker = { type: 'book_ticker', symbol: okexTicker.instrument_id, exchange: this._exchange, askAmount: asNonZeroNumberOrUndefined(okexTicker.best_ask_size), askPrice: asNonZeroNumberOrUndefined(okexTicker.best_ask), bidPrice: asNonZeroNumberOrUndefined(okexTicker.best_bid), bidAmount: asNonZeroNumberOrUndefined(okexTicker.best_bid_size), timestamp: new Date(okexTicker.timestamp), localTimestamp: localTimestamp }; yield ticker; } } } //# sourceMappingURL=okex.js.map