UNPKG

tardis-dev

Version:

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

231 lines 9.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KucoinBookTickerMapper = exports.KucoinBookChangeMapper = exports.KucoinTradesMapper = void 0; const debug_1 = require("../debug"); const handy_1 = require("../handy"); class KucoinTradesMapper { constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { return message.type === 'message' && message.topic.startsWith('/market/match'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'market/match', symbols } ]; } *map(message, localTimestamp) { const kucoinTrade = message.data; const timestamp = new Date(Number(kucoinTrade.time.slice(0, 13))); timestamp.μs = Number(kucoinTrade.time.slice(13, 16)); yield { type: 'trade', symbol: kucoinTrade.symbol, exchange: this._exchange, id: kucoinTrade.tradeId, price: Number(kucoinTrade.price), amount: Number(kucoinTrade.size), side: kucoinTrade.side === 'sell' ? 'sell' : 'buy', timestamp, localTimestamp }; } } exports.KucoinTradesMapper = KucoinTradesMapper; class KucoinBookChangeMapper { constructor(_exchange, ignoreBookSnapshotOverlapError) { this._exchange = _exchange; this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError; this.symbolToDepthInfoMapping = {}; } canHandle(message) { return message.type === 'message' && message.topic.startsWith('/market/level2'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'market/level2', symbols }, { channel: 'market/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 === 'trade.l2Snapshot') { // 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 (!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 proccess 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) { for (const bid of update.data.changes.bids) { if (bid[0] == '0') { continue; } const matchingBid = kucoinSnapshotData.bids.find((b) => b[0] === bid[0]); if (matchingBid !== undefined) { matchingBid[1] = bid[1]; } else { kucoinSnapshotData.bids.push([bid[0], bid[1]]); } } for (const ask of update.data.changes.asks) { if (ask[0] == '0') { continue; } const matchingAsk = kucoinSnapshotData.asks.find((a) => a[0] === ask[0]); if (matchingAsk !== undefined) { matchingAsk[1] = ask[1]; } else { kucoinSnapshotData.asks.push([ask[0], ask[1]]); } } } } // remove all buffered updates symbolDepthInfo.bufferedUpdates.clear(); const bookChange = { type: 'book_change', symbol, exchange: this._exchange, 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 depthContext = this.symbolToDepthInfoMapping[l2UpdateMessage.data.symbol]; const lastUpdateId = depthContext.lastUpdateId; // Drop any event where sequenceEnd is <= lastUpdateId in the snapshot if (l2UpdateMessage.data.sequenceEnd <= lastUpdateId) { return; } // The first processed event should have sequenceStart <= lastUpdateId+1 AND sequenceEnd >= lastUpdateId+1. 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.sequenceStart <= lastUpdateId + 1 && l2UpdateMessage.data.sequenceEnd >= 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 ${this._exchange}`; if (this.ignoreBookSnapshotOverlapError) { depthContext.validatedFirstUpdate = true; (0, debug_1.debug)(message); } else { throw new Error(message); } } } const bids = l2UpdateMessage.data.changes.bids.map(this.mapBookLevel).filter(this.nonZeroLevels); const asks = l2UpdateMessage.data.changes.asks.map(this.mapBookLevel).filter(this.nonZeroLevels); if (bids.length === 0 && asks.length === 0) { return; } const timestamp = l2UpdateMessage.data.time !== undefined ? new Date(l2UpdateMessage.data.time) : localTimestamp; return { type: 'book_change', symbol: l2UpdateMessage.data.symbol, exchange: this._exchange, isSnapshot: false, bids, asks, timestamp: timestamp, localTimestamp: localTimestamp }; } mapBookLevel(level) { const price = Number(level[0]); const amount = Number(level[1]); return { price, amount }; } nonZeroLevels(level) { return level.price > 0; } } exports.KucoinBookChangeMapper = KucoinBookChangeMapper; class KucoinBookTickerMapper { constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { return message.type === 'message' && message.topic.startsWith('/market/ticker'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'market/ticker', symbols } ]; } *map(message, localTimestamp) { const symbol = message.topic.split(':')[1]; const bookTicker = { type: 'book_ticker', symbol, exchange: this._exchange, askAmount: message.data.bestAskSize !== undefined && message.data.bestAskSize !== null ? Number(message.data.bestAskSize) : undefined, askPrice: message.data.bestAsk !== undefined && message.data.bestAsk !== null ? Number(message.data.bestAsk) : undefined, bidPrice: message.data.bestBid !== undefined && message.data.bestBid !== null ? Number(message.data.bestBid) : undefined, bidAmount: message.data.bestBidSize !== undefined && message.data.bestBidSize !== null ? Number(message.data.bestBidSize) : undefined, timestamp: new Date(message.data.time), localTimestamp: localTimestamp }; yield bookTicker; } } exports.KucoinBookTickerMapper = KucoinBookTickerMapper; //# sourceMappingURL=kucoin.js.map