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.
Files changed (102) hide show
  1. package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
  2. package/dist/ws/subscriptionsmappers.js +3 -1
  3. package/dist/ws/subscriptionsmappers.js.map +1 -1
  4. package/node_modules/tardis-dev/dist/consts.d.ts +4 -2
  5. package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
  6. package/node_modules/tardis-dev/dist/consts.js +19 -0
  7. package/node_modules/tardis-dev/dist/consts.js.map +1 -1
  8. package/node_modules/tardis-dev/dist/handy.d.ts +13 -0
  9. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  10. package/node_modules/tardis-dev/dist/handy.js +5 -1
  11. package/node_modules/tardis-dev/dist/handy.js.map +1 -1
  12. package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
  13. package/node_modules/tardis-dev/dist/mappers/aster.d.ts +16 -0
  14. package/node_modules/tardis-dev/dist/mappers/aster.d.ts.map +1 -0
  15. package/node_modules/tardis-dev/dist/mappers/aster.js +297 -0
  16. package/node_modules/tardis-dev/dist/mappers/aster.js.map +1 -0
  17. package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
  18. package/node_modules/tardis-dev/dist/mappers/binance.js +22 -22
  19. package/node_modules/tardis-dev/dist/mappers/binance.js.map +1 -1
  20. package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
  21. package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
  22. package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
  23. package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
  24. package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
  25. package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
  26. package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +1 -1
  27. package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
  28. package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
  29. package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +7 -7
  30. package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
  31. package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
  32. package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
  33. package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
  34. package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
  35. package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
  36. package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +3 -3
  37. package/node_modules/tardis-dev/dist/mappers/deribit.js +16 -5
  38. package/node_modules/tardis-dev/dist/mappers/deribit.js.map +1 -1
  39. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  40. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  41. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  42. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  43. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  44. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  45. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  46. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +3 -3
  47. package/node_modules/tardis-dev/dist/mappers/index.d.ts +180 -162
  48. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  49. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  50. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  51. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  52. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  53. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +5 -5
  54. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  55. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  56. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  57. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  58. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  59. package/node_modules/tardis-dev/dist/mappers/registry.d.ts.map +1 -1
  60. package/node_modules/tardis-dev/dist/mappers/registry.js.map +1 -1
  61. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  62. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  63. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts +53 -0
  64. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts.map +1 -0
  65. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js +271 -0
  66. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js.map +1 -0
  67. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts +1 -4
  68. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
  69. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +36 -95
  70. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
  71. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  72. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +3 -0
  73. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  74. package/node_modules/tardis-dev/dist/realtimefeeds/kucoinfutures.d.ts.map +1 -1
  75. package/node_modules/tardis-dev/dist/realtimefeeds/kucoinfutures.js +6 -1
  76. package/node_modules/tardis-dev/dist/realtimefeeds/kucoinfutures.js.map +1 -1
  77. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  78. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +15 -18
  79. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  80. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts +14 -0
  81. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts.map +1 -0
  82. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js +72 -0
  83. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js.map +1 -0
  84. package/node_modules/tardis-dev/package.json +4 -4
  85. package/node_modules/tardis-dev/src/consts.ts +20 -0
  86. package/node_modules/tardis-dev/src/handy.ts +6 -1
  87. package/node_modules/tardis-dev/src/mappers/aster.ts +441 -0
  88. package/node_modules/tardis-dev/src/mappers/binance.ts +23 -21
  89. package/node_modules/tardis-dev/src/mappers/bitfinex.ts +1 -2
  90. package/node_modules/tardis-dev/src/mappers/deribit.ts +18 -5
  91. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  92. package/node_modules/tardis-dev/src/mappers/registry.ts +1 -2
  93. package/node_modules/tardis-dev/src/realtimefeeds/aster.ts +388 -0
  94. package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +59 -145
  95. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +3 -0
  96. package/node_modules/tardis-dev/src/realtimefeeds/kucoinfutures.ts +7 -1
  97. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +15 -21
  98. package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
  99. package/node_modules/ws/lib/permessage-deflate.js +3 -1
  100. package/node_modules/ws/package.json +1 -1
  101. package/package.json +5 -5
  102. 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.1.0",
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.1"
72
+ "ws": "^8.21.3"
73
73
  },
74
74
  "devDependencies": {
75
- "@types/node": "^26.1.1",
75
+ "@types/node": "^26.2.0",
76
76
  "@types/ws": "^8.18.1",
77
- "prettier": "3.8.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
- for (const bid of update.b) {
173
- const matchingBid = binanceDepthSnapshotData.bids.find((b) => b[0] === bid[0])
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: binanceDepthSnapshotData.bids.map(this.mapBookLevel),
201
- asks: binanceDepthSnapshotData.asks.map(this.mapBookLevel),
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
- // The first processed should have U <= lastUpdateId AND u >= lastUpdateId
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
- if (binanceDepthUpdateData.U <= lastUpdateId && binanceDepthUpdateData.u >= lastUpdateId) {
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(