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