UNPKG

tardis-dev

Version:

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

300 lines 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KucoinFuturesDerivativeTickerMapper = exports.KucoinFuturesBookTickerMapper = exports.KucoinFuturesBookChangeMapper = exports.KucoinFuturesTradesMapper = void 0; const debug_1 = require("../debug"); const handy_1 = require("../handy"); const mapper_1 = require("./mapper"); class KucoinFuturesTradesMapper { canHandle(message) { return message.type === 'message' && message.topic.startsWith('/contractMarket/execution'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'contractMarket/execution', symbols } ]; } *map(message, localTimestamp) { 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 }; } } exports.KucoinFuturesTradesMapper = KucoinFuturesTradesMapper; class KucoinFuturesBookChangeMapper { ignoreBookSnapshotOverlapError; symbolToDepthInfoMapping = {}; constructor(ignoreBookSnapshotOverlapError) { this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError; } canHandle(message) { return message.type === 'message' && message.topic.startsWith('/contractMarket/level2'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'contractMarket/level2', symbols }, { channel: 'contractMarket/level2Snapshot', symbols } ]; } *map(message, localTimestamp) { const symbol = message.topic.split(':')[1]; if (this.symbolToDepthInfoMapping[symbol] === undefined) { this.symbolToDepthInfoMapping[symbol] = { bufferedUpdates: new handy_1.CircularBuffer(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 = { 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); } } mapBookDepthUpdate(l2UpdateMessage, localTimestamp) { // 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; (0, debug_1.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 }; } mapBookLevel(level) { return { price: level[0], amount: level[1] }; } mapChange(change) { const parts = change.split(','); const isBid = parts[1] === 'buy'; const price = Number(parts[0]); const amount = Number(parts[2]); return { isBid, price, amount }; } } exports.KucoinFuturesBookChangeMapper = KucoinFuturesBookChangeMapper; class KucoinFuturesBookTickerMapper { canHandle(message) { return message.type === 'message' && message.topic.startsWith('/contractMarket/tickerV2'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'contractMarket/tickerV2', symbols } ]; } *map(message, localTimestamp) { const symbol = message.topic.split(':')[1]; const 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; } } exports.KucoinFuturesBookTickerMapper = KucoinFuturesBookTickerMapper; class KucoinFuturesDerivativeTickerMapper { pendingTickerInfoHelper = new mapper_1.PendingTickerInfoHelper(); _lastPrices = new Map(); _openInterests = new Map(); canHandle(message) { return (message.type === 'message' && (message.topic.startsWith('/contract/instrument') || message.topic.startsWith('/contractMarket/execution') || message.topic.startsWith('/contract/details'))); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'contract/instrument', symbols }, { channel: 'contractMarket/execution', symbols }, { channel: 'contract/details', symbols } ]; } *map(message, localTimestamp) { 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 = (0, handy_1.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); } } } exports.KucoinFuturesDerivativeTickerMapper = KucoinFuturesDerivativeTickerMapper; //# sourceMappingURL=kucoinfutures.js.map