tardis-machine 18.0.1 → 18.2.0

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 (114) 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 +3 -1
  5. package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
  6. package/node_modules/tardis-dev/dist/consts.js +17 -0
  7. package/node_modules/tardis-dev/dist/consts.js.map +1 -1
  8. package/node_modules/tardis-dev/dist/downloaddatasets.d.ts.map +1 -1
  9. package/node_modules/tardis-dev/dist/downloaddatasets.js +3 -1
  10. package/node_modules/tardis-dev/dist/downloaddatasets.js.map +1 -1
  11. package/node_modules/tardis-dev/dist/handy.d.ts +15 -1
  12. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  13. package/node_modules/tardis-dev/dist/handy.js +10 -5
  14. package/node_modules/tardis-dev/dist/handy.js.map +1 -1
  15. package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
  16. package/node_modules/tardis-dev/dist/mappers/aster.d.ts +16 -0
  17. package/node_modules/tardis-dev/dist/mappers/aster.d.ts.map +1 -0
  18. package/node_modules/tardis-dev/dist/mappers/aster.js +294 -0
  19. package/node_modules/tardis-dev/dist/mappers/aster.js.map +1 -0
  20. package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
  21. package/node_modules/tardis-dev/dist/mappers/binance.js +22 -22
  22. package/node_modules/tardis-dev/dist/mappers/binance.js.map +1 -1
  23. package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
  24. package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
  25. package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
  26. package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
  27. package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
  28. package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
  29. package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +1 -1
  30. package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
  31. package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
  32. package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +7 -7
  33. package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
  34. package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
  35. package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
  36. package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
  37. package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
  38. package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
  39. package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +2 -2
  40. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  41. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  42. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  43. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  44. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  45. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  46. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  47. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +4 -4
  48. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts.map +1 -1
  49. package/node_modules/tardis-dev/dist/mappers/hyperliquid.js +15 -4
  50. package/node_modules/tardis-dev/dist/mappers/hyperliquid.js.map +1 -1
  51. package/node_modules/tardis-dev/dist/mappers/index.d.ts +180 -162
  52. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  53. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  54. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  55. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  56. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  57. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +5 -5
  58. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  59. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  60. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  61. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  62. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  63. package/node_modules/tardis-dev/dist/mappers/registry.d.ts.map +1 -1
  64. package/node_modules/tardis-dev/dist/mappers/registry.js.map +1 -1
  65. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  66. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  67. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts +53 -0
  68. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts.map +1 -0
  69. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js +271 -0
  70. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js.map +1 -0
  71. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts +1 -4
  72. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
  73. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +36 -95
  74. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
  75. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  76. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +3 -0
  77. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  78. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  79. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +12 -3
  80. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  81. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts +14 -0
  82. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts.map +1 -0
  83. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js +72 -0
  84. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js.map +1 -0
  85. package/node_modules/tardis-dev/dist/replay.d.ts.map +1 -1
  86. package/node_modules/tardis-dev/dist/replay.js +6 -4
  87. package/node_modules/tardis-dev/dist/replay.js.map +1 -1
  88. package/node_modules/tardis-dev/dist/worker.d.ts.map +1 -1
  89. package/node_modules/tardis-dev/dist/worker.js +5 -2
  90. package/node_modules/tardis-dev/dist/worker.js.map +1 -1
  91. package/node_modules/tardis-dev/dist/zstd.d.ts +3 -0
  92. package/node_modules/tardis-dev/dist/zstd.d.ts.map +1 -0
  93. package/node_modules/tardis-dev/dist/zstd.js +122 -0
  94. package/node_modules/tardis-dev/dist/zstd.js.map +1 -0
  95. package/node_modules/tardis-dev/package.json +2 -2
  96. package/node_modules/tardis-dev/src/consts.ts +18 -0
  97. package/node_modules/tardis-dev/src/downloaddatasets.ts +3 -1
  98. package/node_modules/tardis-dev/src/handy.ts +13 -5
  99. package/node_modules/tardis-dev/src/mappers/aster.ts +438 -0
  100. package/node_modules/tardis-dev/src/mappers/binance.ts +23 -21
  101. package/node_modules/tardis-dev/src/mappers/bitfinex.ts +1 -2
  102. package/node_modules/tardis-dev/src/mappers/hyperliquid.ts +19 -5
  103. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  104. package/node_modules/tardis-dev/src/mappers/registry.ts +1 -2
  105. package/node_modules/tardis-dev/src/realtimefeeds/aster.ts +388 -0
  106. package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +59 -145
  107. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +3 -0
  108. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +12 -3
  109. package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
  110. package/node_modules/tardis-dev/src/replay.ts +6 -4
  111. package/node_modules/tardis-dev/src/worker.ts +5 -2
  112. package/node_modules/tardis-dev/src/zstd.ts +139 -0
  113. package/package.json +4 -4
  114. package/src/ws/subscriptionsmappers.ts +3 -1
@@ -2,12 +2,13 @@ import { Writable } from 'stream'
2
2
  import { batch, getJSON, wait } from '../handy.ts'
3
3
  import { Filter } from '../types.ts'
4
4
  import { MultiConnectionRealTimeFeedBase, PoolingClientBase, RealTimeFeedBase } from './realtimefeed.ts'
5
+ import { getExchangeScopedNumberEnv, getRequestWeightLimit, parseRequestWeightHeader, RequestWeightLimiter } from './requestweight.ts'
5
6
 
6
7
  const binanceHttpOptions = {
7
8
  timeout: 10 * 1000,
8
9
  retry: {
9
10
  limit: 10,
10
- statusCodes: [418, 429, 500, 403],
11
+ statusCodes: [429, 500, 403],
11
12
  maxRetryAfter: 120 * 1000
12
13
  }
13
14
  }
@@ -25,29 +26,6 @@ const BINANCE_FUTURES_MARKET_STREAM_PATH = '/market/stream'
25
26
 
26
27
  type BinanceFuturesStreamPath = typeof BINANCE_FUTURES_PUBLIC_STREAM_PATH | typeof BINANCE_FUTURES_MARKET_STREAM_PATH
27
28
 
28
- function parseBinanceWeightHeader(headerValue: string | undefined) {
29
- if (headerValue === undefined) {
30
- return undefined
31
- }
32
-
33
- const parsed = Number.parseInt(headerValue, 10)
34
-
35
- return Number.isFinite(parsed) ? parsed : undefined
36
- }
37
-
38
- function getExchangeScopedNumberEnv(exchange: string, suffix: string, fallback: number) {
39
- const envName = `${exchange.toUpperCase().replace(/-/g, '_')}_${suffix}`
40
- const rawValue = process.env[envName]
41
-
42
- if (rawValue === undefined) {
43
- return fallback
44
- }
45
-
46
- const parsed = Number.parseInt(rawValue, 10)
47
-
48
- return Number.isFinite(parsed) ? parsed : fallback
49
- }
50
-
51
29
  function getExchangeScopedWssUrlEnv(exchange: string) {
52
30
  const envName = `WSS_URL_${exchange.toUpperCase().replace(/-/g, '_')}`
53
31
 
@@ -68,31 +46,6 @@ function getBinanceFuturesWebSocketUrl(exchange: string, streamPath: BinanceFutu
68
46
  return `${normalizedBaseUrl}${streamPath}`
69
47
  }
70
48
 
71
- function getBinanceRequestWeightLimit(exchange: string, exchangeInfo: any) {
72
- const configuredLimit = getExchangeScopedNumberEnv(exchange, 'REQUEST_WEIGHT_LIMIT', 0)
73
- if (configuredLimit > 0) {
74
- return configuredLimit
75
- }
76
-
77
- const requestWeightLimit = exchangeInfo.rateLimits.find((d: any) => d.rateLimitType === 'REQUEST_WEIGHT')?.limit as number | undefined
78
-
79
- if (!requestWeightLimit) {
80
- throw new Error('Failed to determine Binance REQUEST_WEIGHT limit')
81
- }
82
-
83
- return requestWeightLimit
84
- }
85
-
86
- function getBinanceAvailableWeight(weightLimit: number, usedWeight: number, buffer: number) {
87
- return weightLimit > 0 ? weightLimit - usedWeight - buffer : Infinity
88
- }
89
-
90
- function getDelayToNextMinuteMS() {
91
- const now = new Date()
92
-
93
- return Math.max((61 - now.getUTCSeconds()) * 1000 - now.getUTCMilliseconds(), 1)
94
- }
95
-
96
49
  abstract class BinanceRealTimeFeedBase extends MultiConnectionRealTimeFeedBase {
97
50
  protected abstract wssURL: string
98
51
  protected abstract httpURL: string
@@ -131,8 +84,7 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
131
84
  private readonly _minAvailableWeightBuffer: number
132
85
  private readonly _maxPollingIntervalMS: number
133
86
  private _currentPollingIntervalMS: number
134
- private _requestWeightLimit: number
135
- private _usedWeight = 0
87
+ private _requestWeightLimiter: RequestWeightLimiter | undefined
136
88
 
137
89
  constructor(
138
90
  private readonly _exchange: string,
@@ -150,12 +102,15 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
150
102
  this._minPollingIntervalMS = minPollingIntervalMS
151
103
  this._maxPollingIntervalMS = Math.max(this._minPollingIntervalMS, OPEN_INTEREST_MAX_POLLING_INTERVAL_MS)
152
104
  this._currentPollingIntervalMS = minPollingIntervalMS
153
- this._requestWeightLimit = getExchangeScopedNumberEnv(_exchange, 'REQUEST_WEIGHT_LIMIT', 0)
154
- this._minAvailableWeightBuffer = getExchangeScopedNumberEnv(
155
- _exchange,
156
- 'MIN_AVAILABLE_WEIGHT_BUFFER',
157
- DEFAULT_OPEN_INTEREST_MIN_AVAILABLE_WEIGHT_BUFFER
105
+ this._minAvailableWeightBuffer = Math.max(
106
+ getExchangeScopedNumberEnv(_exchange, 'MIN_AVAILABLE_WEIGHT_BUFFER', DEFAULT_OPEN_INTEREST_MIN_AVAILABLE_WEIGHT_BUFFER),
107
+ 0
158
108
  )
109
+
110
+ const configuredRequestWeightLimit = getExchangeScopedNumberEnv(_exchange, 'REQUEST_WEIGHT_LIMIT', 0)
111
+ if (configuredRequestWeightLimit > 0) {
112
+ this._requestWeightLimiter = new RequestWeightLimiter(configuredRequestWeightLimit, this._minAvailableWeightBuffer)
113
+ }
159
114
  }
160
115
 
161
116
  protected getPoolingDelayMS() {
@@ -165,22 +120,23 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
165
120
  protected async poolDataToStream(outputStream: Writable) {
166
121
  let waitedForRateLimit = false
167
122
 
168
- if (!this._requestWeightLimit) {
123
+ if (this._requestWeightLimiter === undefined) {
169
124
  await this._initializeRateLimitInfo()
170
125
  }
171
126
 
172
- for (let index = 0; index < this._instruments.length; ) {
127
+ for (let index = 0; index < this._instruments.length;) {
173
128
  if (outputStream.destroyed) {
174
129
  return
175
130
  }
176
131
 
177
- waitedForRateLimit = (await this._waitForAvailableWeight()) || waitedForRateLimit
178
- const batchSize = this._getBatchSize()
179
-
180
- if (batchSize <= 0) {
181
- break
132
+ if (await this._waitForAvailableWeight()) {
133
+ waitedForRateLimit = true
134
+ }
135
+ if (outputStream.destroyed) {
136
+ return
182
137
  }
183
138
 
139
+ const batchSize = Math.min(OPEN_INTEREST_BATCH_SIZE, Math.floor(this._requestWeightLimiter!.availableWeight))
184
140
  const instrumentsBatch = this._instruments.slice(index, index + batchSize)
185
141
  index += instrumentsBatch.length
186
142
 
@@ -193,14 +149,13 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
193
149
 
194
150
  return {
195
151
  instrument,
196
- usedWeight: parseBinanceWeightHeader(openInterestResponse.headers['x-mbx-used-weight-1m']),
152
+ usedWeight: parseRequestWeightHeader(openInterestResponse.headers['x-mbx-used-weight-1m']),
197
153
  data: openInterestResponse.data
198
154
  }
199
155
  })
200
156
  )
201
157
 
202
158
  let maxUsedWeight: number | undefined
203
- let fulfilledCount = 0
204
159
 
205
160
  for (const result of results) {
206
161
  if (result.status === 'rejected') {
@@ -208,8 +163,9 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
208
163
  continue
209
164
  }
210
165
 
211
- fulfilledCount++
212
- maxUsedWeight = Math.max(maxUsedWeight ?? 0, result.value.usedWeight ?? 0)
166
+ if (result.value.usedWeight !== undefined) {
167
+ maxUsedWeight = Math.max(maxUsedWeight ?? 0, result.value.usedWeight)
168
+ }
213
169
 
214
170
  if (outputStream.writable) {
215
171
  outputStream.write({
@@ -220,7 +176,7 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
220
176
  }
221
177
  }
222
178
 
223
- this._updateUsedWeight(maxUsedWeight || undefined, fulfilledCount * OPEN_INTEREST_REQUEST_WEIGHT)
179
+ this._requestWeightLimiter!.updateUsedWeight(maxUsedWeight, instrumentsBatch.length * OPEN_INTEREST_REQUEST_WEIGHT)
224
180
  }
225
181
 
226
182
  if (waitedForRateLimit) {
@@ -234,43 +190,26 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
234
190
  }
235
191
 
236
192
  private async _waitForAvailableWeight() {
237
- const available = getBinanceAvailableWeight(this._requestWeightLimit, this._usedWeight, this._minAvailableWeightBuffer)
238
-
239
- if (available >= OPEN_INTEREST_REQUEST_WEIGHT) {
240
- return false
241
- }
242
-
243
- const delayMS = getDelayToNextMinuteMS()
244
- this.debug(
245
- 'open interest reached rate limit (limit: %s, used: %s, minimum available buffer: %s), waiting %s ms',
246
- this._requestWeightLimit,
247
- this._usedWeight,
248
- this._minAvailableWeightBuffer,
249
- delayMS
250
- )
251
-
252
- await wait(delayMS)
253
-
254
- // Binance request weight is tracked in a rolling 1-minute window. After waiting for the next minute
255
- // we can resume and let the next REST response header refresh the exact current usage.
256
- this._usedWeight = 0
257
-
258
- return true
193
+ return this._requestWeightLimiter!.waitForAvailableWeight(OPEN_INTEREST_REQUEST_WEIGHT, (delayMS) => {
194
+ this.debug(
195
+ 'open interest reached rate limit (limit: %s, used: %s, minimum available buffer: %s), waiting %s ms',
196
+ this._requestWeightLimiter!.limit,
197
+ this._requestWeightLimiter!.usedWeight,
198
+ this._requestWeightLimiter!.minAvailableWeightBuffer,
199
+ delayMS
200
+ )
201
+ })
259
202
  }
260
203
 
261
204
  private async _initializeRateLimitInfo() {
262
205
  const exchangeInfoResponse = await getJSON<any>(`${this._httpURL}/exchangeInfo`, binanceHttpOptions)
263
206
  const exchangeInfo = exchangeInfoResponse.data
264
207
 
265
- this._requestWeightLimit = getBinanceRequestWeightLimit(this._exchange, exchangeInfo)
266
-
267
- this._updateUsedWeight(parseBinanceWeightHeader(exchangeInfoResponse.headers['x-mbx-used-weight-1m']), OPEN_INTEREST_REQUEST_WEIGHT)
268
- }
269
-
270
- private _getBatchSize() {
271
- const available = getBinanceAvailableWeight(this._requestWeightLimit, this._usedWeight, this._minAvailableWeightBuffer)
272
-
273
- return Math.min(OPEN_INTEREST_BATCH_SIZE, Math.max(0, Math.floor(available)))
208
+ this._requestWeightLimiter = new RequestWeightLimiter(
209
+ getRequestWeightLimit(this._exchange, exchangeInfo),
210
+ this._minAvailableWeightBuffer,
211
+ parseRequestWeightHeader(exchangeInfoResponse.headers['x-mbx-used-weight-1m'])
212
+ )
274
213
  }
275
214
 
276
215
  private _notifyError(error: unknown) {
@@ -282,17 +221,6 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
282
221
  this.onError(normalizedError)
283
222
  }
284
223
  }
285
-
286
- private _updateUsedWeight(usedWeight: number | undefined, fallbackIncrement = OPEN_INTEREST_REQUEST_WEIGHT) {
287
- if (usedWeight !== undefined) {
288
- this._usedWeight = usedWeight
289
- return
290
- }
291
-
292
- if (this._requestWeightLimit > 0 && fallbackIncrement > 0) {
293
- this._usedWeight += fallbackIncrement
294
- }
295
- }
296
224
  }
297
225
 
298
226
  class BinanceSingleConnectionRealTimeFeed extends RealTimeFeedBase {
@@ -361,27 +289,21 @@ class BinanceSingleConnectionRealTimeFeed extends RealTimeFeedBase {
361
289
  const exchangeInfo = exchangeInfoResponse.data
362
290
 
363
291
  const DELAY_ENV = `${this._exchange.toUpperCase().replace(/-/g, '_')}_SNAPSHOTS_DELAY_MS`
364
- const currentWeightLimit = getBinanceRequestWeightLimit(this._exchange, exchangeInfo)
292
+ const currentWeightLimit = getRequestWeightLimit(this._exchange, exchangeInfo)
365
293
 
366
- let usedWeight = Number.parseInt(exchangeInfoResponse.headers['x-mbx-used-weight-1m'] as string)
294
+ const usedWeight = parseRequestWeightHeader(exchangeInfoResponse.headers['x-mbx-used-weight-1m']) ?? 0
367
295
 
368
296
  this.debug('current x-mbx-used-weight-1m limit: %s, already used weight: %s', currentWeightLimit, usedWeight)
369
297
 
370
- let concurrencyLimit = 4
371
-
372
- const CONCURRENCY_LIMIT_WEIGHT_ENV = `${this._exchange.toUpperCase().replace(/-/g, '_')}_CONCURRENCY_LIMIT`
373
-
374
- if (process.env[CONCURRENCY_LIMIT_WEIGHT_ENV] !== undefined) {
375
- concurrencyLimit = Number.parseInt(process.env[CONCURRENCY_LIMIT_WEIGHT_ENV] as string)
376
- }
298
+ const concurrencyLimit = Math.max(getExchangeScopedNumberEnv(this._exchange, 'CONCURRENCY_LIMIT', 4), 1)
377
299
 
378
300
  this.debug('current snapshots requests concurrency limit: %s', concurrencyLimit)
379
301
 
380
- const minWeightBuffer = getExchangeScopedNumberEnv(
381
- this._exchange,
382
- 'MIN_AVAILABLE_WEIGHT_BUFFER',
383
- 2 * concurrencyLimit * this._depthRequestRequestWeight
302
+ const minWeightBuffer = Math.max(
303
+ getExchangeScopedNumberEnv(this._exchange, 'MIN_AVAILABLE_WEIGHT_BUFFER', 2 * concurrencyLimit * this._depthRequestRequestWeight),
304
+ 0
384
305
  )
306
+ const requestWeightLimiter = new RequestWeightLimiter(currentWeightLimit, minWeightBuffer, usedWeight)
385
307
 
386
308
  for (const symbolsBatch of batch(depthSnapshotFilter.symbols!, concurrencyLimit)) {
387
309
  if (shouldCancel()) {
@@ -390,31 +312,22 @@ class BinanceSingleConnectionRealTimeFeed extends RealTimeFeedBase {
390
312
 
391
313
  this.debug('requesting manual snapshots for: %s', symbolsBatch)
392
314
 
315
+ await requestWeightLimiter.waitForAvailableWeight(symbolsBatch.length * this._depthRequestRequestWeight, (delayMS) => {
316
+ this.debug(
317
+ 'reached rate limit (x-mbx-used-weight-1m limit: %s, used weight: %s, minimum available weight buffer: %s), waiting: %s seconds',
318
+ currentWeightLimit,
319
+ requestWeightLimiter.usedWeight,
320
+ minWeightBuffer,
321
+ Math.ceil(delayMS / 1000)
322
+ )
323
+ })
324
+
393
325
  const usedWeights = await Promise.all(
394
326
  symbolsBatch.map(async (symbol) => {
395
327
  if (shouldCancel()) {
396
328
  return 0
397
329
  }
398
330
 
399
- const isOverRateLimit = getBinanceAvailableWeight(currentWeightLimit, usedWeight, minWeightBuffer) < 0
400
-
401
- if (isOverRateLimit) {
402
- const delayMS = getDelayToNextMinuteMS()
403
- this.debug(
404
- 'reached rate limit (x-mbx-used-weight-1m limit: %s, used weight: %s, minimum available weight buffer: %s), waiting: %s seconds',
405
- currentWeightLimit,
406
- usedWeight,
407
- minWeightBuffer,
408
- Math.ceil(delayMS / 1000)
409
- )
410
-
411
- await wait(delayMS)
412
- }
413
-
414
- if (shouldCancel()) {
415
- return 0
416
- }
417
-
418
331
  const depthSnapshotResponse = await getJSON<any>(
419
332
  `${this._httpURL}/depth?symbol=${symbol.toUpperCase()}&limit=1000`,
420
333
  binanceHttpOptions
@@ -437,13 +350,14 @@ class BinanceSingleConnectionRealTimeFeed extends RealTimeFeedBase {
437
350
  await wait(msToWait)
438
351
  }
439
352
 
440
- return Number.parseInt(depthSnapshotResponse.headers['x-mbx-used-weight-1m'] as string)
353
+ return parseRequestWeightHeader(depthSnapshotResponse.headers['x-mbx-used-weight-1m'])
441
354
  })
442
355
  )
443
356
 
444
- usedWeight = Math.max(...usedWeights)
357
+ const maxUsedWeight = Math.max(...usedWeights.map((weight) => weight ?? 0))
358
+ requestWeightLimiter.updateUsedWeight(maxUsedWeight || undefined, symbolsBatch.length * this._depthRequestRequestWeight)
445
359
 
446
- this.debug('requested manual snapshots successfully for: %s, used weight: %s', symbolsBatch, usedWeight)
360
+ this.debug('requested manual snapshots successfully for: %s, used weight: %s', symbolsBatch, requestWeightLimiter.usedWeight)
447
361
  }
448
362
  this.debug('requested all manual snapshots successfully')
449
363
  }
@@ -1,4 +1,5 @@
1
1
  import { Exchange, Filter } from '../types.ts'
2
+ import { AsterFuturesRealTimeFeed, AsterRealTimeFeed } from './aster.ts'
2
3
  import {
3
4
  BinanceFuturesRealTimeFeed,
4
5
  BinanceJerseyRealTimeFeed,
@@ -62,6 +63,8 @@ export * from './realtimefeed.ts'
62
63
  const realTimeFeedsMap: {
63
64
  [key in Exchange]?: RealTimeFeed
64
65
  } = {
66
+ aster: AsterRealTimeFeed,
67
+ 'aster-futures': AsterFuturesRealTimeFeed,
65
68
  bitmex: BitmexRealTimeFeed,
66
69
  binance: BinanceRealTimeFeed,
67
70
  'binance-jersey': BinanceJerseyRealTimeFeed,
@@ -3,7 +3,7 @@ import type { ClientRequestArgs } from 'http'
3
3
  import { PassThrough, Writable } from 'stream'
4
4
  import { setTimeout as sleep } from 'node:timers/promises'
5
5
  import { createDebug, type DebugLogger } from '../debug.ts'
6
- import { getProxyAgent, ONE_SEC_IN_MS, optimizeFilters } from '../handy.ts'
6
+ import { getProxyAgent, HttpClientError, ONE_SEC_IN_MS, optimizeFilters } from '../handy.ts'
7
7
  import { createManagedRealTimeIterator, mergeRealTime, type ManagedRealTimeIterator } from '../realtimeiterator.ts'
8
8
  import { Exchange, Filter } from '../types.ts'
9
9
 
@@ -220,8 +220,15 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
220
220
  retries++
221
221
 
222
222
  const MAX_DELAY = 32 * 1000
223
- const isRateLimited = connectionError.message.includes('429')
224
- retryDelay = isRateLimited ? (MAX_DELAY / 2) * retries : Math.min(Math.pow(2, retries - 1) * 1000, MAX_DELAY)
223
+ const httpError = connectionError instanceof HttpClientError ? connectionError : undefined
224
+ const isRateLimited = httpError?.status === 418 || httpError?.status === 429 || connectionError.message.includes('429')
225
+
226
+ if (isRateLimited) {
227
+ const fallbackDelay = httpError?.status === 418 ? 2 * 60 * 1000 : (MAX_DELAY / 2) * retries
228
+ retryDelay = httpError?.retryAfterMS ?? fallbackDelay
229
+ } else {
230
+ retryDelay = Math.min(Math.pow(2, retries - 1) * 1000, MAX_DELAY)
231
+ }
225
232
 
226
233
  this.debug(
227
234
  '(connection id: %d) %s real-time feed connection error, retries count: %d, next retry delay: %dms, rate limited: %s error message: %o',
@@ -373,7 +380,9 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
373
380
  } catch (e) {
374
381
  this.debug('(connection id: %d) providing manual snapshots error: %o', connection.id, e)
375
382
  if (this._isConnectionOpen(connection)) {
383
+ // createWebSocketStream propagates a manually emitted error only after the WebSocket closes.
376
384
  connection.ws.emit('error', e)
385
+ connection.ws.terminate()
377
386
  }
378
387
  }
379
388
  }
@@ -0,0 +1,91 @@
1
+ import { wait } from '../handy.ts'
2
+
3
+ export function parseRequestWeightHeader(headerValue: string | undefined) {
4
+ if (headerValue === undefined) {
5
+ return undefined
6
+ }
7
+
8
+ const parsed = Number.parseInt(headerValue, 10)
9
+
10
+ return Number.isFinite(parsed) ? parsed : undefined
11
+ }
12
+
13
+ export function getExchangeScopedNumberEnv(exchange: string, suffix: string, fallback: number) {
14
+ const envName = `${exchange.toUpperCase().replace(/-/g, '_')}_${suffix}`
15
+ const rawValue = process.env[envName]
16
+
17
+ if (rawValue === undefined) {
18
+ return fallback
19
+ }
20
+
21
+ const parsed = Number.parseInt(rawValue, 10)
22
+
23
+ return Number.isFinite(parsed) ? parsed : fallback
24
+ }
25
+
26
+ export function getRequestWeightLimit(exchange: string, exchangeInfo: any) {
27
+ const configuredLimit = getExchangeScopedNumberEnv(exchange, 'REQUEST_WEIGHT_LIMIT', 0)
28
+ if (configuredLimit > 0) {
29
+ return configuredLimit
30
+ }
31
+
32
+ const requestWeightLimit = exchangeInfo.rateLimits.find((d: any) => d.rateLimitType === 'REQUEST_WEIGHT')?.limit as number | undefined
33
+
34
+ if (!requestWeightLimit) {
35
+ throw new Error(`Failed to determine ${exchange} REQUEST_WEIGHT limit`)
36
+ }
37
+
38
+ return requestWeightLimit
39
+ }
40
+
41
+ export class RequestWeightLimiter {
42
+ constructor(
43
+ readonly limit: number,
44
+ readonly minAvailableWeightBuffer: number,
45
+ private _usedWeight = 0
46
+ ) {}
47
+
48
+ get usedWeight() {
49
+ return this._usedWeight
50
+ }
51
+
52
+ get availableWeight() {
53
+ return this.limit > 0 ? this.limit - this._usedWeight - this.minAvailableWeightBuffer : Infinity
54
+ }
55
+
56
+ async waitForAvailableWeight(requestWeight: number, onWait?: (delayMS: number) => void) {
57
+ const maxAvailableWeight = this.limit - this.minAvailableWeightBuffer
58
+ if (this.limit > 0 && requestWeight > maxAvailableWeight) {
59
+ throw new Error(`Request weight ${requestWeight} exceeds the available per-minute limit ${maxAvailableWeight}`)
60
+ }
61
+
62
+ if (this.availableWeight >= requestWeight) {
63
+ return false
64
+ }
65
+
66
+ const delayMS = getDelayToNextMinuteMS()
67
+ onWait?.(delayMS)
68
+ await wait(delayMS)
69
+
70
+ this._usedWeight = 0
71
+
72
+ return true
73
+ }
74
+
75
+ updateUsedWeight(usedWeight: number | undefined, fallbackIncrement: number) {
76
+ if (usedWeight !== undefined) {
77
+ this._usedWeight = usedWeight
78
+ return
79
+ }
80
+
81
+ if (this.limit > 0 && fallbackIncrement > 0) {
82
+ this._usedWeight += fallbackIncrement
83
+ }
84
+ }
85
+ }
86
+
87
+ function getDelayToNextMinuteMS() {
88
+ const now = new Date()
89
+
90
+ return Math.max((61 - now.getUTCSeconds()) * 1000 - now.getUTCMilliseconds(), 1)
91
+ }
@@ -2,7 +2,7 @@ import { createReadStream } from 'node:fs'
2
2
  import { rm } from 'node:fs/promises'
3
3
  import { EventEmitter, once } from 'events'
4
4
  import { Worker } from 'worker_threads'
5
- import { constants, createGunzip, createZstdDecompress } from 'zlib'
5
+ import { constants, createGunzip } from 'zlib'
6
6
  import { BinarySplitBatchStream } from './binarysplit.ts'
7
7
  import { clearCacheSync } from './clearcache.ts'
8
8
  import { EXCHANGES, EXCHANGE_CHANNELS_INFO } from './consts.ts'
@@ -12,6 +12,7 @@ import { Mapper, MapperFactory, normalizeBookChanges } from './mappers/index.ts'
12
12
  import { getOptions } from './options.ts'
13
13
  import { Disconnect, Exchange, FilterForExchange } from './types.ts'
14
14
  import { WorkerJobPayload, WorkerMessage, WorkerSignal } from './worker.ts'
15
+ import { createZstdDecompressionStream } from './zstd.ts'
15
16
 
16
17
  type MapperOutput<T> = T extends MapperFactory<any, infer U> ? U : never
17
18
  type ReplayNormalizedMessage<U extends readonly MapperFactory<any, any>[], Z extends boolean> = Z extends true
@@ -170,9 +171,10 @@ async function* replayLineBatches<T extends Exchange>({
170
171
  // response is a path to file on disk let' read it as stream
171
172
  const { slicePath: cachedSlicePath, sliceSize } = cachedSlice
172
173
  const isZstdSlice = cachedSlicePath.endsWith('.zst')
173
- const linesStream = createReadStream(cachedSlicePath, { highWaterMark: CHUNK_SIZE })
174
- // decompress it while preserving the on-disk cache in the negotiated wire format
175
- .pipe(isZstdSlice ? createZstdDecompress({ chunkSize: CHUNK_SIZE }) : createGunzip(GZIP_OPTIONS))
174
+ const decompressedStream = isZstdSlice
175
+ ? createZstdDecompressionStream(cachedSlicePath, CHUNK_SIZE)
176
+ : createReadStream(cachedSlicePath, { highWaterMark: CHUNK_SIZE }).pipe(createGunzip(GZIP_OPTIONS))
177
+ const linesStream = decompressedStream
176
178
  .on('error', function onDecompressionError(err) {
177
179
  debug('%s decompression error %o', isZstdSlice ? 'zstd' : 'gzip', err)
178
180
  linesStream.destroy(err)
@@ -8,6 +8,7 @@ import { Exchange, Filter } from './types.ts'
8
8
  const debug = createDebug('tardis-dev')
9
9
 
10
10
  const DEFAULT_DATA_FEED_SLICE_SIZE = 1
11
+ const DATA_FEED_DOWNLOAD_ATTEMPT_TIMEOUT_MS = 135_000
11
12
 
12
13
  if (isMainThread) {
13
14
  debug('current worker is not meant to run in main thread')
@@ -162,7 +163,8 @@ async function getDataFeedSlice(
162
163
  }
163
164
  }
164
165
 
165
- let url = `${endpoint}/data-feeds/${exchange}?from=${fromDate.toISOString()}&offset=${offset}&compression=${dataFeedCompression}`
166
+ const requestCompression = dataFeedCompression === 'zstd' ? 'zstd-multiframe' : dataFeedCompression
167
+ let url = `${endpoint}/data-feeds/${exchange}?from=${fromDate.toISOString()}&offset=${offset}&compression=${requestCompression}`
166
168
  if (requestedSliceSize > DEFAULT_DATA_FEED_SLICE_SIZE) {
167
169
  url += `&sliceSize=${requestedSliceSize}`
168
170
  }
@@ -177,7 +179,8 @@ async function getDataFeedSlice(
177
179
  url,
178
180
  userAgent,
179
181
  appendContentEncodingExtension: true,
180
- acceptEncoding: dataFeedCompression === 'gzip' ? 'gzip' : 'zstd, gzip'
182
+ acceptEncoding: dataFeedCompression === 'gzip' ? 'gzip' : 'zstd, gzip',
183
+ attemptTimeoutMS: DATA_FEED_DOWNLOAD_ATTEMPT_TIMEOUT_MS
181
184
  })
182
185
  const responseSliceSize = Number(downloadResult.headers['x-slice-size'])
183
186
  const suggestedSliceSize = Number(downloadResult.headers['x-suggested-slice-size'] ?? DEFAULT_DATA_FEED_SLICE_SIZE)