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.
- 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/handy.d.ts +13 -0
- package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/handy.js +5 -1
- package/node_modules/tardis-dev/dist/handy.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/aster.d.ts +16 -0
- package/node_modules/tardis-dev/dist/mappers/aster.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/mappers/aster.js +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 +3 -3
- package/node_modules/tardis-dev/dist/mappers/index.d.ts +179 -161
- 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/package.json +2 -2
- package/node_modules/tardis-dev/src/consts.ts +18 -0
- package/node_modules/tardis-dev/src/handy.ts +6 -1
- 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/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/package.json +3 -3
- 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
|
+
}
|