UNPKG

tardis-dev

Version:

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

494 lines (434 loc) 15.4 kB
import { debug } from '../debug' import { asNumberIfValid, CircularBuffer, upperCaseSymbols } from '../handy' import { BookChange, BookTicker, DerivativeTicker, Trade } from '../types' import { Mapper, PendingTickerInfoHelper } from './mapper' export class KucoinFuturesTradesMapper implements Mapper<'kucoin-futures', Trade> { canHandle(message: KucoinFuturesTradeMessage) { return message.type === 'message' && message.topic.startsWith('/contractMarket/execution') } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'contractMarket/execution', symbols } as const ] } *map(message: KucoinFuturesTradeMessage, localTimestamp: Date): IterableIterator<Trade> { const kucoinTrade = message.data const timestamp = new Date(kucoinTrade.ts / 1000000) yield { type: 'trade', symbol: kucoinTrade.symbol, exchange: 'kucoin-futures', id: kucoinTrade.tradeId, price: Number(kucoinTrade.price), amount: Number(kucoinTrade.size), side: kucoinTrade.side === 'sell' ? 'sell' : 'buy', timestamp, localTimestamp } } } export class KucoinFuturesBookChangeMapper implements Mapper<'kucoin-futures', BookChange> { protected readonly symbolToDepthInfoMapping: { [key: string]: LocalDepthInfo } = {} constructor(private readonly ignoreBookSnapshotOverlapError: boolean) {} canHandle(message: KucoinFuturesLevel2SnapshotMessage | KucoinFuturesLevel2UpdateMessage) { return message.type === 'message' && message.topic.startsWith('/contractMarket/level2') } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'contractMarket/level2', symbols } as const, { channel: 'contractMarket/level2Snapshot', symbols } as const ] } *map(message: KucoinFuturesLevel2SnapshotMessage | KucoinFuturesLevel2UpdateMessage, localTimestamp: Date) { const symbol = message.topic.split(':')[1] if (this.symbolToDepthInfoMapping[symbol] === undefined) { this.symbolToDepthInfoMapping[symbol] = { bufferedUpdates: new CircularBuffer<KucoinFuturesLevel2UpdateMessage>(2000) } } const symbolDepthInfo = this.symbolToDepthInfoMapping[symbol] const snapshotAlreadyProcessed = symbolDepthInfo.snapshotProcessed // first check if received message is snapshot and process it as such if it is if (message.subject === 'level2Snapshot') { // if we've already received 'manual' snapshot, ignore if there is another one if (snapshotAlreadyProcessed) { return } // produce snapshot book_change const kucoinSnapshotData = message.data if (!message.data) { return } if (!kucoinSnapshotData.asks) { kucoinSnapshotData.asks = [] } if (!kucoinSnapshotData.bids) { kucoinSnapshotData.bids = [] } // mark given symbol depth info that has snapshot processed symbolDepthInfo.lastUpdateId = Number(kucoinSnapshotData.sequence) symbolDepthInfo.snapshotProcessed = true // if there were any depth updates buffered, let's process those by adding to or updating the initial snapshot for (const update of symbolDepthInfo.bufferedUpdates.items()) { const bookChange = this.mapBookDepthUpdate(update, localTimestamp) if (bookChange !== undefined) { const mappedChange = this.mapChange(update.data.change) if (mappedChange.price == 0) { continue } const matchingSide = mappedChange.isBid ? kucoinSnapshotData.bids : kucoinSnapshotData.asks const matchingLevel = matchingSide.find((b) => b[0] === mappedChange.price) if (matchingLevel !== undefined) { // remove empty level from snapshot if (mappedChange.amount === 0) { const index = matchingSide.findIndex((b) => b[0] === mappedChange.price) if (index > -1) { matchingSide.splice(index, 1) } } else { matchingLevel[1] = mappedChange.amount } } else if (mappedChange.amount != 0) { matchingSide.push([mappedChange.price, mappedChange.amount]) } } } // remove all buffered updates symbolDepthInfo.bufferedUpdates.clear() const bookChange: BookChange = { type: 'book_change', symbol, exchange: 'kucoin-futures', isSnapshot: true, bids: kucoinSnapshotData.bids.map(this.mapBookLevel), asks: kucoinSnapshotData.asks.map(this.mapBookLevel), timestamp: localTimestamp, localTimestamp } yield bookChange } else if (snapshotAlreadyProcessed) { // snapshot was already processed let's map the message as normal book_change const bookChange = this.mapBookDepthUpdate(message, localTimestamp) if (bookChange !== undefined) { yield bookChange } } else { symbolDepthInfo.bufferedUpdates.append(message) } } protected mapBookDepthUpdate(l2UpdateMessage: KucoinFuturesLevel2UpdateMessage, localTimestamp: Date): BookChange | undefined { // we can safely assume here that depthContext and lastUpdateId aren't null here as this is method only works // when we've already processed the snapshot const symbol = l2UpdateMessage.topic.split(':')[1] const depthContext = this.symbolToDepthInfoMapping[symbol]! const lastUpdateId = depthContext.lastUpdateId! // Drop any event where sequence is <= lastUpdateId in the snapshot if (l2UpdateMessage.data.sequence <= lastUpdateId) { return } // The first processed event should have sequence>lastUpdateId if (!depthContext.validatedFirstUpdate) { // if there is new instrument added it can have empty book at first and that's normal const bookSnapshotIsEmpty = lastUpdateId == -1 || lastUpdateId == 0 if (l2UpdateMessage.data.sequence === lastUpdateId + 1 || bookSnapshotIsEmpty) { depthContext.validatedFirstUpdate = true } else { const message = `Book depth snapshot has no overlap with first update, update ${JSON.stringify( l2UpdateMessage )}, lastUpdateId: ${lastUpdateId}, exchange kucoin-futures` if (this.ignoreBookSnapshotOverlapError) { depthContext.validatedFirstUpdate = true debug(message) } else { throw new Error(message) } } } const change = this.mapChange(l2UpdateMessage.data.change) return { type: 'book_change', symbol: symbol, exchange: 'kucoin-futures', isSnapshot: false, bids: change.isBid ? [ { price: change.price, amount: change.amount } ] : [], asks: change.isBid === false ? [ { price: change.price, amount: change.amount } ] : [], timestamp: new Date(l2UpdateMessage.data.timestamp), localTimestamp: localTimestamp } } private mapBookLevel(level: [number, number]) { return { price: level[0], amount: level[1] } } private mapChange(change: string) { const parts = change.split(',') const isBid = parts[1] === 'buy' const price = Number(parts[0]) const amount = Number(parts[2]) return { isBid, price, amount } } } export class KucoinFuturesBookTickerMapper implements Mapper<'kucoin-futures', BookTicker> { canHandle(message: KucoinFuturesTickerMessage) { return message.type === 'message' && message.topic.startsWith('/contractMarket/tickerV2') } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'contractMarket/tickerV2', symbols } as const ] } *map(message: KucoinFuturesTickerMessage, localTimestamp: Date) { const symbol = message.topic.split(':')[1] const bookTicker: BookTicker = { type: 'book_ticker', symbol, exchange: 'kucoin-futures', askAmount: message.data.bestAskSize !== undefined && message.data.bestAskSize !== null ? message.data.bestAskSize : undefined, askPrice: message.data.bestAskPrice !== undefined && message.data.bestAskPrice !== null ? Number(message.data.bestAskPrice) : undefined, bidPrice: message.data.bestBidPrice !== undefined && message.data.bestBidPrice !== null ? Number(message.data.bestBidPrice) : undefined, bidAmount: message.data.bestBidSize !== undefined && message.data.bestBidSize !== null ? message.data.bestBidSize : undefined, timestamp: new Date(message.data.ts / 1000000), localTimestamp: localTimestamp } yield bookTicker } } export class KucoinFuturesDerivativeTickerMapper implements Mapper<'kucoin-futures', DerivativeTicker> { private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper() private readonly _lastPrices = new Map<string, number>() private readonly _openInterests = new Map<string, number>() canHandle(message: KucoinFuturesTickerMessage) { return ( message.type === 'message' && (message.topic.startsWith('/contract/instrument') || message.topic.startsWith('/contractMarket/execution') || message.topic.startsWith('/contract/details')) ) } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'contract/instrument', symbols } as const, { channel: 'contractMarket/execution', symbols } as const, { channel: 'contract/details', symbols } as const ] } *map(message: KucoinFuturesInstrumentMessage | KucoinFuturesTradeMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> { const symbol = message.topic.split(':')[1] const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'kucoin-futures') if (message.subject === 'match') { this._lastPrices.set(symbol, Number(message.data.price)) return } if (message.subject === 'contractDetails') { const openInterestValue = asNumberIfValid(message.data.openInterest) if (openInterestValue === undefined) { return } this._openInterests.set(symbol, openInterestValue) return } const lastPrice = this._lastPrices.get(symbol) const openInterest = this._openInterests.get(symbol) if (message.subject === 'mark.index.price') { pendingTickerInfo.updateIndexPrice(message.data.indexPrice) pendingTickerInfo.updateMarkPrice(message.data.markPrice) } if (message.subject === 'funding.rate') { pendingTickerInfo.updateTimestamp(new Date(message.data.timestamp)) pendingTickerInfo.updateFundingRate(message.data.fundingRate) } if (lastPrice !== undefined) { pendingTickerInfo.updateLastPrice(lastPrice) } if (openInterest !== undefined) { pendingTickerInfo.updateOpenInterest(openInterest) } if (pendingTickerInfo.hasChanged()) { yield pendingTickerInfo.getSnapshot(localTimestamp) } } } type KucoinFuturesTradeMessage = { topic: '/contractMarket/execution:COMPUSDTM' type: 'message' subject: 'match' sn: 1694749771273 data: { symbol: 'COMPUSDTM' sequence: 1694749771273 makerUserId: '64b1a612d570b900017b7281' side: 'buy' | 'sell' size: 102 price: '57.75' takerOrderId: '137974138051522560' takerUserId: '61945720862a310001d6581e' makerOrderId: '137974082376310784' tradeId: '1694749771273' ts: 1705708799996000000 } } type LocalDepthInfo = { bufferedUpdates: CircularBuffer<KucoinFuturesLevel2UpdateMessage> snapshotProcessed?: boolean lastUpdateId?: number validatedFirstUpdate?: boolean } type KucoinFuturesLevel2SnapshotMessage = { type: 'message' generated: true topic: '/contractMarket/level2Snapshot:C98USDTM' subject: 'level2Snapshot' code: '200000' data: { sequence: 1694868048360 symbol: 'C98USDTM' bids: [number, number][] asks: [number, number][] ts: 1705881597161000000 } } type KucoinFuturesLevel2UpdateMessage = { topic: '/contractMarket/level2:C98USDTM' type: 'message' subject: 'level2' sn: 1694868048361 data: { sequence: 1694868048361; change: '0.2353,buy,146'; timestamp: 1705881600096 } } type KucoinFuturesTickerMessage = { topic: '/contractMarket/tickerV2:BCHUSDTM' type: 'message' subject: 'tickerV2' sn: 1695158749093 data: { symbol: 'BCHUSDTM' sequence: 1695158749093 bestBidSize: 480 bestBidPrice: '236.76' bestAskPrice: '236.77' bestAskSize: 126 ts: 1705708800078000000 } } type KucoinFuturesInstrumentMessage = | { topic: '/contract/instrument:ENSUSDTM' type: 'message' subject: 'funding.rate' data: { granularity: 60000; fundingRate: 0.000053; timestamp: 1705708800000 } } | { topic: '/contract/instrument:XAIUSDTM' type: 'message' subject: 'mark.index.price' data: { markPrice: 0.80694; indexPrice: 0.80695; granularity: 1000; timestamp: 1705881600000 } } | { topic: '/contract/instrument:BAKEUSDTM' type: 'message' subject: 'funding.rate' data: { granularity: 28800000; fundingRate: 0.000105; timestamp: 1705982400000 } } | { topic: '/contract/details:XBTUSDTM' type: 'message' subject: 'contractDetails' generated: true data: { symbol: 'XBTUSDTM' rootSymbol: 'USDT' type: 'FFWCSX' firstOpenDate: 1585555200000 baseCurrency: 'XBT' quoteCurrency: 'USDT' settleCurrency: 'USDT' maxOrderQty: 1000000 maxPrice: 1000000.0 lotSize: 1 tickSize: 0.1 indexPriceTickSize: 0.01 multiplier: 0.001 initialMargin: 0.008 maintainMargin: 0.004 maxRiskLimit: 25000 minRiskLimit: 25000 riskStep: 12500 makerFeeRate: 2.0e-4 takerFeeRate: 6.0e-4 takerFixFee: 0.0 makerFixFee: 0.0 isDeleverage: true isQuanto: true isInverse: false markMethod: 'FairPrice' fairMethod: 'FundingRate' fundingBaseSymbol: '.XBTINT8H' fundingQuoteSymbol: '.USDTINT8H' fundingRateSymbol: '.XBTUSDTMFPI8H' indexSymbol: '.KXBTUSDT' settlementSymbol: '' status: 'Open' fundingFeeRate: 3.8e-5 predictedFundingFeeRate: 9.6e-5 fundingRateGranularity: 28800000 openInterest: '9295921' turnoverOf24h: 5.94135187191124e8 volumeOf24h: 15131.243 markPrice: 39995.94 indexPrice: 39999.2 lastTradePrice: 39996.6 nextFundingRateTime: 10561278 maxLeverage: 125 sourceExchanges: ['okex', 'binance', 'kucoin', 'bybit', 'bitget', 'bitmart', 'gateio'] premiumsSymbol1M: '.XBTUSDTMPI' premiumsSymbol8H: '.XBTUSDTMPI8H' fundingBaseSymbol1M: '.XBTINT' fundingQuoteSymbol1M: '.USDTINT' lowPrice: 38560.0 highPrice: 40253.0 priceChgPct: 0.0132 priceChg: 523.4 } }