@hackape/tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
366 lines • 13.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HuobiLiquidationsMapper = exports.HuobiDerivativeTickerMapper = exports.HuobiMBPBookChangeMapper = exports.HuobiBookChangeMapper = exports.HuobiTradesMapper = void 0;
const mapper_1 = require("./mapper");
const handy_1 = require("../handy");
// https://huobiapi.github.io/docs/spot/v1/en/#websocket-market-data
// https://github.com/huobiapi/API_Docs_en/wiki/WS_api_reference_en
class HuobiTradesMapper {
constructor(_exchange) {
this._exchange = _exchange;
this._seenSymbols = new Set();
}
canHandle(message) {
if (message.ch === undefined) {
return false;
}
return message.ch.endsWith('.trade.detail');
}
getFilters(symbols) {
symbols = normalizeSymbols(symbols);
return [
{
channel: 'trade',
symbols
}
];
}
*map(message, localTimestamp) {
const symbol = message.ch.split('.')[1].toUpperCase();
// 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);
return;
}
for (const huobiTrade of message.tick.data) {
yield {
type: 'trade',
symbol,
exchange: this._exchange,
id: String(huobiTrade.tradeId !== undefined ? huobiTrade.tradeId : huobiTrade.id),
price: huobiTrade.price,
amount: huobiTrade.amount,
side: huobiTrade.direction,
timestamp: new Date(huobiTrade.ts),
localTimestamp: localTimestamp
};
}
}
}
exports.HuobiTradesMapper = HuobiTradesMapper;
class HuobiBookChangeMapper {
constructor(_exchange) {
this._exchange = _exchange;
}
canHandle(message) {
if (message.ch === undefined) {
return false;
}
return message.ch.includes('.depth.');
}
getFilters(symbols) {
symbols = normalizeSymbols(symbols);
return [
{
channel: 'depth',
symbols
}
];
}
*map(message, localTimestamp) {
const symbol = message.ch.split('.')[1].toUpperCase();
const isSnapshot = 'event' in message.tick ? message.tick.event === 'snapshot' : 'update' in message ? false : true;
const data = message.tick;
const bids = Array.isArray(data.bids) ? data.bids : [];
const asks = Array.isArray(data.asks) ? data.asks : [];
if (bids.length === 0 && asks.length === 0) {
return;
}
yield {
type: 'book_change',
symbol,
exchange: this._exchange,
isSnapshot,
bids: bids.map(this._mapBookLevel),
asks: asks.map(this._mapBookLevel),
timestamp: new Date(message.ts),
localTimestamp: localTimestamp
};
}
_mapBookLevel(level) {
return { price: level[0], amount: level[1] };
}
}
exports.HuobiBookChangeMapper = HuobiBookChangeMapper;
function isSnapshot(message) {
return 'rep' in message;
}
class HuobiMBPBookChangeMapper {
constructor(_exchange) {
this._exchange = _exchange;
this.symbolToMBPInfoMapping = {};
}
canHandle(message) {
const channel = message.ch || message.rep;
if (channel === undefined) {
return false;
}
return channel.includes('.mbp.');
}
getFilters(symbols) {
symbols = normalizeSymbols(symbols);
return [
{
channel: 'mbp',
symbols
}
];
}
*map(message, localTimestamp) {
const symbol = (isSnapshot(message) ? message.rep : message.ch).split('.')[1].toUpperCase();
if (this.symbolToMBPInfoMapping[symbol] === undefined) {
this.symbolToMBPInfoMapping[symbol] = {
bufferedUpdates: new handy_1.CircularBuffer(200)
};
}
const mbpInfo = this.symbolToMBPInfoMapping[symbol];
const snapshotAlreadyProcessed = mbpInfo.snapshotProcessed;
if (isSnapshot(message)) {
if (snapshotAlreadyProcessed) {
return;
}
const snapshotBids = message.data.bids.map(this._mapBookLevel);
const snapshotAsks = message.data.asks.map(this._mapBookLevel);
// if there were any depth updates buffered, let's proccess those by adding to or updating the initial snapshot
// when prevSeqNum >= snapshot seqNum
for (const update of mbpInfo.bufferedUpdates.items()) {
if (update.tick.prevSeqNum < message.data.seqNum) {
continue;
}
const bookChange = this._mapMBPUpdate(update, symbol, localTimestamp);
if (bookChange !== undefined) {
for (const bid of bookChange.bids) {
const matchingBid = snapshotBids.find((b) => b.price === bid.price);
if (matchingBid !== undefined) {
matchingBid.amount = bid.amount;
}
else {
snapshotBids.push(bid);
}
}
for (const ask of bookChange.asks) {
const matchingAsk = snapshotAsks.find((a) => a.price === ask.price);
if (matchingAsk !== undefined) {
matchingAsk.amount = ask.amount;
}
else {
snapshotAsks.push(ask);
}
}
}
}
mbpInfo.snapshotProcessed = true;
mbpInfo.bufferedUpdates.clear();
yield {
type: 'book_change',
symbol,
exchange: this._exchange,
isSnapshot: true,
bids: snapshotBids,
asks: snapshotAsks,
timestamp: new Date(message.ts),
localTimestamp
};
}
else if (snapshotAlreadyProcessed) {
// snapshot was already processed let's map the mbp message as normal book_change
const update = this._mapMBPUpdate(message, symbol, localTimestamp);
if (update !== undefined) {
yield update;
}
}
else {
// there was no snapshot yet, let's buffer the update
mbpInfo.bufferedUpdates.append(message);
}
}
_mapMBPUpdate(message, symbol, localTimestamp) {
const bids = Array.isArray(message.tick.bids) ? message.tick.bids : [];
const asks = Array.isArray(message.tick.asks) ? message.tick.asks : [];
if (bids.length === 0 && asks.length === 0) {
return;
}
return {
type: 'book_change',
symbol,
exchange: this._exchange,
isSnapshot: false,
bids: bids.map(this._mapBookLevel),
asks: asks.map(this._mapBookLevel),
timestamp: new Date(message.ts),
localTimestamp: localTimestamp
};
}
_mapBookLevel(level) {
return { price: level[0], amount: level[1] };
}
}
exports.HuobiMBPBookChangeMapper = HuobiMBPBookChangeMapper;
function normalizeSymbols(symbols) {
if (symbols !== undefined) {
return symbols.map((s) => {
// huobi-dm and huobi-dm-swap expect symbols to be upper cased
if (s.includes('_') || s.includes('-')) {
return s;
}
// huobi global expects lower cased symbols
return s.toLowerCase();
});
}
return;
}
class HuobiDerivativeTickerMapper {
constructor(_exchange) {
this._exchange = _exchange;
this.pendingTickerInfoHelper = new mapper_1.PendingTickerInfoHelper();
}
canHandle(message) {
if (message.ch !== undefined) {
return message.ch.includes('.basis.') || message.ch.endsWith('.open_interest');
}
if (message.op === 'notify' && message.topic !== undefined) {
return message.topic.endsWith('.funding_rate');
}
return false;
}
getFilters(symbols) {
const filters = [
{
channel: 'basis',
symbols
},
{
channel: 'open_interest',
symbols
}
];
if (this._exchange === 'huobi-dm-swap') {
filters.push({
channel: 'funding_rate',
symbols
});
}
return filters;
}
*map(message, localTimestamp) {
if ('op' in message) {
// handle funding_rate notification message
const fundingInfo = message.data[0];
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(fundingInfo.contract_code, this._exchange);
pendingTickerInfo.updateFundingRate(Number(fundingInfo.funding_rate));
pendingTickerInfo.updateFundingTimestamp(new Date(Number(fundingInfo.settlement_time)));
pendingTickerInfo.updatePredictedFundingRate(Number(fundingInfo.estimated_rate));
pendingTickerInfo.updateTimestamp(new Date(message.ts));
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp);
}
}
else {
const symbol = message.ch.split('.')[1];
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, this._exchange);
// basis message
if ('tick' in message) {
pendingTickerInfo.updateIndexPrice(Number(message.tick.index_price));
pendingTickerInfo.updateLastPrice(Number(message.tick.contract_price));
}
else {
// open interest message
const openInterest = message.data[0];
pendingTickerInfo.updateOpenInterest(Number(openInterest.volume));
}
pendingTickerInfo.updateTimestamp(new Date(message.ts));
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp);
}
}
}
}
exports.HuobiDerivativeTickerMapper = HuobiDerivativeTickerMapper;
class HuobiLiquidationsMapper {
constructor(_exchange) {
this._exchange = _exchange;
this._contractCodeToSymbolMap = new Map();
this._contractTypesSuffixes = { this_week: 'CW', next_week: 'NW', quarter: 'CQ', next_quarter: 'NQ' };
}
canHandle(message) {
if (message.op !== 'notify') {
return false;
}
if (this._exchange === 'huobi-dm' && message.topic.endsWith('.contract_info')) {
this._updateContractCodeToSymbolMap(message);
}
return message.topic.endsWith('.liquidation_orders');
}
getFilters(symbols) {
if (this._exchange === 'huobi-dm') {
// huobi-dm for liquidations requires prividing different symbols which are indexes names for example 'BTC' or 'ETH'
// not futures names like 'BTC_NW'
// see https://huobiapi.github.io/docs/dm/v1/en/#subscribe-liquidation-order-data-no-authentication-sub
if (symbols !== undefined) {
symbols = symbols.map((s) => s.split('_')[0]);
}
// we also need to subscribe to contract_info which will provide us information that will allow us to map
// liquidation message symbol and contract code to symbols we expect (BTC_NW etc)
return [
{
channel: 'liquidation_orders',
symbols
},
{
channel: 'contract_info',
symbols
}
];
}
else {
// huobi dm swap liquidations messages provide correct symbol & contract code
return [
{
channel: 'liquidation_orders',
symbols
}
];
}
}
_updateContractCodeToSymbolMap(message) {
for (const item of message.data) {
this._contractCodeToSymbolMap.set(item.contract_code, `${item.symbol}_${this._contractTypesSuffixes[item.contract_type]}`);
}
}
*map(message, localTimestamp) {
for (const huobiLiquidation of message.data) {
let symbol = huobiLiquidation.contract_code;
// huobi-dm returns index name as a symbol, not future alias, so we need to map it here
if (this._exchange === 'huobi-dm') {
const futureAliasSymbol = this._contractCodeToSymbolMap.get(huobiLiquidation.contract_code);
if (futureAliasSymbol === undefined) {
continue;
}
symbol = futureAliasSymbol;
}
yield {
type: 'liquidation',
symbol,
exchange: this._exchange,
id: undefined,
price: huobiLiquidation.price,
amount: huobiLiquidation.volume,
side: huobiLiquidation.direction,
timestamp: new Date(huobiLiquidation.created_at),
localTimestamp: localTimestamp
};
}
}
}
exports.HuobiLiquidationsMapper = HuobiLiquidationsMapper;
//# sourceMappingURL=huobi.js.map