UNPKG

@hackape/tardis-dev

Version:

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

357 lines 15 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.BinanceLiquidationsMapper = exports.BinanceFuturesDerivativeTickerMapper = exports.BinanceFuturesBookChangeMapper = exports.BinanceBookChangeMapper = exports.BinanceTradesMapper = void 0; const debug_1 = require("../debug"); const handy_1 = require("../handy"); const mapper_1 = require("./mapper"); // https://github.com/binance-exchange/binance-official-api-docs/blob/master/web-socket-streams.md class BinanceTradesMapper { constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { if (message.stream === undefined) { return false; } return message.stream.endsWith('@trade'); } getFilters(symbols) { symbols = lowerCaseSymbols(symbols); return [ { channel: 'trade', symbols } ]; } *map(binanceTradeResponse, localTimestamp) { const binanceTrade = binanceTradeResponse.data; const isOffBookTrade = binanceTrade.X === 'INSURANCE_FUND'; if (isOffBookTrade) { return; } const trade = { type: 'trade', symbol: binanceTrade.s, exchange: this._exchange, id: String(binanceTrade.t), price: Number(binanceTrade.p), amount: Number(binanceTrade.q), side: binanceTrade.m ? 'sell' : 'buy', timestamp: new Date(binanceTrade.T), localTimestamp: localTimestamp }; yield trade; } } exports.BinanceTradesMapper = BinanceTradesMapper; class BinanceBookChangeMapper { constructor(exchange, ignoreBookSnapshotOverlapError) { this.exchange = exchange; this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError; this.symbolToDepthInfoMapping = {}; } canHandle(message) { if (message.stream === undefined) { return false; } return message.stream.includes('@depth'); } getFilters(symbols) { symbols = lowerCaseSymbols(symbols); return [ { channel: 'depth', symbols }, { channel: 'depthSnapshot', symbols } ]; } *map(message, localTimestamp) { const symbol = message.stream.split('@')[0].toUpperCase(); if (this.symbolToDepthInfoMapping[symbol] === undefined) { this.symbolToDepthInfoMapping[symbol] = { bufferedUpdates: new handy_1.CircularBuffer(200) }; } 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.data.lastUpdateId !== undefined) { // if we've already received 'manual' snapshot, ignore if there is another one if (snapshotAlreadyProcessed) { return; } // produce snapshot book_change const binanceDepthSnapshotData = message.data; // mark given symbol depth info that has snapshot processed symbolDepthInfo.lastUpdateId = binanceDepthSnapshotData.lastUpdateId; 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 = binanceDepthSnapshotData.bids.find((b) => b[0] === bid[0]); if (matchingBid !== undefined) { matchingBid[1] = bid[1]; } else { binanceDepthSnapshotData.bids.push(bid); } } for (const ask of update.a) { const matchingAsk = binanceDepthSnapshotData.asks.find((a) => a[0] === ask[0]); if (matchingAsk !== undefined) { matchingAsk[1] = ask[1]; } else { binanceDepthSnapshotData.asks.push(ask); } } } } // remove all buffered updates symbolDepthInfo.bufferedUpdates.clear(); const bookChange = { type: 'book_change', symbol, exchange: this.exchange, isSnapshot: true, bids: binanceDepthSnapshotData.bids.map(this.mapBookLevel), asks: binanceDepthSnapshotData.asks.map(this.mapBookLevel), timestamp: binanceDepthSnapshotData.T !== undefined ? new Date(binanceDepthSnapshotData.T) : 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.data, localTimestamp); if (bookChange !== undefined) { yield bookChange; } } else { const binanceDepthUpdateData = message.data; symbolDepthInfo.bufferedUpdates.append(binanceDepthUpdateData); } } mapBookDepthUpdate(binanceDepthUpdateData, 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[binanceDepthUpdateData.s]; const lastUpdateId = depthContext.lastUpdateId; // Drop any event where u is <= lastUpdateId in the snapshot if (binanceDepthUpdateData.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 ((binanceDepthUpdateData.U <= lastUpdateId + 1 && binanceDepthUpdateData.u >= lastUpdateId + 1) || bookSnapshotIsEmpty) { depthContext.validatedFirstUpdate = true; } else { const message = `Book depth snaphot has no overlap with first update, update ${JSON.stringify(binanceDepthUpdateData)}, lastUpdateId: ${lastUpdateId}, exchange ${this.exchange}`; if (this.ignoreBookSnapshotOverlapError) { depthContext.validatedFirstUpdate = true; debug_1.debug(message); } else { throw new Error(message); } } } return { type: 'book_change', symbol: binanceDepthUpdateData.s, exchange: this.exchange, isSnapshot: false, bids: binanceDepthUpdateData.b.map(this.mapBookLevel), asks: binanceDepthUpdateData.a.map(this.mapBookLevel), timestamp: new Date(binanceDepthUpdateData.E), localTimestamp: localTimestamp }; } mapBookLevel(level) { const price = Number(level[0]); const amount = Number(level[1]); return { price, amount }; } } exports.BinanceBookChangeMapper = BinanceBookChangeMapper; class BinanceFuturesBookChangeMapper extends BinanceBookChangeMapper { constructor(exchange, ignoreBookSnapshotOverlapError) { super(exchange, ignoreBookSnapshotOverlapError); this.exchange = exchange; this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError; } mapBookDepthUpdate(binanceDepthUpdateData, 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[binanceDepthUpdateData.s]; const lastUpdateId = depthContext.lastUpdateId; // based on https://binanceapitest.github.io/Binance-Futures-API-doc/wss/#how-to-manage-a-local-order-book-correctly // Drop any event where u is < lastUpdateId in the snapshot if (binanceDepthUpdateData.u < lastUpdateId) { return; } // The first processed should have U <= lastUpdateId AND u >= lastUpdateId if (!depthContext.validatedFirstUpdate) { if (binanceDepthUpdateData.U <= lastUpdateId && binanceDepthUpdateData.u >= lastUpdateId) { depthContext.validatedFirstUpdate = true; } else { const message = `Book depth snaphot has no overlap with first update, update ${JSON.stringify(binanceDepthUpdateData)}, lastUpdateId: ${lastUpdateId}, exchange ${this.exchange}`; if (this.ignoreBookSnapshotOverlapError) { depthContext.validatedFirstUpdate = true; debug_1.debug(message); } else { throw new Error(message); } } } return { type: 'book_change', symbol: binanceDepthUpdateData.s, exchange: this.exchange, isSnapshot: false, bids: binanceDepthUpdateData.b.map(this.mapBookLevel), asks: binanceDepthUpdateData.a.map(this.mapBookLevel), timestamp: new Date(binanceDepthUpdateData.T), localTimestamp: localTimestamp }; } } exports.BinanceFuturesBookChangeMapper = BinanceFuturesBookChangeMapper; class BinanceFuturesDerivativeTickerMapper { constructor(exchange) { this.exchange = exchange; this.pendingTickerInfoHelper = new mapper_1.PendingTickerInfoHelper(); this._indexPrices = new Map(); } canHandle(message) { if (message.stream === undefined) { return false; } return (message.stream.includes('@markPrice') || message.stream.endsWith('@ticker') || message.stream.endsWith('@openInterest') || message.stream.includes('@indexPrice')); } getFilters(symbols) { symbols = lowerCaseSymbols(symbols); const filters = [ { channel: 'markPrice', symbols }, { channel: 'ticker', symbols }, { channel: 'openInterest', symbols } ]; if (this.exchange === 'binance-delivery') { // index channel requires index symbol filters.push({ channel: 'indexPrice', symbols: symbols !== undefined ? symbols.map((s) => s.split('_')[0]) : undefined }); } return filters; } *map(message, localTimestamp) { if (message.data.e === 'indexPriceUpdate') { this._indexPrices.set(message.data.i, Number(message.data.p)); } else { const symbol = 's' in message.data ? message.data.s : message.data.symbol; const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, this.exchange); const lastIndexPrice = this._indexPrices.get(symbol.split('_')[0]); if (lastIndexPrice !== undefined) { pendingTickerInfo.updateIndexPrice(lastIndexPrice); } if (message.data.e === 'markPriceUpdate') { if ('r' in message.data && message.data.r !== '') { // only perpetual futures have funding rate info in mark price // delivery futures sometimes send empty ('') r value pendingTickerInfo.updateFundingRate(Number(message.data.r)); pendingTickerInfo.updateFundingTimestamp(new Date(message.data.T)); } if (message.data.i !== undefined) { pendingTickerInfo.updateIndexPrice(Number(message.data.i)); } pendingTickerInfo.updateMarkPrice(Number(message.data.p)); pendingTickerInfo.updateTimestamp(new Date(message.data.E)); } if (message.data.e === '24hrTicker') { pendingTickerInfo.updateLastPrice(Number(message.data.c)); pendingTickerInfo.updateTimestamp(new Date(message.data.E)); } if ('openInterest' in message.data) { pendingTickerInfo.updateOpenInterest(Number(message.data.openInterest)); } if (pendingTickerInfo.hasChanged()) { yield pendingTickerInfo.getSnapshot(localTimestamp); } } } } exports.BinanceFuturesDerivativeTickerMapper = BinanceFuturesDerivativeTickerMapper; class BinanceLiquidationsMapper { constructor(_exchange) { this._exchange = _exchange; } canHandle(message) { if (message.stream === undefined) { return false; } return message.stream.endsWith('@forceOrder'); } getFilters(symbols) { symbols = lowerCaseSymbols(symbols); return [ { channel: 'forceOrder', symbols } ]; } *map(binanceTradeResponse, localTimestamp) { const binanceLiquidation = binanceTradeResponse.data.o; // not sure if order status can be different to 'FILLED' for liquodations in practice, but... if (binanceLiquidation.X !== 'FILLED') { return; } const liquidation = { type: 'liquidation', symbol: binanceLiquidation.s, exchange: this._exchange, id: undefined, price: Number(binanceLiquidation.ap), amount: Number(binanceLiquidation.z), side: binanceLiquidation.S === 'SELL' ? 'sell' : 'buy', timestamp: new Date(binanceLiquidation.T), localTimestamp: localTimestamp }; yield liquidation; } } exports.BinanceLiquidationsMapper = BinanceLiquidationsMapper; function lowerCaseSymbols(symbols) { if (symbols !== undefined) { return symbols.map((s) => s.toLowerCase()); } return; } //# sourceMappingURL=binance.js.map