tardis-dev
Advanced tools
Comparing version 13.20.5 to 13.21.0
@@ -37,3 +37,3 @@ export declare const EXCHANGES: readonly ["bitmex", "deribit", "binance-futures", "binance-delivery", "binance-options", "binance", "ftx", "okex-futures", "okex-options", "okex-swap", "okex", "huobi-dm", "huobi-dm-swap", "huobi-dm-linear-swap", "huobi", "bitfinex-derivatives", "bitfinex", "coinbase", "cryptofacilities", "kraken", "bitstamp", "gemini", "poloniex", "bybit", "bybit-spot", "bybit-options", "phemex", "delta", "ftx-us", "binance-us", "gate-io-futures", "gate-io", "okcoin", "bitflyer", "hitbtc", "coinflex", "binance-jersey", "binance-dex", "upbit", "ascendex", "dydx", "serum", "mango", "huobi-dm-options", "star-atlas", "crypto-com", "crypto-com-derivatives", "kucoin", "bitnomial", "woo-x", "blockchain-com"]; | ||
delta: readonly ["l2_orderbook", "recent_trade", "recent_trade_snapshot", "mark_price", "spot_price", "funding_rate", "product_updates", "announcements", "all_trades", "v2/ticker", "l1_orderbook", "l2_updates", "spot_30mtwap_price"]; | ||
'gate-io': readonly ["trades", "depth", "ticker"]; | ||
'gate-io': readonly ["trades", "depth", "ticker", "book_ticker", "order_book_update"]; | ||
'gate-io-futures': readonly ["trades", "order_book", "tickers", "book_ticker"]; | ||
@@ -40,0 +40,0 @@ poloniex: readonly ["price_aggregated_book", "trades", "ticker", "book_lv2"]; |
@@ -362,3 +362,3 @@ "use strict"; | ||
]; | ||
const GATE_IO_CHANNELS = ['trades', 'depth', 'ticker']; | ||
const GATE_IO_CHANNELS = ['trades', 'depth', 'ticker', 'book_ticker', 'order_book_update']; | ||
const GATE_IO_FUTURES_CHANNELS = ['trades', 'order_book', 'tickers', 'book_ticker']; | ||
@@ -365,0 +365,0 @@ const POLONIEX_CHANNELS = ['price_aggregated_book', 'trades', 'ticker', 'book_lv2']; |
@@ -1,3 +0,42 @@ | ||
import { BookChange, Exchange, Trade } from '../types'; | ||
import { CircularBuffer } from '../handy'; | ||
import { BookChange, BookTicker, Exchange, Trade } from '../types'; | ||
import { Mapper } from './mapper'; | ||
export declare class GateIOV4BookChangeMapper implements Mapper<'gate-io', BookChange> { | ||
protected readonly exchange: Exchange; | ||
protected readonly symbolToDepthInfoMapping: { | ||
[key: string]: LocalDepthInfo; | ||
}; | ||
constructor(exchange: Exchange); | ||
canHandle(message: GateV4OrderBookUpdate | Gatev4OrderBookSnapshot): boolean; | ||
getFilters(symbols?: string[]): { | ||
readonly channel: "order_book_update"; | ||
readonly symbols: string[] | undefined; | ||
}[]; | ||
map(message: GateV4OrderBookUpdate | Gatev4OrderBookSnapshot, localTimestamp: Date): Generator<BookChange, void, unknown>; | ||
protected mapBookDepthUpdate(depthUpdateData: DepthData, localTimestamp: Date): BookChange | undefined; | ||
protected mapBookLevel(level: [string, string]): { | ||
price: number; | ||
amount: number; | ||
}; | ||
} | ||
export declare class GateIOV4BookTickerMapper implements Mapper<'gate-io', BookTicker> { | ||
private readonly _exchange; | ||
constructor(_exchange: Exchange); | ||
canHandle(message: GateV4BookTicker): boolean; | ||
getFilters(symbols?: string[]): { | ||
readonly channel: "book_ticker"; | ||
readonly symbols: string[] | undefined; | ||
}[]; | ||
map(bookTickerResponse: GateV4BookTicker, localTimestamp: Date): Generator<BookTicker, void, unknown>; | ||
} | ||
export declare class GateIOV4TradesMapper implements Mapper<'gate-io', Trade> { | ||
private readonly _exchange; | ||
constructor(_exchange: Exchange); | ||
canHandle(message: GateV4Trade): boolean; | ||
getFilters(symbols?: string[]): { | ||
readonly channel: "trades"; | ||
readonly symbols: string[] | undefined; | ||
}[]; | ||
map(tradesMessage: GateV4Trade, localTimestamp: Date): IterableIterator<Trade>; | ||
} | ||
export declare class GateIOTradesMapper implements Mapper<'gate-io', Trade> { | ||
@@ -47,3 +86,78 @@ private readonly _exchange; | ||
}; | ||
type GateV4Trade = { | ||
time: 1682689046; | ||
time_ms: 1682689046133; | ||
channel: 'spot.trades'; | ||
event: 'update'; | ||
result: { | ||
id: 5541729596; | ||
create_time: 1682689046; | ||
create_time_ms: '1682689046123.0'; | ||
side: 'sell'; | ||
currency_pair: 'SUSD_USDT'; | ||
amount: '8.5234'; | ||
price: '0.9782'; | ||
}; | ||
}; | ||
type GateV4BookTicker = { | ||
time: 1682689046; | ||
time_ms: 1682689046142; | ||
channel: 'spot.book_ticker'; | ||
event: 'update'; | ||
result: { | ||
t: 1682689046131; | ||
u: 517377894; | ||
s: 'ETC_ETH'; | ||
b: '0.010326'; | ||
B: '0.001'; | ||
a: '0.010366'; | ||
A: '10'; | ||
}; | ||
}; | ||
type Gatev4OrderBookSnapshot = { | ||
channel: 'spot.order_book_update'; | ||
event: 'snapshot'; | ||
generated: true; | ||
symbol: '1ART_USDT'; | ||
result: { | ||
id: 154857784; | ||
current: 1682689045318; | ||
update: 1682689045056; | ||
asks: [string, string][]; | ||
bids: [string, string][]; | ||
}; | ||
}; | ||
type GateV4OrderBookUpdate = { | ||
time: 1682689045; | ||
time_ms: 1682689045532; | ||
channel: 'spot.order_book_update'; | ||
event: 'update'; | ||
result: { | ||
lastUpdateId: undefined; | ||
t: 1682689045424; | ||
e: 'depthUpdate'; | ||
E: 1682689045; | ||
s: '1ART_USDT'; | ||
U: 154857785; | ||
u: 154857785; | ||
b: [string, string][]; | ||
a: [string, string][]; | ||
}; | ||
}; | ||
type LocalDepthInfo = { | ||
bufferedUpdates: CircularBuffer<DepthData>; | ||
snapshotProcessed?: boolean; | ||
lastUpdateId?: number; | ||
validatedFirstUpdate?: boolean; | ||
}; | ||
type DepthData = { | ||
lastUpdateId: undefined; | ||
t: number; | ||
s: string; | ||
U: number; | ||
u: number; | ||
b: [string, string][]; | ||
a: [string, string][]; | ||
}; | ||
export {}; | ||
//# sourceMappingURL=gateio.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.GateIOBookChangeMapper = exports.GateIOTradesMapper = void 0; | ||
exports.GateIOBookChangeMapper = exports.GateIOTradesMapper = exports.GateIOV4TradesMapper = exports.GateIOV4BookTickerMapper = exports.GateIOV4BookChangeMapper = void 0; | ||
const handy_1 = require("../handy"); | ||
// https://www.gate.io/docs/websocket/index.html | ||
//v4 | ||
class GateIOV4BookChangeMapper { | ||
constructor(exchange) { | ||
this.exchange = exchange; | ||
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}`; | ||
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: new Date(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 { | ||
@@ -7,0 +216,0 @@ constructor(_exchange) { |
@@ -9,3 +9,3 @@ import { BookChange, DerivativeTicker, Liquidation, OptionSummary, BookTicker, Trade } from '../types'; | ||
export declare const normalizeLiquidations: <T extends "bitmex" | "deribit" | "binance-futures" | "binance-delivery" | "ftx" | "okex-futures" | "okex-swap" | "huobi-dm" | "huobi-dm-swap" | "huobi-dm-linear-swap" | "bitfinex-derivatives" | "cryptofacilities" | "bybit">(exchange: T, localTimestamp: Date) => Mapper<T, Liquidation>; | ||
export declare const normalizeBookTickers: <T extends "bitmex" | "deribit" | "binance-futures" | "binance-delivery" | "binance" | "ftx" | "okex-futures" | "okex-options" | "okex-swap" | "okex" | "huobi-dm" | "huobi-dm-swap" | "huobi-dm-linear-swap" | "huobi" | "bitfinex-derivatives" | "bitfinex" | "coinbase" | "cryptofacilities" | "kraken" | "bybit" | "bybit-spot" | "delta" | "ftx-us" | "binance-us" | "gate-io-futures" | "okcoin" | "bitflyer" | "binance-dex" | "ascendex" | "serum" | "mango" | "star-atlas" | "crypto-com" | "crypto-com-derivatives" | "kucoin" | "woo-x">(exchange: T, localTimestamp: Date) => Mapper<T, BookTicker>; | ||
export declare const normalizeBookTickers: <T extends "bitmex" | "deribit" | "binance-futures" | "binance-delivery" | "binance" | "ftx" | "okex-futures" | "okex-options" | "okex-swap" | "okex" | "huobi-dm" | "huobi-dm-swap" | "huobi-dm-linear-swap" | "huobi" | "bitfinex-derivatives" | "bitfinex" | "coinbase" | "cryptofacilities" | "kraken" | "bybit" | "bybit-spot" | "delta" | "ftx-us" | "binance-us" | "gate-io-futures" | "gate-io" | "okcoin" | "bitflyer" | "binance-dex" | "ascendex" | "serum" | "mango" | "star-atlas" | "crypto-com" | "crypto-com-derivatives" | "kucoin" | "woo-x">(exchange: T, localTimestamp: Date) => Mapper<T, BookTicker>; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -91,2 +91,6 @@ "use strict"; | ||
}; | ||
const GATE_IO_V4_API_SWITCH_DATE = new Date('2023-04-29T00:00:00.000Z'); | ||
const shouldUseGateIOV4Mappers = (localTimestamp) => { | ||
return isRealTime(localTimestamp) || localTimestamp.valueOf() >= GATE_IO_V4_API_SWITCH_DATE.valueOf(); | ||
}; | ||
const tradesMappers = { | ||
@@ -125,3 +129,3 @@ bitmex: () => bitmex_1.bitmexTradesMapper, | ||
delta: (localTimestamp) => new delta_1.DeltaTradesMapper(localTimestamp.valueOf() >= new Date('2020-10-14').valueOf()), | ||
'gate-io': () => new gateio_1.GateIOTradesMapper('gate-io'), | ||
'gate-io': (localTimestamp) => shouldUseGateIOV4Mappers(localTimestamp) ? new gateio_1.GateIOV4TradesMapper('gate-io') : new gateio_1.GateIOTradesMapper('gate-io'), | ||
'gate-io-futures': () => new gateiofutures_1.GateIOFuturesTradesMapper('gate-io-futures'), | ||
@@ -192,3 +196,3 @@ poloniex: (localTimestamp) => shouldUsePoloniexV2Mappers(localTimestamp) ? new poloniex_1.PoloniexV2TradesMapper() : new poloniex_1.PoloniexTradesMapper(), | ||
delta: (localTimestamp) => new delta_1.DeltaBookChangeMapper(localTimestamp.valueOf() >= new Date('2023-04-01').valueOf()), | ||
'gate-io': () => new gateio_1.GateIOBookChangeMapper('gate-io'), | ||
'gate-io': (localTimestamp) => shouldUseGateIOV4Mappers(localTimestamp) ? new gateio_1.GateIOV4BookChangeMapper('gate-io') : new gateio_1.GateIOBookChangeMapper('gate-io'), | ||
'gate-io-futures': () => new gateiofutures_1.GateIOFuturesBookChangeMapper('gate-io-futures'), | ||
@@ -306,3 +310,4 @@ poloniex: (localTimestamp) => shouldUsePoloniexV2Mappers(localTimestamp) ? new poloniex_1.PoloniexV2BookChangeMapper() : new poloniex_1.PoloniexBookChangeMapper(), | ||
delta: () => new delta_1.DeltaBookTickerMapper(), | ||
bybit: () => new bybit_1.BybitV5BookTickerMapper('bybit') | ||
bybit: () => new bybit_1.BybitV5BookTickerMapper('bybit'), | ||
'gate-io': () => new gateio_1.GateIOV4BookTickerMapper('gate-io') | ||
}; | ||
@@ -309,0 +314,0 @@ const normalizeTrades = (exchange, localTimestamp) => { |
import { Filter } from '../types'; | ||
import { RealTimeFeedBase } from './realtimefeed'; | ||
export declare class GateIORealTimeFeed extends RealTimeFeedBase { | ||
protected readonly wssURL = "wss://ws.gate.io/v3/"; | ||
protected readonly wssURL = "wss://api.gateio.ws/ws/v4/"; | ||
protected httpURL: string; | ||
protected mapToSubscribeMessages(filters: Filter<string>[]): any[]; | ||
protected messageIsError(message: any): boolean; | ||
protected provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean): Promise<void>; | ||
} | ||
//# sourceMappingURL=gateio.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.GateIORealTimeFeed = void 0; | ||
const handy_1 = require("../handy"); | ||
const realtimefeed_1 = require("./realtimefeed"); | ||
@@ -8,6 +9,6 @@ class GateIORealTimeFeed extends realtimefeed_1.RealTimeFeedBase { | ||
super(...arguments); | ||
this.wssURL = 'wss://ws.gate.io/v3/'; | ||
this.wssURL = 'wss://api.gateio.ws/ws/v4/'; | ||
this.httpURL = 'https://api.gateio.ws/api/v4'; | ||
} | ||
mapToSubscribeMessages(filters) { | ||
const id = 1; | ||
const payload = filters.map((filter) => { | ||
@@ -17,20 +18,26 @@ if (!filter.symbols || filter.symbols.length === 0) { | ||
} | ||
if (filter.channel === 'depth') { | ||
return { | ||
id, | ||
method: `${filter.channel}.subscribe`, | ||
params: filter.symbols.map((s) => { | ||
return [s, 30, '0']; | ||
}) | ||
}; | ||
if (filter.channel === 'order_book_update') { | ||
return filter.symbols.map((symbol) => { | ||
return { | ||
time: new Date().valueOf(), | ||
channel: `spot.${filter.channel}`, | ||
event: 'subscribe', | ||
method: `${filter.channel}.subscribe`, | ||
payload: [symbol, '100ms'] | ||
}; | ||
}); | ||
} | ||
else { | ||
return { | ||
id, | ||
method: `${filter.channel}.subscribe`, | ||
params: filter.symbols | ||
}; | ||
return filter.symbols.map((symbol) => { | ||
return { | ||
time: new Date().valueOf(), | ||
channel: `spot.${filter.channel}`, | ||
event: 'subscribe', | ||
method: `${filter.channel}.subscribe`, | ||
payload: [symbol] | ||
}; | ||
}); | ||
} | ||
}); | ||
return payload; | ||
return payload.flatMap((f) => f); | ||
} | ||
@@ -43,4 +50,28 @@ messageIsError(message) { | ||
} | ||
async provideManualSnapshots(filters, shouldCancel) { | ||
const orderBookFilter = filters.find((f) => f.channel === 'order_book_update'); | ||
if (!orderBookFilter) { | ||
return; | ||
} | ||
this.debug('requesting manual snapshots for: %s', orderBookFilter.symbols); | ||
for (let symbol of orderBookFilter.symbols) { | ||
if (shouldCancel()) { | ||
return; | ||
} | ||
const depthSnapshotResponse = await handy_1.httpClient | ||
.get(`${this.httpURL}/spot/order_book?currency_pair=${symbol}&limit=100&with_id=true`) | ||
.json(); | ||
const snapshot = { | ||
result: depthSnapshotResponse, | ||
event: 'snapshot', | ||
channel: `spot.order_book_update`, | ||
symbol, | ||
generated: true | ||
}; | ||
this.manualSnapshotsBuffer.push(snapshot); | ||
} | ||
this.debug('requested manual snapshots successfully for: %s ', orderBookFilter.symbols); | ||
} | ||
} | ||
exports.GateIORealTimeFeed = GateIORealTimeFeed; | ||
//# sourceMappingURL=gateio.js.map |
{ | ||
"name": "tardis-dev", | ||
"version": "13.20.5", | ||
"version": "13.21.0", | ||
"engines": { | ||
@@ -5,0 +5,0 @@ "node": ">=12" |
@@ -394,3 +394,3 @@ export const EXCHANGES = [ | ||
const GATE_IO_CHANNELS = ['trades', 'depth', 'ticker'] as const | ||
const GATE_IO_CHANNELS = ['trades', 'depth', 'ticker', 'book_ticker', 'order_book_update'] as const | ||
const GATE_IO_FUTURES_CHANNELS = ['trades', 'order_book', 'tickers', 'book_ticker'] as const | ||
@@ -397,0 +397,0 @@ const POLONIEX_CHANNELS = ['price_aggregated_book', 'trades', 'ticker', 'book_lv2'] as const |
@@ -1,7 +0,246 @@ | ||
import { upperCaseSymbols } from '../handy' | ||
import { BookChange, Exchange, Trade } from '../types' | ||
import { CircularBuffer, upperCaseSymbols } from '../handy' | ||
import { BookChange, BookTicker, Exchange, Trade } from '../types' | ||
import { Mapper } from './mapper' | ||
// https://www.gate.io/docs/websocket/index.html | ||
//v4 | ||
export class GateIOV4BookChangeMapper implements Mapper<'gate-io', BookChange> { | ||
protected readonly symbolToDepthInfoMapping: { | ||
[key: string]: LocalDepthInfo | ||
} = {} | ||
constructor(protected readonly exchange: Exchange) {} | ||
canHandle(message: GateV4OrderBookUpdate | Gatev4OrderBookSnapshot) { | ||
if (message.channel === undefined) { | ||
return false | ||
} | ||
if (message.event !== 'update' && message.event !== 'snapshot') { | ||
return false | ||
} | ||
return message.channel.endsWith('order_book_update') | ||
} | ||
getFilters(symbols?: string[]) { | ||
symbols = upperCaseSymbols(symbols) | ||
return [ | ||
{ | ||
channel: 'order_book_update', | ||
symbols | ||
} as const | ||
] | ||
} | ||
*map(message: GateV4OrderBookUpdate | Gatev4OrderBookSnapshot, localTimestamp: Date) { | ||
const symbol = message.event === 'snapshot' ? message.symbol : message.result.s | ||
if (this.symbolToDepthInfoMapping[symbol] === undefined) { | ||
this.symbolToDepthInfoMapping[symbol] = { | ||
bufferedUpdates: new CircularBuffer<DepthData>(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: 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 as DepthData, localTimestamp) | ||
if (bookChange !== undefined) { | ||
yield bookChange | ||
} | ||
} else { | ||
const depthUpdate = message.result as DepthData | ||
symbolDepthInfo.bufferedUpdates.append(depthUpdate) | ||
} | ||
} | ||
protected mapBookDepthUpdate(depthUpdateData: DepthData, localTimestamp: Date): BookChange | undefined { | ||
// 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}` | ||
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: new Date(depthUpdateData.t), | ||
localTimestamp: localTimestamp | ||
} | ||
} | ||
protected mapBookLevel(level: [string, string]) { | ||
const price = Number(level[0]) | ||
const amount = Number(level[1]) | ||
return { price, amount } | ||
} | ||
} | ||
export class GateIOV4BookTickerMapper implements Mapper<'gate-io', BookTicker> { | ||
constructor(private readonly _exchange: Exchange) {} | ||
canHandle(message: GateV4BookTicker) { | ||
if (message.channel === undefined) { | ||
return false | ||
} | ||
if (message.event !== 'update') { | ||
return false | ||
} | ||
return message.channel.endsWith('book_ticker') | ||
} | ||
getFilters(symbols?: string[]) { | ||
symbols = upperCaseSymbols(symbols) | ||
return [ | ||
{ | ||
channel: 'book_ticker', | ||
symbols | ||
} as const | ||
] | ||
} | ||
*map(bookTickerResponse: GateV4BookTicker, localTimestamp: Date) { | ||
const gateBookTicker = bookTickerResponse.result | ||
const ticker: BookTicker = { | ||
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 | ||
} | ||
} | ||
export class GateIOV4TradesMapper implements Mapper<'gate-io', Trade> { | ||
constructor(private readonly _exchange: Exchange) {} | ||
canHandle(message: GateV4Trade) { | ||
if (message.channel === undefined) { | ||
return false | ||
} | ||
if (message.event !== 'update') { | ||
return false | ||
} | ||
return message.channel.endsWith('trades') | ||
} | ||
getFilters(symbols?: string[]) { | ||
symbols = upperCaseSymbols(symbols) | ||
return [ | ||
{ | ||
channel: 'trades', | ||
symbols | ||
} as const | ||
] | ||
} | ||
*map(tradesMessage: GateV4Trade, localTimestamp: Date): IterableIterator<Trade> { | ||
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 | ||
} | ||
} | ||
} | ||
// v3 https://www.gate.io/docs/websocket/index.html | ||
export class GateIOTradesMapper implements Mapper<'gate-io', Trade> { | ||
@@ -127,1 +366,74 @@ private readonly _seenSymbols = new Set<string>() | ||
} | ||
type GateV4Trade = { | ||
time: 1682689046 | ||
time_ms: 1682689046133 | ||
channel: 'spot.trades' | ||
event: 'update' | ||
result: { | ||
id: 5541729596 | ||
create_time: 1682689046 | ||
create_time_ms: '1682689046123.0' | ||
side: 'sell' | ||
currency_pair: 'SUSD_USDT' | ||
amount: '8.5234' | ||
price: '0.9782' | ||
} | ||
} | ||
type GateV4BookTicker = { | ||
time: 1682689046 | ||
time_ms: 1682689046142 | ||
channel: 'spot.book_ticker' | ||
event: 'update' | ||
result: { t: 1682689046131; u: 517377894; s: 'ETC_ETH'; b: '0.010326'; B: '0.001'; a: '0.010366'; A: '10' } | ||
} | ||
type Gatev4OrderBookSnapshot = { | ||
channel: 'spot.order_book_update' | ||
event: 'snapshot' | ||
generated: true | ||
symbol: '1ART_USDT' | ||
result: { | ||
id: 154857784 | ||
current: 1682689045318 | ||
update: 1682689045056 | ||
asks: [string, string][] | ||
bids: [string, string][] | ||
} | ||
} | ||
type GateV4OrderBookUpdate = { | ||
time: 1682689045 | ||
time_ms: 1682689045532 | ||
channel: 'spot.order_book_update' | ||
event: 'update' | ||
result: { | ||
lastUpdateId: undefined | ||
t: 1682689045424 | ||
e: 'depthUpdate' | ||
E: 1682689045 | ||
s: '1ART_USDT' | ||
U: 154857785 | ||
u: 154857785 | ||
b: [string, string][] | ||
a: [string, string][] | ||
} | ||
} | ||
type LocalDepthInfo = { | ||
bufferedUpdates: CircularBuffer<DepthData> | ||
snapshotProcessed?: boolean | ||
lastUpdateId?: number | ||
validatedFirstUpdate?: boolean | ||
} | ||
type DepthData = { | ||
lastUpdateId: undefined | ||
t: number | ||
s: string | ||
U: number | ||
u: number | ||
b: [string, string][] | ||
a: [string, string][] | ||
} |
@@ -66,4 +66,10 @@ import { ONE_SEC_IN_MS } from '../handy' | ||
import { FTXBookChangeMapper, FTXDerivativeTickerMapper, FTXLiquidationsMapper, FTXBookTickerMapper, FTXTradesMapper } from './ftx' | ||
import { GateIOBookChangeMapper, GateIOTradesMapper } from './gateio' | ||
import { | ||
GateIOBookChangeMapper, | ||
GateIOTradesMapper, | ||
GateIOV4BookChangeMapper, | ||
GateIOV4BookTickerMapper, | ||
GateIOV4TradesMapper | ||
} from './gateio' | ||
import { | ||
GateIOFuturesBookChangeMapper, | ||
@@ -162,2 +168,8 @@ GateIOFuturesBookTickerMapper, | ||
const GATE_IO_V4_API_SWITCH_DATE = new Date('2023-04-29T00:00:00.000Z') | ||
const shouldUseGateIOV4Mappers = (localTimestamp: Date) => { | ||
return isRealTime(localTimestamp) || localTimestamp.valueOf() >= GATE_IO_V4_API_SWITCH_DATE.valueOf() | ||
} | ||
const tradesMappers = { | ||
@@ -206,3 +218,4 @@ bitmex: () => bitmexTradesMapper, | ||
delta: (localTimestamp: Date) => new DeltaTradesMapper(localTimestamp.valueOf() >= new Date('2020-10-14').valueOf()), | ||
'gate-io': () => new GateIOTradesMapper('gate-io'), | ||
'gate-io': (localTimestamp: Date) => | ||
shouldUseGateIOV4Mappers(localTimestamp) ? new GateIOV4TradesMapper('gate-io') : new GateIOTradesMapper('gate-io'), | ||
'gate-io-futures': () => new GateIOFuturesTradesMapper('gate-io-futures'), | ||
@@ -290,3 +303,4 @@ poloniex: (localTimestamp: Date) => | ||
delta: (localTimestamp: Date) => new DeltaBookChangeMapper(localTimestamp.valueOf() >= new Date('2023-04-01').valueOf()), | ||
'gate-io': () => new GateIOBookChangeMapper('gate-io'), | ||
'gate-io': (localTimestamp: Date) => | ||
shouldUseGateIOV4Mappers(localTimestamp) ? new GateIOV4BookChangeMapper('gate-io') : new GateIOBookChangeMapper('gate-io'), | ||
'gate-io-futures': () => new GateIOFuturesBookChangeMapper('gate-io-futures'), | ||
@@ -428,3 +442,4 @@ poloniex: (localTimestamp: Date) => | ||
delta: () => new DeltaBookTickerMapper(), | ||
bybit: () => new BybitV5BookTickerMapper('bybit') | ||
bybit: () => new BybitV5BookTickerMapper('bybit'), | ||
'gate-io': () => new GateIOV4BookTickerMapper('gate-io') | ||
} | ||
@@ -431,0 +446,0 @@ |
@@ -0,1 +1,2 @@ | ||
import { httpClient } from '../handy' | ||
import { Filter } from '../types' | ||
@@ -5,6 +6,6 @@ import { RealTimeFeedBase } from './realtimefeed' | ||
export class GateIORealTimeFeed extends RealTimeFeedBase { | ||
protected readonly wssURL = 'wss://ws.gate.io/v3/' | ||
protected readonly wssURL = 'wss://api.gateio.ws/ws/v4/' | ||
protected httpURL = 'https://api.gateio.ws/api/v4' | ||
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] { | ||
const id = 1 | ||
const payload = filters.map((filter) => { | ||
@@ -15,20 +16,26 @@ if (!filter.symbols || filter.symbols.length === 0) { | ||
if (filter.channel === 'depth') { | ||
return { | ||
id, | ||
method: `${filter.channel}.subscribe`, | ||
params: filter.symbols.map((s) => { | ||
return [s, 30, '0'] | ||
}) | ||
} | ||
if (filter.channel === 'order_book_update') { | ||
return filter.symbols!.map((symbol) => { | ||
return { | ||
time: new Date().valueOf(), | ||
channel: `spot.${filter.channel}`, | ||
event: 'subscribe', | ||
method: `${filter.channel}.subscribe`, | ||
payload: [symbol, '100ms'] | ||
} | ||
}) | ||
} else { | ||
return { | ||
id, | ||
method: `${filter.channel}.subscribe`, | ||
params: filter.symbols | ||
} | ||
return filter.symbols!.map((symbol) => { | ||
return { | ||
time: new Date().valueOf(), | ||
channel: `spot.${filter.channel}`, | ||
event: 'subscribe', | ||
method: `${filter.channel}.subscribe`, | ||
payload: [symbol] | ||
} | ||
}) | ||
} | ||
}) | ||
return payload | ||
return payload.flatMap((f) => f) | ||
} | ||
@@ -43,2 +50,33 @@ | ||
} | ||
protected async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) { | ||
const orderBookFilter = filters.find((f) => f.channel === 'order_book_update') | ||
if (!orderBookFilter) { | ||
return | ||
} | ||
this.debug('requesting manual snapshots for: %s', orderBookFilter.symbols!) | ||
for (let symbol of orderBookFilter.symbols!) { | ||
if (shouldCancel()) { | ||
return | ||
} | ||
const depthSnapshotResponse = await httpClient | ||
.get(`${this.httpURL}/spot/order_book?currency_pair=${symbol}&limit=100&with_id=true`) | ||
.json() | ||
const snapshot = { | ||
result: depthSnapshotResponse, | ||
event: 'snapshot', | ||
channel: `spot.order_book_update`, | ||
symbol, | ||
generated: true | ||
} | ||
this.manualSnapshotsBuffer.push(snapshot) | ||
} | ||
this.debug('requested manual snapshots successfully for: %s ', orderBookFilter.symbols!) | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1774020
32782