tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
309 lines • 12.1 kB
JavaScript
;
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