tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
497 lines • 19.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HuobiBookTickerMapper = exports.HuobiOptionsSummaryMapper = exports.HuobiLiquidationsMapper = exports.HuobiDerivativeTickerMapper = exports.HuobiMBPBookChangeMapper = exports.HuobiBookChangeMapper = exports.HuobiTradesMapper = void 0;
const handy_1 = require("../handy");
const mapper_1 = require("./mapper");
// 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;
}
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();
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 === 'buy' ? 'buy' : huobiTrade.direction === 'sell' ? 'sell' : 'unknown',
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(20)
};
}
const mbpInfo = this.symbolToMBPInfoMapping[symbol];
const snapshotAlreadyProcessed = mbpInfo.snapshotProcessed;
if (isSnapshot(message)) {
if (message.data == null) {
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;
yield {
type: 'book_change',
symbol,
exchange: this._exchange,
isSnapshot: true,
bids: snapshotBids,
asks: snapshotAsks,
timestamp: new Date(message.ts),
localTimestamp
};
}
else {
mbpInfo.bufferedUpdates.append(message);
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;
}
}
}
}
_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.toUpperCase();
}
// 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) {
symbols = (0, handy_1.upperCaseSymbols)(symbols);
const filters = [
{
channel: 'basis',
symbols
},
{
channel: 'open_interest',
symbols
}
];
if (this._exchange === 'huobi-dm-swap' || this._exchange === 'huobi-dm-linear-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) {
symbols = (0, handy_1.upperCaseSymbols)(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 === 'buy' ? 'buy' : huobiLiquidation.direction === 'sell' ? 'sell' : 'unknown',
timestamp: new Date(huobiLiquidation.created_at),
localTimestamp: localTimestamp
};
}
}
}
exports.HuobiLiquidationsMapper = HuobiLiquidationsMapper;
class HuobiOptionsSummaryMapper {
constructor() {
this._indexPrices = new Map();
this._openInterest = new Map();
}
canHandle(message) {
if (message.ch === undefined) {
return false;
}
return message.ch.endsWith('.open_interest') || message.ch.endsWith('.option_index') || message.ch.endsWith('.option_market_index');
}
getFilters(symbols) {
const indexes = symbols !== undefined
? symbols.map((s) => {
const symbolParts = s.split('-');
return `${symbolParts[0]}-${symbolParts[1]}`;
})
: undefined;
return [
{
channel: `open_interest`,
symbols
},
{
channel: `option_index`,
symbols: indexes
},
{
channel: 'option_market_index',
symbols
}
];
}
*map(message, localTimestamp) {
if (message.ch.endsWith('.option_index')) {
const indexUpdateMessage = message;
this._indexPrices.set(indexUpdateMessage.data.symbol, indexUpdateMessage.data.index_price);
return;
}
if (message.ch.endsWith('.open_interest')) {
const openInterestMessage = message;
for (const ioMessage of openInterestMessage.data) {
this._openInterest.set(ioMessage.contract_code, ioMessage.volume);
}
return;
}
const marketIndexMessage = message;
const symbolParts = marketIndexMessage.data.contract_code.split('-');
const expirationDate = new Date(`20${symbolParts[2].slice(0, 2)}-${symbolParts[2].slice(2, 4)}-${symbolParts[2].slice(4, 6)}Z`);
expirationDate.setUTCHours(8);
const underlying = `${symbolParts[0]}-${symbolParts[1]}`;
const lastUnderlyingPrice = this._indexPrices.get(underlying);
const openInterest = this._openInterest.get(marketIndexMessage.data.contract_code);
const optionSummary = {
type: 'option_summary',
symbol: marketIndexMessage.data.contract_code,
exchange: 'huobi-dm-options',
optionType: marketIndexMessage.data.option_right_type === 'P' ? 'put' : 'call',
strikePrice: Number(symbolParts[4]),
expirationDate,
bestBidPrice: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.bid_one),
bestBidAmount: undefined,
bestBidIV: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.iv_bid_one),
bestAskPrice: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.ask_one),
bestAskAmount: undefined,
bestAskIV: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.iv_ask_one),
lastPrice: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.last_price),
openInterest,
markPrice: marketIndexMessage.data.mark_price > 0 ? (0, handy_1.asNumberIfValid)(marketIndexMessage.data.mark_price) : undefined,
markIV: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.iv_mark_price),
delta: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.delta),
gamma: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.gamma),
vega: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.vega),
theta: (0, handy_1.asNumberIfValid)(marketIndexMessage.data.theta),
rho: undefined,
underlyingPrice: lastUnderlyingPrice,
underlyingIndex: underlying,
timestamp: new Date(marketIndexMessage.ts),
localTimestamp: localTimestamp
};
yield optionSummary;
}
}
exports.HuobiOptionsSummaryMapper = HuobiOptionsSummaryMapper;
class HuobiBookTickerMapper {
constructor(_exchange) {
this._exchange = _exchange;
}
canHandle(message) {
if (message.ch === undefined) {
return false;
}
return message.ch.endsWith('.bbo');
}
getFilters(symbols) {
symbols = normalizeSymbols(symbols);
return [
{
channel: 'bbo',
symbols
}
];
}
*map(message, localTimestamp) {
const symbol = message.ch.split('.')[1].toUpperCase();
if ('quoteTime' in message.tick) {
if (message.tick.quoteTime === 0) {
return;
}
yield {
type: 'book_ticker',
symbol,
exchange: this._exchange,
askAmount: (0, handy_1.asNumberIfValid)(message.tick.askSize),
askPrice: (0, handy_1.asNumberIfValid)(message.tick.ask),
bidPrice: (0, handy_1.asNumberIfValid)(message.tick.bid),
bidAmount: (0, handy_1.asNumberIfValid)(message.tick.bidSize),
timestamp: new Date(message.tick.quoteTime),
localTimestamp: localTimestamp
};
}
else {
yield {
type: 'book_ticker',
symbol,
exchange: this._exchange,
askAmount: message.tick.ask !== undefined && message.tick.ask !== null ? (0, handy_1.asNumberIfValid)(message.tick.ask[1]) : undefined,
askPrice: message.tick.ask !== undefined && message.tick.ask !== null ? (0, handy_1.asNumberIfValid)(message.tick.ask[0]) : undefined,
bidPrice: message.tick.bid !== undefined && message.tick.bid !== null ? (0, handy_1.asNumberIfValid)(message.tick.bid[0]) : undefined,
bidAmount: message.tick.bid !== undefined && message.tick.bid !== null ? (0, handy_1.asNumberIfValid)(message.tick.bid[1]) : undefined,
timestamp: new Date(message.tick.ts),
localTimestamp: localTimestamp
};
}
}
}
exports.HuobiBookTickerMapper = HuobiBookTickerMapper;
//# sourceMappingURL=huobi.js.map