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.
- 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 +3 -1
- package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/consts.js +17 -0
- package/node_modules/tardis-dev/dist/consts.js.map +1 -1
- package/node_modules/tardis-dev/dist/downloaddatasets.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/downloaddatasets.js +3 -1
- package/node_modules/tardis-dev/dist/downloaddatasets.js.map +1 -1
- package/node_modules/tardis-dev/dist/handy.d.ts +15 -1
- package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/handy.js +10 -5
- 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 +294 -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 +2 -2
- 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 +4 -4
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.js +15 -4
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.js.map +1 -1
- 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/realtimefeed.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +12 -3
- 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/dist/replay.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/replay.js +6 -4
- package/node_modules/tardis-dev/dist/replay.js.map +1 -1
- package/node_modules/tardis-dev/dist/worker.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/worker.js +5 -2
- package/node_modules/tardis-dev/dist/worker.js.map +1 -1
- package/node_modules/tardis-dev/dist/zstd.d.ts +3 -0
- package/node_modules/tardis-dev/dist/zstd.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/zstd.js +122 -0
- package/node_modules/tardis-dev/dist/zstd.js.map +1 -0
- package/node_modules/tardis-dev/package.json +2 -2
- package/node_modules/tardis-dev/src/consts.ts +18 -0
- package/node_modules/tardis-dev/src/downloaddatasets.ts +3 -1
- package/node_modules/tardis-dev/src/handy.ts +13 -5
- package/node_modules/tardis-dev/src/mappers/aster.ts +438 -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/hyperliquid.ts +19 -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/realtimefeed.ts +12 -3
- package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
- package/node_modules/tardis-dev/src/replay.ts +6 -4
- package/node_modules/tardis-dev/src/worker.ts +5 -2
- package/node_modules/tardis-dev/src/zstd.ts +139 -0
- package/package.json +4 -4
- 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: [
|
|
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
|
|
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.
|
|
154
|
-
|
|
155
|
-
|
|
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 (
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if (
|
|
181
|
-
|
|
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:
|
|
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
|
-
|
|
212
|
-
|
|
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.
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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.
|
|
266
|
-
|
|
267
|
-
|
|
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 =
|
|
292
|
+
const currentWeightLimit = getRequestWeightLimit(this._exchange, exchangeInfo)
|
|
365
293
|
|
|
366
|
-
|
|
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
|
-
|
|
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 =
|
|
381
|
-
this._exchange,
|
|
382
|
-
|
|
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
|
|
353
|
+
return parseRequestWeightHeader(depthSnapshotResponse.headers['x-mbx-used-weight-1m'])
|
|
441
354
|
})
|
|
442
355
|
)
|
|
443
356
|
|
|
444
|
-
|
|
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
|
|
224
|
-
|
|
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
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
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)
|