tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
300 lines • 12.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.KucoinFuturesDerivativeTickerMapper = exports.KucoinFuturesBookTickerMapper = exports.KucoinFuturesBookChangeMapper = exports.KucoinFuturesTradesMapper = void 0;
const debug_1 = require("../debug");
const handy_1 = require("../handy");
const mapper_1 = require("./mapper");
class KucoinFuturesTradesMapper {
canHandle(message) {
return message.type === 'message' && message.topic.startsWith('/contractMarket/execution');
}
getFilters(symbols) {
symbols = (0, handy_1.upperCaseSymbols)(symbols);
return [
{
channel: 'contractMarket/execution',
symbols
}
];
}
*map(message, localTimestamp) {
const kucoinTrade = message.data;
const timestamp = new Date(kucoinTrade.ts / 1000000);
yield {
type: 'trade',
symbol: kucoinTrade.symbol,
exchange: 'kucoin-futures',
id: kucoinTrade.tradeId,
price: Number(kucoinTrade.price),
amount: Number(kucoinTrade.size),
side: kucoinTrade.side === 'sell' ? 'sell' : 'buy',
timestamp,
localTimestamp
};
}
}
exports.KucoinFuturesTradesMapper = KucoinFuturesTradesMapper;
class KucoinFuturesBookChangeMapper {
ignoreBookSnapshotOverlapError;
symbolToDepthInfoMapping = {};
constructor(ignoreBookSnapshotOverlapError) {
this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError;
}
canHandle(message) {
return message.type === 'message' && message.topic.startsWith('/contractMarket/level2');
}
getFilters(symbols) {
symbols = (0, handy_1.upperCaseSymbols)(symbols);
return [
{
channel: 'contractMarket/level2',
symbols
},
{
channel: 'contractMarket/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 === 'level2Snapshot') {
// 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 (!message.data) {
return;
}
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 process 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) {
const mappedChange = this.mapChange(update.data.change);
if (mappedChange.price == 0) {
continue;
}
const matchingSide = mappedChange.isBid ? kucoinSnapshotData.bids : kucoinSnapshotData.asks;
const matchingLevel = matchingSide.find((b) => b[0] === mappedChange.price);
if (matchingLevel !== undefined) {
// remove empty level from snapshot
if (mappedChange.amount === 0) {
const index = matchingSide.findIndex((b) => b[0] === mappedChange.price);
if (index > -1) {
matchingSide.splice(index, 1);
}
}
else {
matchingLevel[1] = mappedChange.amount;
}
}
else if (mappedChange.amount != 0) {
matchingSide.push([mappedChange.price, mappedChange.amount]);
}
}
}
// remove all buffered updates
symbolDepthInfo.bufferedUpdates.clear();
const bookChange = {
type: 'book_change',
symbol,
exchange: 'kucoin-futures',
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 symbol = l2UpdateMessage.topic.split(':')[1];
const depthContext = this.symbolToDepthInfoMapping[symbol];
const lastUpdateId = depthContext.lastUpdateId;
// Drop any event where sequence is <= lastUpdateId in the snapshot
if (l2UpdateMessage.data.sequence <= lastUpdateId) {
return;
}
// The first processed event should have sequence>lastUpdateId
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.sequence === 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 kucoin-futures`;
if (this.ignoreBookSnapshotOverlapError) {
depthContext.validatedFirstUpdate = true;
(0, debug_1.debug)(message);
}
else {
throw new Error(message);
}
}
}
const change = this.mapChange(l2UpdateMessage.data.change);
return {
type: 'book_change',
symbol: symbol,
exchange: 'kucoin-futures',
isSnapshot: false,
bids: change.isBid
? [
{
price: change.price,
amount: change.amount
}
]
: [],
asks: change.isBid === false
? [
{
price: change.price,
amount: change.amount
}
]
: [],
timestamp: new Date(l2UpdateMessage.data.timestamp),
localTimestamp: localTimestamp
};
}
mapBookLevel(level) {
return { price: level[0], amount: level[1] };
}
mapChange(change) {
const parts = change.split(',');
const isBid = parts[1] === 'buy';
const price = Number(parts[0]);
const amount = Number(parts[2]);
return { isBid, price, amount };
}
}
exports.KucoinFuturesBookChangeMapper = KucoinFuturesBookChangeMapper;
class KucoinFuturesBookTickerMapper {
canHandle(message) {
return message.type === 'message' && message.topic.startsWith('/contractMarket/tickerV2');
}
getFilters(symbols) {
symbols = (0, handy_1.upperCaseSymbols)(symbols);
return [
{
channel: 'contractMarket/tickerV2',
symbols
}
];
}
*map(message, localTimestamp) {
const symbol = message.topic.split(':')[1];
const bookTicker = {
type: 'book_ticker',
symbol,
exchange: 'kucoin-futures',
askAmount: message.data.bestAskSize !== undefined && message.data.bestAskSize !== null ? message.data.bestAskSize : undefined,
askPrice: message.data.bestAskPrice !== undefined && message.data.bestAskPrice !== null ? Number(message.data.bestAskPrice) : undefined,
bidPrice: message.data.bestBidPrice !== undefined && message.data.bestBidPrice !== null ? Number(message.data.bestBidPrice) : undefined,
bidAmount: message.data.bestBidSize !== undefined && message.data.bestBidSize !== null ? message.data.bestBidSize : undefined,
timestamp: new Date(message.data.ts / 1000000),
localTimestamp: localTimestamp
};
yield bookTicker;
}
}
exports.KucoinFuturesBookTickerMapper = KucoinFuturesBookTickerMapper;
class KucoinFuturesDerivativeTickerMapper {
pendingTickerInfoHelper = new mapper_1.PendingTickerInfoHelper();
_lastPrices = new Map();
_openInterests = new Map();
canHandle(message) {
return (message.type === 'message' &&
(message.topic.startsWith('/contract/instrument') ||
message.topic.startsWith('/contractMarket/execution') ||
message.topic.startsWith('/contract/details')));
}
getFilters(symbols) {
symbols = (0, handy_1.upperCaseSymbols)(symbols);
return [
{
channel: 'contract/instrument',
symbols
},
{
channel: 'contractMarket/execution',
symbols
},
{
channel: 'contract/details',
symbols
}
];
}
*map(message, localTimestamp) {
const symbol = message.topic.split(':')[1];
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'kucoin-futures');
if (message.subject === 'match') {
this._lastPrices.set(symbol, Number(message.data.price));
return;
}
if (message.subject === 'contractDetails') {
const openInterestValue = (0, handy_1.asNumberIfValid)(message.data.openInterest);
if (openInterestValue === undefined) {
return;
}
this._openInterests.set(symbol, openInterestValue);
return;
}
const lastPrice = this._lastPrices.get(symbol);
const openInterest = this._openInterests.get(symbol);
if (message.subject === 'mark.index.price') {
pendingTickerInfo.updateIndexPrice(message.data.indexPrice);
pendingTickerInfo.updateMarkPrice(message.data.markPrice);
}
if (message.subject === 'funding.rate') {
pendingTickerInfo.updateTimestamp(new Date(message.data.timestamp));
pendingTickerInfo.updateFundingRate(message.data.fundingRate);
}
if (lastPrice !== undefined) {
pendingTickerInfo.updateLastPrice(lastPrice);
}
if (openInterest !== undefined) {
pendingTickerInfo.updateOpenInterest(openInterest);
}
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp);
}
}
}
exports.KucoinFuturesDerivativeTickerMapper = KucoinFuturesDerivativeTickerMapper;
//# sourceMappingURL=kucoinfutures.js.map