tardis-machine 18.1.0 → 18.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
- package/dist/ws/subscriptionsmappers.js +3 -1
- package/dist/ws/subscriptionsmappers.js.map +1 -1
- package/node_modules/tardis-dev/dist/consts.d.ts +4 -2
- package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/consts.js +19 -0
- package/node_modules/tardis-dev/dist/consts.js.map +1 -1
- package/node_modules/tardis-dev/dist/handy.d.ts +13 -0
- package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/handy.js +5 -1
- package/node_modules/tardis-dev/dist/handy.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/aster.d.ts +16 -0
- package/node_modules/tardis-dev/dist/mappers/aster.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/mappers/aster.js +297 -0
- package/node_modules/tardis-dev/dist/mappers/aster.js.map +1 -0
- package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
- package/node_modules/tardis-dev/dist/mappers/binance.js +22 -22
- package/node_modules/tardis-dev/dist/mappers/binance.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
- package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
- package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
- package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
- package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +7 -7
- package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
- package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
- package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/deribit.js +16 -5
- package/node_modules/tardis-dev/dist/mappers/deribit.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
- package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/index.d.ts +180 -162
- package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
- package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +5 -5
- package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
- package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/registry.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/registry.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
- package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts +53 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.js +271 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.js.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts +1 -4
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +36 -95
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/index.js +3 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/kucoinfutures.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/kucoinfutures.js +6 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/kucoinfutures.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +15 -18
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts +14 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js +72 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js.map +1 -0
- package/node_modules/tardis-dev/package.json +4 -4
- package/node_modules/tardis-dev/src/consts.ts +20 -0
- package/node_modules/tardis-dev/src/handy.ts +6 -1
- package/node_modules/tardis-dev/src/mappers/aster.ts +441 -0
- package/node_modules/tardis-dev/src/mappers/binance.ts +23 -21
- package/node_modules/tardis-dev/src/mappers/bitfinex.ts +1 -2
- package/node_modules/tardis-dev/src/mappers/deribit.ts +18 -5
- package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
- package/node_modules/tardis-dev/src/mappers/registry.ts +1 -2
- package/node_modules/tardis-dev/src/realtimefeeds/aster.ts +388 -0
- package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +59 -145
- package/node_modules/tardis-dev/src/realtimefeeds/index.ts +3 -0
- package/node_modules/tardis-dev/src/realtimefeeds/kucoinfutures.ts +7 -1
- package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +15 -21
- package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
- package/node_modules/ws/lib/permessage-deflate.js +3 -1
- package/node_modules/ws/package.json +1 -1
- package/package.json +5 -5
- package/src/ws/subscriptionsmappers.ts +3 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { wait } from "../handy.js";
|
|
2
|
+
export function parseRequestWeightHeader(headerValue) {
|
|
3
|
+
if (headerValue === undefined) {
|
|
4
|
+
return undefined;
|
|
5
|
+
}
|
|
6
|
+
const parsed = Number.parseInt(headerValue, 10);
|
|
7
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
8
|
+
}
|
|
9
|
+
export function getExchangeScopedNumberEnv(exchange, suffix, fallback) {
|
|
10
|
+
const envName = `${exchange.toUpperCase().replace(/-/g, '_')}_${suffix}`;
|
|
11
|
+
const rawValue = process.env[envName];
|
|
12
|
+
if (rawValue === undefined) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
16
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
17
|
+
}
|
|
18
|
+
export function getRequestWeightLimit(exchange, exchangeInfo) {
|
|
19
|
+
const configuredLimit = getExchangeScopedNumberEnv(exchange, 'REQUEST_WEIGHT_LIMIT', 0);
|
|
20
|
+
if (configuredLimit > 0) {
|
|
21
|
+
return configuredLimit;
|
|
22
|
+
}
|
|
23
|
+
const requestWeightLimit = exchangeInfo.rateLimits.find((d) => d.rateLimitType === 'REQUEST_WEIGHT')?.limit;
|
|
24
|
+
if (!requestWeightLimit) {
|
|
25
|
+
throw new Error(`Failed to determine ${exchange} REQUEST_WEIGHT limit`);
|
|
26
|
+
}
|
|
27
|
+
return requestWeightLimit;
|
|
28
|
+
}
|
|
29
|
+
export class RequestWeightLimiter {
|
|
30
|
+
limit;
|
|
31
|
+
minAvailableWeightBuffer;
|
|
32
|
+
_usedWeight;
|
|
33
|
+
constructor(limit, minAvailableWeightBuffer, _usedWeight = 0) {
|
|
34
|
+
this.limit = limit;
|
|
35
|
+
this.minAvailableWeightBuffer = minAvailableWeightBuffer;
|
|
36
|
+
this._usedWeight = _usedWeight;
|
|
37
|
+
}
|
|
38
|
+
get usedWeight() {
|
|
39
|
+
return this._usedWeight;
|
|
40
|
+
}
|
|
41
|
+
get availableWeight() {
|
|
42
|
+
return this.limit > 0 ? this.limit - this._usedWeight - this.minAvailableWeightBuffer : Infinity;
|
|
43
|
+
}
|
|
44
|
+
async waitForAvailableWeight(requestWeight, onWait) {
|
|
45
|
+
const maxAvailableWeight = this.limit - this.minAvailableWeightBuffer;
|
|
46
|
+
if (this.limit > 0 && requestWeight > maxAvailableWeight) {
|
|
47
|
+
throw new Error(`Request weight ${requestWeight} exceeds the available per-minute limit ${maxAvailableWeight}`);
|
|
48
|
+
}
|
|
49
|
+
if (this.availableWeight >= requestWeight) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const delayMS = getDelayToNextMinuteMS();
|
|
53
|
+
onWait?.(delayMS);
|
|
54
|
+
await wait(delayMS);
|
|
55
|
+
this._usedWeight = 0;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
updateUsedWeight(usedWeight, fallbackIncrement) {
|
|
59
|
+
if (usedWeight !== undefined) {
|
|
60
|
+
this._usedWeight = usedWeight;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (this.limit > 0 && fallbackIncrement > 0) {
|
|
64
|
+
this._usedWeight += fallbackIncrement;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function getDelayToNextMinuteMS() {
|
|
69
|
+
const now = new Date();
|
|
70
|
+
return Math.max((61 - now.getUTCSeconds()) * 1000 - now.getUTCMilliseconds(), 1);
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=requestweight.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requestweight.js","sourceRoot":"","sources":["../../src/realtimefeeds/requestweight.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tardis-dev",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.2.2",
|
|
4
4
|
"engines": {
|
|
5
5
|
"node": ">=24.19"
|
|
6
6
|
},
|
|
@@ -69,12 +69,12 @@
|
|
|
69
69
|
"license": "MPL-2.0",
|
|
70
70
|
"dependencies": {
|
|
71
71
|
"debug": "^4.4.3",
|
|
72
|
-
"ws": "^8.21.
|
|
72
|
+
"ws": "^8.21.3"
|
|
73
73
|
},
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"@types/node": "^26.
|
|
75
|
+
"@types/node": "^26.2.0",
|
|
76
76
|
"@types/ws": "^8.18.1",
|
|
77
|
-
"prettier": "3.
|
|
77
|
+
"prettier": "3.9.6",
|
|
78
78
|
"typescript": "^6.0.3"
|
|
79
79
|
},
|
|
80
80
|
"runkitExampleFilename": "example.js"
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const EXCHANGES = [
|
|
2
|
+
'aster',
|
|
3
|
+
'aster-futures',
|
|
2
4
|
'bitmex',
|
|
3
5
|
'deribit',
|
|
4
6
|
'binance-futures',
|
|
@@ -64,6 +66,20 @@ export const EXCHANGES = [
|
|
|
64
66
|
] as const
|
|
65
67
|
|
|
66
68
|
const BINANCE_CHANNELS = ['trade', 'aggTrade', 'ticker', 'depth', 'depthSnapshot', 'bookTicker', 'recentTrades', 'borrowInterest'] as const
|
|
69
|
+
|
|
70
|
+
const ASTER_CHANNELS = ['trade', 'aggTrade', 'ticker', 'depth', 'depthSnapshot', 'bookTicker'] as const
|
|
71
|
+
const ASTER_FUTURES_CHANNELS = [
|
|
72
|
+
'trade',
|
|
73
|
+
'aggTrade',
|
|
74
|
+
'ticker',
|
|
75
|
+
'depth',
|
|
76
|
+
'depthSnapshot',
|
|
77
|
+
'bookTicker',
|
|
78
|
+
'markPrice',
|
|
79
|
+
'forceOrder',
|
|
80
|
+
'assetIndex',
|
|
81
|
+
'openInterest'
|
|
82
|
+
] as const
|
|
67
83
|
const BINANCE_DEX_CHANNELS = ['trades', 'marketDiff', 'depthSnapshot', 'ticker'] as const
|
|
68
84
|
const BITFINEX_CHANNELS = ['trades', 'book', 'raw_book', 'ticker'] as const
|
|
69
85
|
|
|
@@ -404,6 +420,8 @@ const BYBIT_CHANNELS = [
|
|
|
404
420
|
'long_short_ratio',
|
|
405
421
|
'orderbook.1',
|
|
406
422
|
'orderbook.50',
|
|
423
|
+
'orderbook.rpi',
|
|
424
|
+
'orderbook.full',
|
|
407
425
|
'orderbook.500',
|
|
408
426
|
'publicTrade',
|
|
409
427
|
'tickers',
|
|
@@ -558,6 +576,8 @@ const POLYMARKET_CHANNELS = [
|
|
|
558
576
|
] as const
|
|
559
577
|
|
|
560
578
|
export const EXCHANGE_CHANNELS_INFO = {
|
|
579
|
+
aster: ASTER_CHANNELS,
|
|
580
|
+
'aster-futures': ASTER_FUTURES_CHANNELS,
|
|
561
581
|
bitmex: BITMEX_CHANNELS,
|
|
562
582
|
coinbase: COINBASE_CHANNELS,
|
|
563
583
|
'coinbase-international': COINBASE_INTERNATIONAL_CHANNELS,
|
|
@@ -76,13 +76,18 @@ export class HttpError extends Error {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
class HttpClientError extends Error {
|
|
79
|
+
export class HttpClientError extends Error {
|
|
80
|
+
public readonly status: number
|
|
81
|
+
public readonly retryAfterMS: number | undefined
|
|
82
|
+
|
|
80
83
|
constructor(
|
|
81
84
|
public readonly response: HttpResponse,
|
|
82
85
|
public readonly method: string,
|
|
83
86
|
public readonly url: string
|
|
84
87
|
) {
|
|
85
88
|
super(`HTTP ${method} ${url} failed with status ${response.statusCode}`)
|
|
89
|
+
this.status = response.statusCode
|
|
90
|
+
this.retryAfterMS = getRetryAfterDelayMS(response.headers)
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { debug } from '../debug.ts'
|
|
2
|
+
import { asNumberOrUndefined, CircularBuffer, lowerCaseSymbols } from '../handy.ts'
|
|
3
|
+
import { BookChange, BookTicker, DerivativeTicker, Liquidation, Trade } from '../types.ts'
|
|
4
|
+
import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
|
|
5
|
+
import { exchangeMappers, isRealTime } from './registry.ts'
|
|
6
|
+
|
|
7
|
+
export const asterMappers = exchangeMappers({
|
|
8
|
+
aster: {
|
|
9
|
+
trades: () => new AsterTradesMapper('aster'),
|
|
10
|
+
bookChanges: (localTimestamp) =>
|
|
11
|
+
new AsterBookChangeMapper('aster', {
|
|
12
|
+
ignoreBookSnapshotOverlapError: shouldIgnoreBookSnapshotOverlap(localTimestamp)
|
|
13
|
+
}),
|
|
14
|
+
bookTickers: () => new AsterBookTickerMapper('aster')
|
|
15
|
+
},
|
|
16
|
+
'aster-futures': {
|
|
17
|
+
trades: () => new AsterTradesMapper('aster-futures'),
|
|
18
|
+
bookChanges: (localTimestamp) =>
|
|
19
|
+
new AsterFuturesBookChangeMapper({
|
|
20
|
+
ignoreBookSnapshotOverlapError: shouldIgnoreBookSnapshotOverlap(localTimestamp)
|
|
21
|
+
}),
|
|
22
|
+
derivativeTickers: () => new AsterFuturesDerivativeTickerMapper(),
|
|
23
|
+
liquidations: () => new AsterFuturesLiquidationsMapper(),
|
|
24
|
+
bookTickers: () => new AsterBookTickerMapper('aster-futures')
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
function shouldIgnoreBookSnapshotOverlap(date?: Date) {
|
|
29
|
+
if (process.env.IGNORE_BOOK_SNAPSHOT_OVERLAP_ERROR) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return isRealTime(date) === false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class AsterTradesMapper implements Mapper<'aster' | 'aster-futures', Trade> {
|
|
37
|
+
constructor(private readonly exchange: 'aster' | 'aster-futures') {}
|
|
38
|
+
|
|
39
|
+
canHandle(message: AsterMessage<any>) {
|
|
40
|
+
return message.stream?.endsWith('@trade') === true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getFilters(symbols?: string[]) {
|
|
44
|
+
return [{ channel: 'trade', symbols: lowerCaseSymbols(symbols) } as const]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
*map({ data }: AsterMessage<AsterTradeData>, localTimestamp: Date) {
|
|
48
|
+
// Aster's Binance-compatible trade stream uses these X values for off-book
|
|
49
|
+
// activity, so exclude them from normalized public trades too.
|
|
50
|
+
const isOffBookTrade = data.X === 'INSURANCE_FUND' || data.X === 'ADL' || data.X === 'NA'
|
|
51
|
+
if (isOffBookTrade) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const trade: Trade = {
|
|
56
|
+
type: 'trade',
|
|
57
|
+
symbol: data.s,
|
|
58
|
+
exchange: this.exchange,
|
|
59
|
+
id: String(data.t),
|
|
60
|
+
price: Number(data.p),
|
|
61
|
+
amount: Number(data.q),
|
|
62
|
+
side: data.m ? 'sell' : 'buy',
|
|
63
|
+
timestamp: new Date(data.T),
|
|
64
|
+
localTimestamp
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
yield trade
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class AsterBookChangeMapper implements Mapper<'aster' | 'aster-futures', BookChange> {
|
|
72
|
+
protected readonly symbolToDepthInfoMapping: { [key: string]: LocalDepthInfo } = {}
|
|
73
|
+
protected readonly ignoreBookSnapshotOverlapError: boolean
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
protected readonly exchange: 'aster' | 'aster-futures',
|
|
77
|
+
{ ignoreBookSnapshotOverlapError }: { ignoreBookSnapshotOverlapError: boolean }
|
|
78
|
+
) {
|
|
79
|
+
this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
canHandle(message: AsterMessage<any>) {
|
|
83
|
+
return message.stream?.includes('@depth') === true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
getFilters(symbols?: string[]) {
|
|
87
|
+
return [
|
|
88
|
+
{ channel: 'depth', symbols: lowerCaseSymbols(symbols) } as const,
|
|
89
|
+
{ channel: 'depthSnapshot', symbols: lowerCaseSymbols(symbols) } as const
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
*map({ stream, data }: AsterMessage<AsterDepthData | AsterDepthSnapshotData>, localTimestamp: Date) {
|
|
94
|
+
const symbol = stream.split('@')[0].toUpperCase()
|
|
95
|
+
|
|
96
|
+
if (this.symbolToDepthInfoMapping[symbol] === undefined) {
|
|
97
|
+
this.symbolToDepthInfoMapping[symbol] = {
|
|
98
|
+
bufferedUpdates: new CircularBuffer<AsterDepthData>(2000)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const symbolDepthInfo = this.symbolToDepthInfoMapping[symbol]
|
|
103
|
+
|
|
104
|
+
if (data.lastUpdateId !== undefined) {
|
|
105
|
+
if (symbolDepthInfo.snapshotProcessed) {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
symbolDepthInfo.lastUpdateId = data.lastUpdateId
|
|
110
|
+
symbolDepthInfo.snapshotProcessed = true
|
|
111
|
+
const bidsByPrice = new Map(data.bids.map((level) => [level[0], level]))
|
|
112
|
+
const asksByPrice = new Map(data.asks.map((level) => [level[0], level]))
|
|
113
|
+
|
|
114
|
+
for (const update of symbolDepthInfo.bufferedUpdates.items()) {
|
|
115
|
+
const bookChange = this.mapBookDepthUpdate(update, localTimestamp)
|
|
116
|
+
if (bookChange !== undefined) {
|
|
117
|
+
this.applyLevelUpdates(bidsByPrice, update.b)
|
|
118
|
+
this.applyLevelUpdates(asksByPrice, update.a)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
symbolDepthInfo.bufferedUpdates.clear()
|
|
123
|
+
|
|
124
|
+
const bookChange: BookChange = {
|
|
125
|
+
type: 'book_change',
|
|
126
|
+
symbol,
|
|
127
|
+
exchange: this.exchange,
|
|
128
|
+
isSnapshot: true,
|
|
129
|
+
bids: [...bidsByPrice.values()].map(this.mapBookLevel),
|
|
130
|
+
asks: [...asksByPrice.values()].map(this.mapBookLevel),
|
|
131
|
+
timestamp: data.T !== undefined ? new Date(data.T) : localTimestamp,
|
|
132
|
+
localTimestamp
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
yield bookChange
|
|
136
|
+
} else if (symbolDepthInfo.snapshotProcessed) {
|
|
137
|
+
const bookChange = this.mapBookDepthUpdate(data as AsterDepthData, localTimestamp)
|
|
138
|
+
if (bookChange !== undefined) {
|
|
139
|
+
yield bookChange
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
symbolDepthInfo.bufferedUpdates.append(data as AsterDepthData)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
protected mapBookDepthUpdate(depthUpdateData: AsterDepthData, localTimestamp: Date): BookChange | undefined {
|
|
147
|
+
const depthContext = this.symbolToDepthInfoMapping[depthUpdateData.s]!
|
|
148
|
+
const lastUpdateId = depthContext.lastUpdateId!
|
|
149
|
+
|
|
150
|
+
if (this.isOutdatedDepthUpdate(depthUpdateData, lastUpdateId)) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!depthContext.validatedFirstUpdate) {
|
|
155
|
+
if (this.hasInitialSnapshotOverlap(depthUpdateData, lastUpdateId) || lastUpdateId === -1) {
|
|
156
|
+
depthContext.validatedFirstUpdate = true
|
|
157
|
+
} else if (this.ignoreBookSnapshotOverlapError) {
|
|
158
|
+
depthContext.validatedFirstUpdate = true
|
|
159
|
+
debug(this.getSnapshotOverlapError(depthUpdateData, lastUpdateId))
|
|
160
|
+
} else {
|
|
161
|
+
throw new Error(this.getSnapshotOverlapError(depthUpdateData, lastUpdateId))
|
|
162
|
+
}
|
|
163
|
+
} else if (depthUpdateData.pu !== lastUpdateId) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Book depth update has a sequence gap, update ${JSON.stringify(
|
|
166
|
+
depthUpdateData
|
|
167
|
+
)}, lastUpdateId: ${lastUpdateId}, exchange ${this.exchange}`
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
depthContext.lastUpdateId = depthUpdateData.u
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
type: 'book_change',
|
|
175
|
+
symbol: depthUpdateData.s,
|
|
176
|
+
exchange: this.exchange,
|
|
177
|
+
isSnapshot: false,
|
|
178
|
+
bids: depthUpdateData.b.map(this.mapBookLevel),
|
|
179
|
+
asks: depthUpdateData.a.map(this.mapBookLevel),
|
|
180
|
+
timestamp: new Date(depthUpdateData.E),
|
|
181
|
+
localTimestamp
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
protected isOutdatedDepthUpdate(depthUpdateData: AsterDepthData, lastUpdateId: number) {
|
|
186
|
+
return depthUpdateData.u <= lastUpdateId
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
protected hasInitialSnapshotOverlap(depthUpdateData: AsterDepthData, lastUpdateId: number) {
|
|
190
|
+
// Live and recorded Spot data use pu as the continuous sequence link. The
|
|
191
|
+
// first non-stale update is safe when its pu..u range contains the snapshot.
|
|
192
|
+
return depthUpdateData.pu <= lastUpdateId && depthUpdateData.u >= lastUpdateId
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private applyLevelUpdates(levelsByPrice: Map<string, AsterBookLevel>, updates: AsterBookLevel[]) {
|
|
196
|
+
for (const level of updates) {
|
|
197
|
+
if (Number(level[1]) === 0) {
|
|
198
|
+
levelsByPrice.delete(level[0])
|
|
199
|
+
} else {
|
|
200
|
+
levelsByPrice.set(level[0], level)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private getSnapshotOverlapError(depthUpdateData: AsterDepthData, lastUpdateId: number) {
|
|
206
|
+
return `Book depth snapshot has no overlap with first update, update ${JSON.stringify(
|
|
207
|
+
depthUpdateData
|
|
208
|
+
)}, lastUpdateId: ${lastUpdateId}, exchange ${this.exchange}`
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private mapBookLevel(level: AsterBookLevel) {
|
|
212
|
+
return {
|
|
213
|
+
price: Number(level[0]),
|
|
214
|
+
amount: Number(level[1])
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
class AsterBookTickerMapper implements Mapper<'aster' | 'aster-futures', BookTicker> {
|
|
220
|
+
constructor(private readonly exchange: 'aster' | 'aster-futures') {}
|
|
221
|
+
|
|
222
|
+
canHandle(message: AsterMessage<any>) {
|
|
223
|
+
return message.stream?.endsWith('@bookTicker') === true
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getFilters(symbols?: string[]) {
|
|
227
|
+
return [{ channel: 'bookTicker', symbols: lowerCaseSymbols(symbols) } as const]
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
*map({ data }: AsterMessage<AsterBookTickerData>, localTimestamp: Date) {
|
|
231
|
+
const ticker: BookTicker = {
|
|
232
|
+
type: 'book_ticker',
|
|
233
|
+
symbol: data.s,
|
|
234
|
+
exchange: this.exchange,
|
|
235
|
+
askAmount: asNumberOrUndefined(data.A),
|
|
236
|
+
askPrice: asNumberOrUndefined(data.a),
|
|
237
|
+
bidPrice: asNumberOrUndefined(data.b),
|
|
238
|
+
bidAmount: asNumberOrUndefined(data.B),
|
|
239
|
+
timestamp: data.E !== undefined ? new Date(data.E) : localTimestamp,
|
|
240
|
+
localTimestamp
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
yield ticker
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
class AsterFuturesBookChangeMapper extends AsterBookChangeMapper {
|
|
248
|
+
constructor({ ignoreBookSnapshotOverlapError }: { ignoreBookSnapshotOverlapError: boolean }) {
|
|
249
|
+
super('aster-futures', { ignoreBookSnapshotOverlapError })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
protected override isOutdatedDepthUpdate(depthUpdateData: AsterDepthData, lastUpdateId: number) {
|
|
253
|
+
return depthUpdateData.u < lastUpdateId
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
protected override hasInitialSnapshotOverlap(depthUpdateData: AsterDepthData, lastUpdateId: number) {
|
|
257
|
+
// Futures normally contains the snapshot id in U..u, but live and recorded
|
|
258
|
+
// data also continues it directly with pu == lastUpdateId. Exact-id partial
|
|
259
|
+
// depth and bookTicker comparisons confirmed that neither form skips a state.
|
|
260
|
+
return (depthUpdateData.U <= lastUpdateId && depthUpdateData.u >= lastUpdateId) || depthUpdateData.pu === lastUpdateId
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
class AsterFuturesDerivativeTickerMapper implements Mapper<'aster-futures', DerivativeTicker> {
|
|
265
|
+
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
|
|
266
|
+
|
|
267
|
+
canHandle(message: AsterMessage<any>) {
|
|
268
|
+
return (
|
|
269
|
+
message.stream?.includes('@markPrice') === true ||
|
|
270
|
+
message.stream?.endsWith('@ticker') === true ||
|
|
271
|
+
message.stream?.endsWith('@openInterest') === true
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
getFilters(symbols?: string[]) {
|
|
276
|
+
const normalizedSymbols = lowerCaseSymbols(symbols)
|
|
277
|
+
|
|
278
|
+
return [
|
|
279
|
+
{ channel: 'markPrice', symbols: normalizedSymbols } as const,
|
|
280
|
+
{ channel: 'ticker', symbols: normalizedSymbols } as const,
|
|
281
|
+
{ channel: 'openInterest', symbols: normalizedSymbols } as const
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
*map(
|
|
286
|
+
{ data }: AsterMessage<AsterFuturesMarkPriceData | AsterFuturesTickerData | AsterFuturesOpenInterestData>,
|
|
287
|
+
localTimestamp: Date
|
|
288
|
+
): IterableIterator<DerivativeTicker> {
|
|
289
|
+
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo('s' in data ? data.s : data.symbol, 'aster-futures')
|
|
290
|
+
|
|
291
|
+
if ('e' in data) {
|
|
292
|
+
if (data.e === 'markPriceUpdate') {
|
|
293
|
+
pendingTickerInfo.updateMarkPrice(Number(data.p))
|
|
294
|
+
if (data.i !== undefined) {
|
|
295
|
+
pendingTickerInfo.updateIndexPrice(Number(data.i))
|
|
296
|
+
}
|
|
297
|
+
if (data.r !== '' && data.T !== 0) {
|
|
298
|
+
pendingTickerInfo.updateFundingRate(Number(data.r))
|
|
299
|
+
pendingTickerInfo.updateFundingTimestamp(new Date(data.T))
|
|
300
|
+
}
|
|
301
|
+
pendingTickerInfo.updateTimestamp(new Date(data.E))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (data.e === '24hrTicker') {
|
|
305
|
+
pendingTickerInfo.updateLastPrice(Number(data.c))
|
|
306
|
+
pendingTickerInfo.updateTimestamp(new Date(data.E))
|
|
307
|
+
}
|
|
308
|
+
} else if ('openInterest' in data) {
|
|
309
|
+
if (data.time === 0) {
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
pendingTickerInfo.updateOpenInterest(Number(data.openInterest))
|
|
313
|
+
pendingTickerInfo.updateTimestamp(new Date(data.time))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (pendingTickerInfo.hasChanged()) {
|
|
317
|
+
yield pendingTickerInfo.getSnapshot(localTimestamp)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
class AsterFuturesLiquidationsMapper implements Mapper<'aster-futures', Liquidation> {
|
|
323
|
+
canHandle(message: AsterMessage<any>) {
|
|
324
|
+
return message.stream?.endsWith('@forceOrder') === true
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
getFilters(symbols?: string[]) {
|
|
328
|
+
return [{ channel: 'forceOrder', symbols: lowerCaseSymbols(symbols) } as const]
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
*map({ data }: AsterMessage<AsterFuturesForceOrderData>, localTimestamp: Date) {
|
|
332
|
+
const order = data.o
|
|
333
|
+
if (order.X !== 'FILLED') {
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const liquidation: Liquidation = {
|
|
338
|
+
type: 'liquidation',
|
|
339
|
+
symbol: order.s,
|
|
340
|
+
exchange: 'aster-futures',
|
|
341
|
+
id: undefined,
|
|
342
|
+
price: Number(order.p),
|
|
343
|
+
amount: Number(order.z),
|
|
344
|
+
side: order.S === 'SELL' ? 'sell' : 'buy',
|
|
345
|
+
timestamp: new Date(order.T),
|
|
346
|
+
localTimestamp
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
yield liquidation
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
type AsterMessage<T> = {
|
|
354
|
+
stream: string
|
|
355
|
+
data: T
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
type AsterTradeData = {
|
|
359
|
+
e: 'trade'
|
|
360
|
+
s: string
|
|
361
|
+
t: number
|
|
362
|
+
p: string
|
|
363
|
+
q: string
|
|
364
|
+
T: number
|
|
365
|
+
m: boolean
|
|
366
|
+
X?: 'INSURANCE_FUND' | 'MARKET' | 'ADL' | 'NA'
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
type AsterDepthData = {
|
|
370
|
+
lastUpdateId: undefined
|
|
371
|
+
E: number
|
|
372
|
+
T: number
|
|
373
|
+
s: string
|
|
374
|
+
U: number
|
|
375
|
+
u: number
|
|
376
|
+
pu: number
|
|
377
|
+
b: AsterBookLevel[]
|
|
378
|
+
a: AsterBookLevel[]
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
type AsterDepthSnapshotData = {
|
|
382
|
+
lastUpdateId: number
|
|
383
|
+
bids: AsterBookLevel[]
|
|
384
|
+
asks: AsterBookLevel[]
|
|
385
|
+
T?: number
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
type AsterBookLevel = [string, string]
|
|
389
|
+
|
|
390
|
+
type LocalDepthInfo = {
|
|
391
|
+
bufferedUpdates: CircularBuffer<AsterDepthData>
|
|
392
|
+
snapshotProcessed?: boolean
|
|
393
|
+
lastUpdateId?: number
|
|
394
|
+
validatedFirstUpdate?: boolean
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
type AsterBookTickerData = {
|
|
398
|
+
u: number
|
|
399
|
+
s: string
|
|
400
|
+
b: string
|
|
401
|
+
B: string
|
|
402
|
+
a: string
|
|
403
|
+
A: string
|
|
404
|
+
E?: number
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
type AsterFuturesTickerData = {
|
|
408
|
+
e: '24hrTicker'
|
|
409
|
+
E: number
|
|
410
|
+
s: string
|
|
411
|
+
c: string
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
type AsterFuturesMarkPriceData = {
|
|
415
|
+
e: 'markPriceUpdate'
|
|
416
|
+
E: number
|
|
417
|
+
s: string
|
|
418
|
+
p: string
|
|
419
|
+
i?: string
|
|
420
|
+
r: string
|
|
421
|
+
T: number
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
type AsterFuturesForceOrderData = {
|
|
425
|
+
e: 'forceOrder'
|
|
426
|
+
E: number
|
|
427
|
+
o: {
|
|
428
|
+
s: string
|
|
429
|
+
S: 'BUY' | 'SELL'
|
|
430
|
+
p: string
|
|
431
|
+
X: string
|
|
432
|
+
z: string
|
|
433
|
+
T: number
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
type AsterFuturesOpenInterestData = {
|
|
438
|
+
symbol: string
|
|
439
|
+
openInterest: string
|
|
440
|
+
time: number
|
|
441
|
+
}
|
|
@@ -164,28 +164,15 @@ class BinanceBookChangeMapper implements Mapper<
|
|
|
164
164
|
// mark given symbol depth info that has snapshot processed
|
|
165
165
|
symbolDepthInfo.lastUpdateId = binanceDepthSnapshotData.lastUpdateId
|
|
166
166
|
symbolDepthInfo.snapshotProcessed = true
|
|
167
|
+
const bidsByPrice = new Map(binanceDepthSnapshotData.bids.map((level) => [level[0], level]))
|
|
168
|
+
const asksByPrice = new Map(binanceDepthSnapshotData.asks.map((level) => [level[0], level]))
|
|
167
169
|
|
|
168
170
|
// if there were any depth updates buffered, let's proccess those by adding to or updating the initial snapshot
|
|
169
171
|
for (const update of symbolDepthInfo.bufferedUpdates.items()) {
|
|
170
172
|
const bookChange = this.mapBookDepthUpdate(update, localTimestamp)
|
|
171
173
|
if (bookChange !== undefined) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (matchingBid !== undefined) {
|
|
175
|
-
matchingBid[1] = bid[1]
|
|
176
|
-
} else {
|
|
177
|
-
binanceDepthSnapshotData.bids.push(bid)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
for (const ask of update.a) {
|
|
182
|
-
const matchingAsk = binanceDepthSnapshotData.asks.find((a) => a[0] === ask[0])
|
|
183
|
-
if (matchingAsk !== undefined) {
|
|
184
|
-
matchingAsk[1] = ask[1]
|
|
185
|
-
} else {
|
|
186
|
-
binanceDepthSnapshotData.asks.push(ask)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
174
|
+
this.applyLevelUpdates(bidsByPrice, update.b)
|
|
175
|
+
this.applyLevelUpdates(asksByPrice, update.a)
|
|
189
176
|
}
|
|
190
177
|
}
|
|
191
178
|
|
|
@@ -197,8 +184,8 @@ class BinanceBookChangeMapper implements Mapper<
|
|
|
197
184
|
symbol,
|
|
198
185
|
exchange: this.exchange,
|
|
199
186
|
isSnapshot: true,
|
|
200
|
-
bids:
|
|
201
|
-
asks:
|
|
187
|
+
bids: [...bidsByPrice.values()].map(this.mapBookLevel),
|
|
188
|
+
asks: [...asksByPrice.values()].map(this.mapBookLevel),
|
|
202
189
|
timestamp: binanceDepthSnapshotData.T !== undefined ? fromMicroSecondsToDate(binanceDepthSnapshotData.T) : localTimestamp,
|
|
203
190
|
localTimestamp
|
|
204
191
|
}
|
|
@@ -266,6 +253,16 @@ class BinanceBookChangeMapper implements Mapper<
|
|
|
266
253
|
const amount = Number(level[1])
|
|
267
254
|
return { price, amount }
|
|
268
255
|
}
|
|
256
|
+
|
|
257
|
+
private applyLevelUpdates(levelsByPrice: Map<string, BinanceBookLevel>, updates: BinanceBookLevel[]) {
|
|
258
|
+
for (const level of updates) {
|
|
259
|
+
if (Number(level[1]) === 0) {
|
|
260
|
+
levelsByPrice.delete(level[0])
|
|
261
|
+
} else {
|
|
262
|
+
levelsByPrice.set(level[0], level)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
269
266
|
}
|
|
270
267
|
|
|
271
268
|
class BinanceFuturesBookChangeMapper extends BinanceBookChangeMapper implements Mapper<'binance-futures' | 'binance-delivery', BookChange> {
|
|
@@ -281,9 +278,14 @@ class BinanceFuturesBookChangeMapper extends BinanceBookChangeMapper implements
|
|
|
281
278
|
return
|
|
282
279
|
}
|
|
283
280
|
|
|
284
|
-
//
|
|
281
|
+
// Binance documents a first update that contains the snapshot boundary. Live
|
|
282
|
+
// USD-M and COIN-M streams can instead continue it directly with pu == lastUpdateId.
|
|
283
|
+
// Both relations join the buffered stream to the snapshot without an update gap.
|
|
285
284
|
if (!depthContext.validatedFirstUpdate) {
|
|
286
|
-
|
|
285
|
+
const containsSnapshotBoundary = binanceDepthUpdateData.U <= lastUpdateId && binanceDepthUpdateData.u >= lastUpdateId
|
|
286
|
+
const continuesSnapshotDirectly = binanceDepthUpdateData.pu === lastUpdateId
|
|
287
|
+
|
|
288
|
+
if (containsSnapshotBoundary || continuesSnapshotDirectly) {
|
|
287
289
|
depthContext.validatedFirstUpdate = true
|
|
288
290
|
} else {
|
|
289
291
|
const message = `Book depth snaphot has no overlap with first update, update ${JSON.stringify(
|