UNPKG

tardis-dev

Version:

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

309 lines 12.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.GateIOBookChangeMapper = exports.GateIOTradesMapper = exports.GateIOV4TradesMapper = exports.GateIOV4BookTickerMapper = exports.GateIOV4BookChangeMapper = void 0; const debug_1 = require("../debug"); const handy_1 = require("../handy"); //v4 class GateIOV4BookChangeMapper { constructor(exchange, ignoreBookSnapshotOverlapError) { this.exchange = exchange; this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError; this.symbolToDepthInfoMapping = {}; } canHandle(message) { if (message.channel === undefined) { return false; } if (message.event !== 'update' && message.event !== 'snapshot') { return false; } return message.channel.endsWith('order_book_update'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'order_book_update', symbols } ]; } *map(message, localTimestamp) { const symbol = message.event === 'snapshot' ? message.symbol : message.result.s; 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.event === 'snapshot') { // if we've already received 'manual' snapshot, ignore if there is another one if (snapshotAlreadyProcessed) { return; } // produce snapshot book_change const snapshotData = message.result; // mark given symbol depth info that has snapshot processed symbolDepthInfo.lastUpdateId = snapshotData.id; 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.b) { const matchingBid = snapshotData.bids.find((b) => b[0] === bid[0]); if (matchingBid !== undefined) { matchingBid[1] = bid[1]; } else { snapshotData.bids.push(bid); } } for (const ask of update.a) { const matchingAsk = snapshotData.asks.find((a) => a[0] === ask[0]); if (matchingAsk !== undefined) { matchingAsk[1] = ask[1]; } else { snapshotData.asks.push(ask); } } } } // remove all buffered updates symbolDepthInfo.bufferedUpdates.clear(); const bookChange = { type: 'book_change', symbol, exchange: this.exchange, isSnapshot: true, bids: snapshotData.bids.map(this.mapBookLevel), asks: snapshotData.asks.map(this.mapBookLevel), timestamp: new Date(snapshotData.update), localTimestamp }; yield bookChange; } else if (snapshotAlreadyProcessed) { // snapshot was already processed let's map the message as normal book_change const bookChange = this.mapBookDepthUpdate(message.result, localTimestamp); if (bookChange !== undefined) { yield bookChange; } } else { const depthUpdate = message.result; symbolDepthInfo.bufferedUpdates.append(depthUpdate); } } mapBookDepthUpdate(depthUpdateData, 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[depthUpdateData.s]; const lastUpdateId = depthContext.lastUpdateId; // Drop any event where u is <= lastUpdateId in the snapshot if (depthUpdateData.u <= lastUpdateId) { return; } // The first processed event should have U <= lastUpdateId+1 AND u >= 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; if ((depthUpdateData.U <= lastUpdateId + 1 && depthUpdateData.u >= lastUpdateId + 1) || bookSnapshotIsEmpty) { depthContext.validatedFirstUpdate = true; } else { const message = `Book depth snapshot has no overlap with first update, update ${JSON.stringify(depthUpdateData)}, lastUpdateId: ${lastUpdateId}, exchange ${this.exchange}`; if (this.ignoreBookSnapshotOverlapError) { depthContext.validatedFirstUpdate = true; (0, debug_1.debug)(message); } else { throw new Error(message); } } } return { type: 'book_change', symbol: depthUpdateData.s, exchange: this.exchange, isSnapshot: false, bids: depthUpdateData.b.map(this.mapBookLevel), asks: depthUpdateData.a.map(this.mapBookLevel), timestamp: (0, handy_1.fromMicroSecondsToDate)(depthUpdateData.t), localTimestamp: localTimestamp }; } mapBookLevel(level) { const price = Number(level[0]); const amount = Number(level[1]); return { price, amount }; } } exports.GateIOV4BookChangeMapper = GateIOV4BookChangeMapper; class GateIOV4BookTickerMapper { constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { if (message.channel === undefined) { return false; } if (message.event !== 'update') { return false; } return message.channel.endsWith('book_ticker'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'book_ticker', symbols } ]; } *map(bookTickerResponse, localTimestamp) { const gateBookTicker = bookTickerResponse.result; const ticker = { type: 'book_ticker', symbol: gateBookTicker.s, exchange: this._exchange, askAmount: gateBookTicker.A !== undefined ? Number(gateBookTicker.A) : undefined, askPrice: gateBookTicker.a !== undefined ? Number(gateBookTicker.a) : undefined, bidPrice: gateBookTicker.b !== undefined ? Number(gateBookTicker.b) : undefined, bidAmount: gateBookTicker.B !== undefined ? Number(gateBookTicker.B) : undefined, timestamp: gateBookTicker.t !== undefined ? new Date(gateBookTicker.t) : localTimestamp, localTimestamp: localTimestamp }; yield ticker; } } exports.GateIOV4BookTickerMapper = GateIOV4BookTickerMapper; class GateIOV4TradesMapper { constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { if (message.channel === undefined) { return false; } if (message.event !== 'update') { return false; } return message.channel.endsWith('trades'); } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'trades', symbols } ]; } *map(tradesMessage, localTimestamp) { yield { type: 'trade', symbol: tradesMessage.result.currency_pair, exchange: this._exchange, id: tradesMessage.result.id.toString(), price: Number(tradesMessage.result.price), amount: Number(tradesMessage.result.amount), side: tradesMessage.result.side == 'sell' ? 'sell' : 'buy', timestamp: new Date(Number(tradesMessage.result.create_time_ms)), localTimestamp: localTimestamp }; } } exports.GateIOV4TradesMapper = GateIOV4TradesMapper; // v3 https://www.gate.io/docs/websocket/index.html class GateIOTradesMapper { constructor(_exchange) { this._exchange = _exchange; this._seenSymbols = new Set(); } canHandle(message) { return message.method === 'trades.update'; } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'trades', symbols } ]; } *map(tradesMessage, localTimestamp) { const symbol = tradesMessage.params[0]; if (!tradesMessage.params[1]) { return; } // gate io sends trades from newest to oldest for some reason for (const gateIOTrade of tradesMessage.params[1].reverse()) { // always ignore first returned trade as it's a 'stale' trade, which has already been published before disconnect if (this._seenSymbols.has(symbol) === false) { this._seenSymbols.add(symbol); break; } const timestamp = new Date(gateIOTrade.time * 1000); timestamp.μs = Math.floor(gateIOTrade.time * 1000000) % 1000; yield { type: 'trade', symbol, exchange: this._exchange, id: gateIOTrade.id.toString(), price: Number(gateIOTrade.price), amount: Number(gateIOTrade.amount), side: gateIOTrade.type == 'sell' ? 'sell' : 'buy', timestamp, localTimestamp: localTimestamp }; } } } exports.GateIOTradesMapper = GateIOTradesMapper; const mapBookLevel = (level) => { const price = Number(level[0]); const amount = Number(level[1]); return { price, amount }; }; class GateIOBookChangeMapper { constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { return message.method === 'depth.update'; } getFilters(symbols) { symbols = (0, handy_1.upperCaseSymbols)(symbols); return [ { channel: 'depth', symbols } ]; } *map(depthMessage, localTimestamp) { const symbol = depthMessage.params[2]; const isSnapshot = depthMessage.params[0]; const bids = Array.isArray(depthMessage.params[1].bids) ? depthMessage.params[1].bids : []; const asks = Array.isArray(depthMessage.params[1].asks) ? depthMessage.params[1].asks : []; const timestamp = depthMessage.params[1].current !== undefined ? new Date(depthMessage.params[1].current * 1000) : localTimestamp; yield { type: 'book_change', symbol, exchange: this._exchange, isSnapshot, bids: bids.map(mapBookLevel), asks: asks.map(mapBookLevel), timestamp: timestamp, localTimestamp: localTimestamp }; } } exports.GateIOBookChangeMapper = GateIOBookChangeMapper; //# sourceMappingURL=gateio.js.map