tardis-machine 18.1.0 → 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 (93) 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/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 +294 -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 +2 -2
  37. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  38. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  39. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  40. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  41. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  42. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  43. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  44. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +3 -3
  45. package/node_modules/tardis-dev/dist/mappers/index.d.ts +179 -161
  46. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  47. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  48. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  49. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  50. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  51. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +5 -5
  52. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  53. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  54. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  55. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  56. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  57. package/node_modules/tardis-dev/dist/mappers/registry.d.ts.map +1 -1
  58. package/node_modules/tardis-dev/dist/mappers/registry.js.map +1 -1
  59. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  60. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  61. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts +53 -0
  62. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts.map +1 -0
  63. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js +271 -0
  64. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js.map +1 -0
  65. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts +1 -4
  66. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
  67. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +36 -95
  68. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
  69. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  70. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +3 -0
  71. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  72. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  73. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +12 -3
  74. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  75. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts +14 -0
  76. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts.map +1 -0
  77. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js +72 -0
  78. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js.map +1 -0
  79. package/node_modules/tardis-dev/package.json +2 -2
  80. package/node_modules/tardis-dev/src/consts.ts +18 -0
  81. package/node_modules/tardis-dev/src/handy.ts +6 -1
  82. package/node_modules/tardis-dev/src/mappers/aster.ts +438 -0
  83. package/node_modules/tardis-dev/src/mappers/binance.ts +23 -21
  84. package/node_modules/tardis-dev/src/mappers/bitfinex.ts +1 -2
  85. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  86. package/node_modules/tardis-dev/src/mappers/registry.ts +1 -2
  87. package/node_modules/tardis-dev/src/realtimefeeds/aster.ts +388 -0
  88. package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +59 -145
  89. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +3 -0
  90. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +12 -3
  91. package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
  92. package/package.json +3 -3
  93. package/src/ws/subscriptionsmappers.ts +3 -1
@@ -0,0 +1,388 @@
1
+ import { Writable } from 'stream'
2
+ import { batch, getJSON, wait } from '../handy.ts'
3
+ import { Filter } from '../types.ts'
4
+ import { MultiConnectionRealTimeFeedBase, PoolingClientBase, RealTimeFeedBase } from './realtimefeed.ts'
5
+ import { getExchangeScopedNumberEnv, getRequestWeightLimit, parseRequestWeightHeader, RequestWeightLimiter } from './requestweight.ts'
6
+
7
+ const asterHttpOptions = {
8
+ timeout: 10 * 1000,
9
+ retry: {
10
+ limit: 10,
11
+ statusCodes: [429, 500, 403],
12
+ maxRetryAfter: 120 * 1000
13
+ }
14
+ }
15
+
16
+ const DEFAULT_OPEN_INTEREST_MIN_AVAILABLE_WEIGHT_BUFFER = 100
17
+ const DEFAULT_OPEN_INTEREST_POLLING_INTERVAL_MS = 30 * 1000
18
+ const OPEN_INTEREST_BATCH_SIZE = 10
19
+ const OPEN_INTEREST_REQUEST_WEIGHT = 1
20
+ const OPEN_INTEREST_POLLING_RECOVERY_MS = 1000
21
+ const OPEN_INTEREST_MAX_POLLING_INTERVAL_MS = 60 * 1000
22
+
23
+ export class AsterRealTimeFeed extends RealTimeFeedBase {
24
+ protected static readonly depthChannel = 'depth'
25
+ protected static readonly depthSnapshotChannel = 'depthSnapshot'
26
+ protected readonly depthStream: string = 'depth@0ms'
27
+ protected readonly depthRequestRequestWeight = 20
28
+ protected readonly wssURL: string = 'wss://sstream.asterdex.com/stream'
29
+ protected readonly httpURL: string = 'https://sapi.asterdex.com/api/v3'
30
+ protected readonly channels = new Set([
31
+ 'trade',
32
+ 'aggTrade',
33
+ 'ticker',
34
+ AsterRealTimeFeed.depthChannel,
35
+ AsterRealTimeFeed.depthSnapshotChannel,
36
+ 'bookTicker'
37
+ ])
38
+ protected readonly channelMappings: { [key: string]: string | undefined } = {
39
+ [AsterRealTimeFeed.depthChannel]: this.depthStream
40
+ }
41
+
42
+ protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
43
+ const filtersWithSymbols = filters.map<Required<Filter<string>>>((filter) => {
44
+ if (!this.channels.has(filter.channel)) {
45
+ throw new Error(`AsterRealTimeFeed unsupported channel ${filter.channel}`)
46
+ }
47
+
48
+ if (!filter.symbols || filter.symbols.length === 0) {
49
+ throw new Error('AsterRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
50
+ }
51
+
52
+ return filter as Required<Filter<string>>
53
+ })
54
+
55
+ const depthSnapshotFilters = filtersWithSymbols.filter((filter) => filter.channel === AsterRealTimeFeed.depthSnapshotChannel)
56
+ this.validateDepthSnapshotFilters(filtersWithSymbols, depthSnapshotFilters)
57
+
58
+ return filtersWithSymbols
59
+ .filter((f) => f.channel !== AsterRealTimeFeed.depthSnapshotChannel)
60
+ .map((filter, index) => {
61
+ return {
62
+ method: 'SUBSCRIBE',
63
+ params: filter.symbols.map((symbol) => `${symbol.toLowerCase()}@${this.channelMappings[filter.channel] ?? filter.channel}`),
64
+ id: index + 1
65
+ }
66
+ })
67
+ }
68
+
69
+ protected messageIsError(message: any): boolean {
70
+ if (message.result === null) {
71
+ return false
72
+ }
73
+
74
+ if (message.stream !== undefined) {
75
+ return false
76
+ }
77
+
78
+ if (message.code !== undefined || message.error !== undefined) {
79
+ return true
80
+ }
81
+
82
+ return false
83
+ }
84
+
85
+ protected async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) {
86
+ const depthSnapshotFilter = filters.find((f) => f.channel === AsterRealTimeFeed.depthSnapshotChannel)
87
+ if (!depthSnapshotFilter) {
88
+ return
89
+ }
90
+
91
+ const exchangeInfoResponse = await getJSON<any>(`${this.httpURL}/exchangeInfo`, asterHttpOptions)
92
+ if (shouldCancel()) {
93
+ return
94
+ }
95
+ const exchangeInfo = exchangeInfoResponse.data
96
+
97
+ const DELAY_ENV = `${this._exchange.toUpperCase().replace(/-/g, '_')}_SNAPSHOTS_DELAY_MS`
98
+ const currentWeightLimit = getRequestWeightLimit(this._exchange, exchangeInfo)
99
+
100
+ const usedWeight = parseRequestWeightHeader(exchangeInfoResponse.headers['x-mbx-used-weight-1m']) ?? 0
101
+
102
+ this.debug('current x-mbx-used-weight-1m limit: %s, already used weight: %s', currentWeightLimit, usedWeight)
103
+
104
+ const concurrencyLimit = Math.max(getExchangeScopedNumberEnv(this._exchange, 'CONCURRENCY_LIMIT', 4), 1)
105
+
106
+ this.debug('current snapshots requests concurrency limit: %s', concurrencyLimit)
107
+
108
+ const minWeightBuffer = Math.max(
109
+ getExchangeScopedNumberEnv(this._exchange, 'MIN_AVAILABLE_WEIGHT_BUFFER', 2 * concurrencyLimit * this.depthRequestRequestWeight),
110
+ 0
111
+ )
112
+ const requestWeightLimiter = new RequestWeightLimiter(currentWeightLimit, minWeightBuffer, usedWeight)
113
+
114
+ for (const symbolsBatch of batch(depthSnapshotFilter.symbols!, concurrencyLimit)) {
115
+ if (shouldCancel()) {
116
+ return
117
+ }
118
+
119
+ this.debug('requesting manual snapshots for: %s', symbolsBatch)
120
+
121
+ await requestWeightLimiter.waitForAvailableWeight(symbolsBatch.length * this.depthRequestRequestWeight, (delayMS) => {
122
+ this.debug(
123
+ 'reached rate limit (x-mbx-used-weight-1m limit: %s, used weight: %s, minimum available weight buffer: %s), waiting: %s seconds',
124
+ currentWeightLimit,
125
+ requestWeightLimiter.usedWeight,
126
+ minWeightBuffer,
127
+ Math.ceil(delayMS / 1000)
128
+ )
129
+ })
130
+
131
+ const usedWeights = await Promise.all(
132
+ symbolsBatch.map(async (symbol) => {
133
+ if (shouldCancel()) {
134
+ return 0
135
+ }
136
+
137
+ const depthSnapshotResponse = await getJSON<any>(
138
+ `${this.httpURL}/depth?symbol=${symbol.toUpperCase()}&limit=1000`,
139
+ asterHttpOptions
140
+ )
141
+ if (shouldCancel()) {
142
+ return 0
143
+ }
144
+
145
+ const snapshot = {
146
+ stream: `${symbol.toLowerCase()}@${AsterRealTimeFeed.depthSnapshotChannel}`,
147
+ generated: true,
148
+ data: depthSnapshotResponse.data
149
+ }
150
+
151
+ this.manualSnapshotsBuffer.push(snapshot)
152
+
153
+ if (process.env[DELAY_ENV] !== undefined) {
154
+ const msToWait = Number.parseInt(process.env[DELAY_ENV] as string)
155
+
156
+ await wait(msToWait)
157
+ }
158
+
159
+ return parseRequestWeightHeader(depthSnapshotResponse.headers['x-mbx-used-weight-1m'])
160
+ })
161
+ )
162
+
163
+ const maxUsedWeight = Math.max(...usedWeights.map((weight) => weight ?? 0))
164
+ requestWeightLimiter.updateUsedWeight(maxUsedWeight || undefined, symbolsBatch.length * this.depthRequestRequestWeight)
165
+
166
+ this.debug('requested manual snapshots successfully for: %s, used weight: %s', symbolsBatch, requestWeightLimiter.usedWeight)
167
+ }
168
+
169
+ this.debug('requested all manual snapshots successfully')
170
+ }
171
+
172
+ private validateDepthSnapshotFilters(filters: Required<Filter<string>>[], depthSnapshotFilters: Required<Filter<string>>[]) {
173
+ if (depthSnapshotFilters.length === 0) {
174
+ return
175
+ }
176
+
177
+ const depthSymbols = new Set(
178
+ filters
179
+ .filter((filter) => filter.channel === AsterRealTimeFeed.depthChannel)
180
+ .flatMap((filter) => filter.symbols.map((symbol) => symbol.toUpperCase()))
181
+ )
182
+
183
+ for (const filter of depthSnapshotFilters) {
184
+ for (const symbol of filter.symbols) {
185
+ if (depthSymbols.has(symbol.toUpperCase()) === false) {
186
+ throw new Error(
187
+ `AsterRealTimeFeed requires ${AsterRealTimeFeed.depthChannel} for every ${AsterRealTimeFeed.depthSnapshotChannel} symbol`
188
+ )
189
+ }
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ export class AsterFuturesRealTimeFeed extends MultiConnectionRealTimeFeedBase {
196
+ protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
197
+ const webSocketFilters = filters.filter((filter) => filter.channel !== 'openInterest')
198
+ if (webSocketFilters.length > 0) {
199
+ yield new AsterFuturesWebSocketRealTimeFeed(exchange, webSocketFilters, timeoutIntervalMS, onError)
200
+ }
201
+
202
+ const openInterestFilters = filters.filter((filter) => filter.channel === 'openInterest')
203
+ if (openInterestFilters.length > 0) {
204
+ for (const filter of openInterestFilters) {
205
+ if (!filter.symbols || filter.symbols.length === 0) {
206
+ throw new Error('AsterFuturesRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
207
+ }
208
+ }
209
+
210
+ yield new AsterFuturesOpenInterestClient(
211
+ exchange,
212
+ 'https://fapi.asterdex.com/fapi/v3',
213
+ openInterestFilters.flatMap((filter) => filter.symbols!),
214
+ onError
215
+ )
216
+ }
217
+ }
218
+ }
219
+
220
+ export class AsterFuturesWebSocketRealTimeFeed extends AsterRealTimeFeed {
221
+ protected readonly wssURL: string = 'wss://fstream.asterdex.com/stream'
222
+ protected readonly httpURL: string = 'https://fapi.asterdex.com/fapi/v3'
223
+ protected readonly channels = new Set([
224
+ 'trade',
225
+ 'aggTrade',
226
+ 'ticker',
227
+ AsterRealTimeFeed.depthChannel,
228
+ AsterRealTimeFeed.depthSnapshotChannel,
229
+ 'markPrice',
230
+ 'forceOrder',
231
+ 'bookTicker',
232
+ 'assetIndex'
233
+ ])
234
+ protected readonly channelMappings: { [key: string]: string | undefined } = {
235
+ [AsterRealTimeFeed.depthChannel]: this.depthStream,
236
+ markPrice: 'markPrice@1s'
237
+ }
238
+ }
239
+
240
+ class AsterFuturesOpenInterestClient extends PoolingClientBase {
241
+ private readonly _minPollingIntervalMS: number
242
+ private readonly _minAvailableWeightBuffer: number
243
+ private readonly _maxPollingIntervalMS: number
244
+ private _currentPollingIntervalMS: number
245
+ private _requestWeightLimiter: RequestWeightLimiter | undefined
246
+
247
+ constructor(
248
+ private readonly _exchange: string,
249
+ private readonly _httpURL: string,
250
+ private readonly _instruments: string[],
251
+ onError?: (error: Error) => void
252
+ ) {
253
+ const minPollingIntervalMS = Math.max(
254
+ getExchangeScopedNumberEnv(_exchange, 'OPEN_INTEREST_POLLING_INTERVAL_MS', DEFAULT_OPEN_INTEREST_POLLING_INTERVAL_MS),
255
+ 1000
256
+ )
257
+
258
+ super(_exchange, minPollingIntervalMS / 1000, onError)
259
+
260
+ this._minPollingIntervalMS = minPollingIntervalMS
261
+ this._maxPollingIntervalMS = Math.max(this._minPollingIntervalMS, OPEN_INTEREST_MAX_POLLING_INTERVAL_MS)
262
+ this._currentPollingIntervalMS = minPollingIntervalMS
263
+ this._minAvailableWeightBuffer = Math.max(
264
+ getExchangeScopedNumberEnv(_exchange, 'MIN_AVAILABLE_WEIGHT_BUFFER', DEFAULT_OPEN_INTEREST_MIN_AVAILABLE_WEIGHT_BUFFER),
265
+ 0
266
+ )
267
+
268
+ const configuredRequestWeightLimit = getExchangeScopedNumberEnv(_exchange, 'REQUEST_WEIGHT_LIMIT', 0)
269
+ if (configuredRequestWeightLimit > 0) {
270
+ this._requestWeightLimiter = new RequestWeightLimiter(configuredRequestWeightLimit, this._minAvailableWeightBuffer)
271
+ }
272
+ }
273
+
274
+ protected getPoolingDelayMS() {
275
+ return this._currentPollingIntervalMS
276
+ }
277
+
278
+ protected async poolDataToStream(outputStream: Writable) {
279
+ let waitedForRateLimit = false
280
+
281
+ if (this._requestWeightLimiter === undefined) {
282
+ await this._initializeRateLimitInfo()
283
+ }
284
+
285
+ for (let index = 0; index < this._instruments.length;) {
286
+ if (outputStream.destroyed) {
287
+ return
288
+ }
289
+
290
+ if (await this._waitForAvailableWeight()) {
291
+ waitedForRateLimit = true
292
+ }
293
+ if (outputStream.destroyed) {
294
+ return
295
+ }
296
+
297
+ const batchSize = Math.min(OPEN_INTEREST_BATCH_SIZE, Math.floor(this._requestWeightLimiter!.availableWeight))
298
+ const instrumentsBatch = this._instruments.slice(index, index + batchSize)
299
+ index += instrumentsBatch.length
300
+
301
+ const results = await Promise.allSettled(
302
+ instrumentsBatch.map(async (instrument) => {
303
+ const openInterestResponse = await getJSON<AsterFuturesOpenInterestData>(
304
+ `${this._httpURL}/openInterest?symbol=${instrument.toUpperCase()}`,
305
+ asterHttpOptions
306
+ )
307
+
308
+ return {
309
+ instrument,
310
+ usedWeight: parseRequestWeightHeader(openInterestResponse.headers['x-mbx-used-weight-1m']),
311
+ data: openInterestResponse.data
312
+ }
313
+ })
314
+ )
315
+
316
+ let maxUsedWeight: number | undefined
317
+
318
+ for (const result of results) {
319
+ if (result.status === 'rejected') {
320
+ this._notifyError(result.reason)
321
+ continue
322
+ }
323
+
324
+ if (result.value.usedWeight !== undefined) {
325
+ maxUsedWeight = Math.max(maxUsedWeight ?? 0, result.value.usedWeight)
326
+ }
327
+
328
+ if (outputStream.writable) {
329
+ outputStream.write({
330
+ stream: `${result.value.instrument.toLowerCase()}@openInterest`,
331
+ generated: true,
332
+ data: result.value.data
333
+ })
334
+ }
335
+ }
336
+
337
+ this._requestWeightLimiter!.updateUsedWeight(maxUsedWeight, instrumentsBatch.length * OPEN_INTEREST_REQUEST_WEIGHT)
338
+ }
339
+
340
+ if (waitedForRateLimit) {
341
+ this._currentPollingIntervalMS = Math.min(this._currentPollingIntervalMS + this._minPollingIntervalMS, this._maxPollingIntervalMS)
342
+ } else {
343
+ this._currentPollingIntervalMS = Math.max(
344
+ this._minPollingIntervalMS,
345
+ this._currentPollingIntervalMS - OPEN_INTEREST_POLLING_RECOVERY_MS
346
+ )
347
+ }
348
+ }
349
+
350
+ private async _waitForAvailableWeight() {
351
+ return this._requestWeightLimiter!.waitForAvailableWeight(OPEN_INTEREST_REQUEST_WEIGHT, (delayMS) => {
352
+ this.debug(
353
+ 'open interest reached rate limit (limit: %s, used: %s, minimum available buffer: %s), waiting %s ms',
354
+ this._requestWeightLimiter!.limit,
355
+ this._requestWeightLimiter!.usedWeight,
356
+ this._requestWeightLimiter!.minAvailableWeightBuffer,
357
+ delayMS
358
+ )
359
+ })
360
+ }
361
+
362
+ private async _initializeRateLimitInfo() {
363
+ const exchangeInfoResponse = await getJSON<any>(`${this._httpURL}/exchangeInfo`, asterHttpOptions)
364
+ const exchangeInfo = exchangeInfoResponse.data
365
+
366
+ this._requestWeightLimiter = new RequestWeightLimiter(
367
+ getRequestWeightLimit(this._exchange, exchangeInfo),
368
+ this._minAvailableWeightBuffer,
369
+ parseRequestWeightHeader(exchangeInfoResponse.headers['x-mbx-used-weight-1m'])
370
+ )
371
+ }
372
+
373
+ private _notifyError(error: unknown) {
374
+ const normalizedError = error instanceof Error ? error : new Error(String(error))
375
+
376
+ this.debug('open interest request error %o', normalizedError)
377
+
378
+ if (this.onError !== undefined) {
379
+ this.onError(normalizedError)
380
+ }
381
+ }
382
+ }
383
+
384
+ export type AsterFuturesOpenInterestData = {
385
+ symbol: string
386
+ openInterest: string
387
+ time: number
388
+ }