tardis-dev 13.27.2 → 13.28.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.
@@ -0,0 +1,483 @@
1
+ import { debug } from '../debug'
2
+ import { CircularBuffer, upperCaseSymbols } from '../handy'
3
+ import { BookChange, BookTicker, DerivativeTicker, Trade } from '../types'
4
+ import { Mapper, PendingTickerInfoHelper } from './mapper'
5
+
6
+ export class KucoinFuturesTradesMapper implements Mapper<'kucoin-futures', Trade> {
7
+ canHandle(message: KucoinFuturesTradeMessage) {
8
+ return message.type === 'message' && message.topic.startsWith('/contractMarket/execution')
9
+ }
10
+
11
+ getFilters(symbols?: string[]) {
12
+ symbols = upperCaseSymbols(symbols)
13
+
14
+ return [
15
+ {
16
+ channel: 'contractMarket/execution',
17
+ symbols
18
+ } as const
19
+ ]
20
+ }
21
+
22
+ *map(message: KucoinFuturesTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
23
+ const kucoinTrade = message.data
24
+
25
+ const timestamp = new Date(kucoinTrade.ts / 1000000)
26
+
27
+ yield {
28
+ type: 'trade',
29
+ symbol: kucoinTrade.symbol,
30
+ exchange: 'kucoin-futures',
31
+ id: kucoinTrade.tradeId,
32
+ price: Number(kucoinTrade.price),
33
+ amount: Number(kucoinTrade.size),
34
+ side: kucoinTrade.side === 'sell' ? 'sell' : 'buy',
35
+ timestamp,
36
+ localTimestamp
37
+ }
38
+ }
39
+ }
40
+
41
+ export class KucoinFuturesBookChangeMapper implements Mapper<'kucoin-futures', BookChange> {
42
+ protected readonly symbolToDepthInfoMapping: {
43
+ [key: string]: LocalDepthInfo
44
+ } = {}
45
+
46
+ constructor(private readonly ignoreBookSnapshotOverlapError: boolean) {}
47
+
48
+ canHandle(message: KucoinFuturesLevel2SnapshotMessage | KucoinFuturesLevel2UpdateMessage) {
49
+ return message.type === 'message' && message.topic.startsWith('/contractMarket/level2')
50
+ }
51
+
52
+ getFilters(symbols?: string[]) {
53
+ symbols = upperCaseSymbols(symbols)
54
+ return [
55
+ {
56
+ channel: 'contractMarket/level2',
57
+ symbols
58
+ } as const,
59
+ {
60
+ channel: 'contractMarket/level2Snapshot',
61
+ symbols
62
+ } as const
63
+ ]
64
+ }
65
+
66
+ *map(message: KucoinFuturesLevel2SnapshotMessage | KucoinFuturesLevel2UpdateMessage, localTimestamp: Date) {
67
+ const symbol = message.topic.split(':')[1]
68
+
69
+ if (this.symbolToDepthInfoMapping[symbol] === undefined) {
70
+ this.symbolToDepthInfoMapping[symbol] = {
71
+ bufferedUpdates: new CircularBuffer<KucoinFuturesLevel2UpdateMessage>(2000)
72
+ }
73
+ }
74
+
75
+ const symbolDepthInfo = this.symbolToDepthInfoMapping[symbol]
76
+ const snapshotAlreadyProcessed = symbolDepthInfo.snapshotProcessed
77
+
78
+ // first check if received message is snapshot and process it as such if it is
79
+ if (message.subject === 'level2Snapshot') {
80
+ // if we've already received 'manual' snapshot, ignore if there is another one
81
+ if (snapshotAlreadyProcessed) {
82
+ return
83
+ }
84
+ // produce snapshot book_change
85
+ const kucoinSnapshotData = message.data
86
+ if (!kucoinSnapshotData.asks) {
87
+ kucoinSnapshotData.asks = []
88
+ }
89
+ if (!kucoinSnapshotData.bids) {
90
+ kucoinSnapshotData.bids = []
91
+ }
92
+
93
+ // mark given symbol depth info that has snapshot processed
94
+ symbolDepthInfo.lastUpdateId = Number(kucoinSnapshotData.sequence)
95
+ symbolDepthInfo.snapshotProcessed = true
96
+
97
+ // if there were any depth updates buffered, let's process those by adding to or updating the initial snapshot
98
+ for (const update of symbolDepthInfo.bufferedUpdates.items()) {
99
+ const bookChange = this.mapBookDepthUpdate(update, localTimestamp)
100
+
101
+ if (bookChange !== undefined) {
102
+ const mappedChange = this.mapChange(update.data.change)
103
+ if (mappedChange.price == 0) {
104
+ continue
105
+ }
106
+
107
+ const matchingSide = mappedChange.isBid ? kucoinSnapshotData.bids : kucoinSnapshotData.asks
108
+ const matchingLevel = matchingSide.find((b) => b[0] === mappedChange.price)
109
+
110
+ if (matchingLevel !== undefined) {
111
+ // remove empty level from snapshot
112
+ if (mappedChange.amount === 0) {
113
+ const index = matchingSide.findIndex((b) => b[0] === mappedChange.price)
114
+ if (index > -1) {
115
+ matchingSide.splice(index, 1)
116
+ }
117
+ } else {
118
+ matchingLevel[1] = mappedChange.amount
119
+ }
120
+ } else if (mappedChange.amount != 0) {
121
+ matchingSide.push([mappedChange.price, mappedChange.amount])
122
+ }
123
+ }
124
+ }
125
+
126
+ // remove all buffered updates
127
+ symbolDepthInfo.bufferedUpdates.clear()
128
+
129
+ const bookChange: BookChange = {
130
+ type: 'book_change',
131
+ symbol,
132
+ exchange: 'kucoin-futures',
133
+ isSnapshot: true,
134
+ bids: kucoinSnapshotData.bids.map(this.mapBookLevel),
135
+ asks: kucoinSnapshotData.asks.map(this.mapBookLevel),
136
+ timestamp: localTimestamp,
137
+ localTimestamp
138
+ }
139
+
140
+ yield bookChange
141
+ } else if (snapshotAlreadyProcessed) {
142
+ // snapshot was already processed let's map the message as normal book_change
143
+ const bookChange = this.mapBookDepthUpdate(message, localTimestamp)
144
+ if (bookChange !== undefined) {
145
+ yield bookChange
146
+ }
147
+ } else {
148
+ symbolDepthInfo.bufferedUpdates.append(message)
149
+ }
150
+ }
151
+
152
+ protected mapBookDepthUpdate(l2UpdateMessage: KucoinFuturesLevel2UpdateMessage, localTimestamp: Date): BookChange | undefined {
153
+ // we can safely assume here that depthContext and lastUpdateId aren't null here as this is method only works
154
+ // when we've already processed the snapshot
155
+ const symbol = l2UpdateMessage.topic.split(':')[1]
156
+ const depthContext = this.symbolToDepthInfoMapping[symbol]!
157
+ const lastUpdateId = depthContext.lastUpdateId!
158
+
159
+ // Drop any event where sequence is <= lastUpdateId in the snapshot
160
+ if (l2UpdateMessage.data.sequence <= lastUpdateId) {
161
+ return
162
+ }
163
+
164
+ // The first processed event should have sequence>lastUpdateId
165
+ if (!depthContext.validatedFirstUpdate) {
166
+ // if there is new instrument added it can have empty book at first and that's normal
167
+ const bookSnapshotIsEmpty = lastUpdateId == -1 || lastUpdateId == 0
168
+
169
+ if (l2UpdateMessage.data.sequence === lastUpdateId + 1 || bookSnapshotIsEmpty) {
170
+ depthContext.validatedFirstUpdate = true
171
+ } else {
172
+ const message = `Book depth snapshot has no overlap with first update, update ${JSON.stringify(
173
+ l2UpdateMessage
174
+ )}, lastUpdateId: ${lastUpdateId}, exchange kucoin-futures`
175
+ if (this.ignoreBookSnapshotOverlapError) {
176
+ depthContext.validatedFirstUpdate = true
177
+ debug(message)
178
+ } else {
179
+ throw new Error(message)
180
+ }
181
+ }
182
+ }
183
+
184
+ const change = this.mapChange(l2UpdateMessage.data.change)
185
+
186
+ return {
187
+ type: 'book_change',
188
+ symbol: symbol,
189
+ exchange: 'kucoin-futures',
190
+ isSnapshot: false,
191
+ bids: change.isBid
192
+ ? [
193
+ {
194
+ price: change.price,
195
+ amount: change.amount
196
+ }
197
+ ]
198
+ : [],
199
+ asks:
200
+ change.isBid === false
201
+ ? [
202
+ {
203
+ price: change.price,
204
+ amount: change.amount
205
+ }
206
+ ]
207
+ : [],
208
+ timestamp: new Date(l2UpdateMessage.data.timestamp),
209
+ localTimestamp: localTimestamp
210
+ }
211
+ }
212
+
213
+ private mapBookLevel(level: [number, number]) {
214
+ return { price: level[0], amount: level[1] }
215
+ }
216
+
217
+ private mapChange(change: string) {
218
+ const parts = change.split(',')
219
+ const isBid = parts[1] === 'buy'
220
+ const price = Number(parts[0])
221
+ const amount = Number(parts[2])
222
+
223
+ return { isBid, price, amount }
224
+ }
225
+ }
226
+
227
+ export class KucoinFuturesBookTickerMapper implements Mapper<'kucoin-futures', BookTicker> {
228
+ canHandle(message: KucoinFuturesTickerMessage) {
229
+ return message.type === 'message' && message.topic.startsWith('/contractMarket/tickerV2')
230
+ }
231
+
232
+ getFilters(symbols?: string[]) {
233
+ symbols = upperCaseSymbols(symbols)
234
+ return [
235
+ {
236
+ channel: 'contractMarket/tickerV2',
237
+ symbols
238
+ } as const
239
+ ]
240
+ }
241
+
242
+ *map(message: KucoinFuturesTickerMessage, localTimestamp: Date) {
243
+ const symbol = message.topic.split(':')[1]
244
+
245
+ const bookTicker: BookTicker = {
246
+ type: 'book_ticker',
247
+ symbol,
248
+ exchange: 'kucoin-futures',
249
+ askAmount: message.data.bestAskSize !== undefined && message.data.bestAskSize !== null ? message.data.bestAskSize : undefined,
250
+ askPrice:
251
+ message.data.bestAskPrice !== undefined && message.data.bestAskPrice !== null ? Number(message.data.bestAskPrice) : undefined,
252
+
253
+ bidPrice:
254
+ message.data.bestBidPrice !== undefined && message.data.bestBidPrice !== null ? Number(message.data.bestBidPrice) : undefined,
255
+
256
+ bidAmount: message.data.bestBidSize !== undefined && message.data.bestBidSize !== null ? message.data.bestBidSize : undefined,
257
+ timestamp: new Date(message.data.ts / 1000000),
258
+ localTimestamp: localTimestamp
259
+ }
260
+
261
+ yield bookTicker
262
+ }
263
+ }
264
+
265
+ export class KucoinFuturesDerivativeTickerMapper implements Mapper<'kucoin-futures', DerivativeTicker> {
266
+ private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
267
+ private readonly _lastPrices = new Map<string, number>()
268
+ private readonly _openInterests = new Map<string, number>()
269
+
270
+ canHandle(message: KucoinFuturesTickerMessage) {
271
+ return (
272
+ message.type === 'message' &&
273
+ (message.topic.startsWith('/contract/instrument') ||
274
+ message.topic.startsWith('/contractMarket/execution') ||
275
+ message.topic.startsWith('/contract/details'))
276
+ )
277
+ }
278
+
279
+ getFilters(symbols?: string[]) {
280
+ symbols = upperCaseSymbols(symbols)
281
+ return [
282
+ {
283
+ channel: 'contract/instrument',
284
+ symbols
285
+ } as const,
286
+ {
287
+ channel: 'contractMarket/execution',
288
+ symbols
289
+ } as const,
290
+ {
291
+ channel: 'contract/details',
292
+ symbols
293
+ } as const
294
+ ]
295
+ }
296
+
297
+ *map(message: KucoinFuturesInstrumentMessage | KucoinFuturesTradeMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
298
+ const symbol = message.topic.split(':')[1]
299
+
300
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'kucoin-futures')
301
+
302
+ if (message.subject === 'match') {
303
+ this._lastPrices.set(symbol, Number(message.data.price))
304
+ return
305
+ }
306
+
307
+ if (message.subject === 'contractDetails') {
308
+ this._openInterests.set(symbol, Number(message.data.openInterest))
309
+ return
310
+ }
311
+ const lastPrice = this._lastPrices.get(symbol)
312
+ const openInterest = this._openInterests.get(symbol)
313
+
314
+ if (message.subject === 'mark.index.price') {
315
+ pendingTickerInfo.updateIndexPrice(message.data.indexPrice)
316
+ pendingTickerInfo.updateMarkPrice(message.data.markPrice)
317
+ }
318
+
319
+ if (message.subject === 'funding.rate') {
320
+ pendingTickerInfo.updateTimestamp(new Date(message.data.timestamp))
321
+ pendingTickerInfo.updateFundingRate(message.data.fundingRate)
322
+ }
323
+
324
+ if (lastPrice !== undefined) {
325
+ pendingTickerInfo.updateLastPrice(lastPrice)
326
+ }
327
+
328
+ if (openInterest !== undefined) {
329
+ pendingTickerInfo.updateOpenInterest(openInterest)
330
+ }
331
+
332
+ if (pendingTickerInfo.hasChanged()) {
333
+ yield pendingTickerInfo.getSnapshot(localTimestamp)
334
+ }
335
+ }
336
+ }
337
+
338
+ type KucoinFuturesTradeMessage = {
339
+ topic: '/contractMarket/execution:COMPUSDTM'
340
+ type: 'message'
341
+ subject: 'match'
342
+ sn: 1694749771273
343
+ data: {
344
+ symbol: 'COMPUSDTM'
345
+ sequence: 1694749771273
346
+ makerUserId: '64b1a612d570b900017b7281'
347
+ side: 'buy' | 'sell'
348
+ size: 102
349
+ price: '57.75'
350
+ takerOrderId: '137974138051522560'
351
+ takerUserId: '61945720862a310001d6581e'
352
+ makerOrderId: '137974082376310784'
353
+ tradeId: '1694749771273'
354
+ ts: 1705708799996000000
355
+ }
356
+ }
357
+
358
+ type LocalDepthInfo = {
359
+ bufferedUpdates: CircularBuffer<KucoinFuturesLevel2UpdateMessage>
360
+ snapshotProcessed?: boolean
361
+ lastUpdateId?: number
362
+ validatedFirstUpdate?: boolean
363
+ }
364
+
365
+ type KucoinFuturesLevel2SnapshotMessage = {
366
+ type: 'message'
367
+ generated: true
368
+ topic: '/contractMarket/level2Snapshot:C98USDTM'
369
+ subject: 'level2Snapshot'
370
+ code: '200000'
371
+ data: {
372
+ sequence: 1694868048360
373
+ symbol: 'C98USDTM'
374
+ bids: [number, number][]
375
+ asks: [number, number][]
376
+ ts: 1705881597161000000
377
+ }
378
+ }
379
+
380
+ type KucoinFuturesLevel2UpdateMessage = {
381
+ topic: '/contractMarket/level2:C98USDTM'
382
+ type: 'message'
383
+ subject: 'level2'
384
+ sn: 1694868048361
385
+ data: { sequence: 1694868048361; change: '0.2353,buy,146'; timestamp: 1705881600096 }
386
+ }
387
+
388
+ type KucoinFuturesTickerMessage = {
389
+ topic: '/contractMarket/tickerV2:BCHUSDTM'
390
+ type: 'message'
391
+ subject: 'tickerV2'
392
+ sn: 1695158749093
393
+ data: {
394
+ symbol: 'BCHUSDTM'
395
+ sequence: 1695158749093
396
+ bestBidSize: 480
397
+ bestBidPrice: '236.76'
398
+ bestAskPrice: '236.77'
399
+ bestAskSize: 126
400
+ ts: 1705708800078000000
401
+ }
402
+ }
403
+
404
+ type KucoinFuturesInstrumentMessage =
405
+ | {
406
+ topic: '/contract/instrument:ENSUSDTM'
407
+ type: 'message'
408
+ subject: 'funding.rate'
409
+ data: { granularity: 60000; fundingRate: 0.000053; timestamp: 1705708800000 }
410
+ }
411
+ | {
412
+ topic: '/contract/instrument:XAIUSDTM'
413
+ type: 'message'
414
+ subject: 'mark.index.price'
415
+ data: { markPrice: 0.80694; indexPrice: 0.80695; granularity: 1000; timestamp: 1705881600000 }
416
+ }
417
+ | {
418
+ topic: '/contract/instrument:BAKEUSDTM'
419
+ type: 'message'
420
+ subject: 'funding.rate'
421
+ data: { granularity: 28800000; fundingRate: 0.000105; timestamp: 1705982400000 }
422
+ }
423
+ | {
424
+ topic: '/contract/details:XBTUSDTM'
425
+ type: 'message'
426
+ subject: 'contractDetails'
427
+ generated: true
428
+ data: {
429
+ symbol: 'XBTUSDTM'
430
+ rootSymbol: 'USDT'
431
+ type: 'FFWCSX'
432
+ firstOpenDate: 1585555200000
433
+ baseCurrency: 'XBT'
434
+ quoteCurrency: 'USDT'
435
+ settleCurrency: 'USDT'
436
+ maxOrderQty: 1000000
437
+ maxPrice: 1000000.0
438
+ lotSize: 1
439
+ tickSize: 0.1
440
+ indexPriceTickSize: 0.01
441
+ multiplier: 0.001
442
+ initialMargin: 0.008
443
+ maintainMargin: 0.004
444
+ maxRiskLimit: 25000
445
+ minRiskLimit: 25000
446
+ riskStep: 12500
447
+ makerFeeRate: 2.0e-4
448
+ takerFeeRate: 6.0e-4
449
+ takerFixFee: 0.0
450
+ makerFixFee: 0.0
451
+ isDeleverage: true
452
+ isQuanto: true
453
+ isInverse: false
454
+ markMethod: 'FairPrice'
455
+ fairMethod: 'FundingRate'
456
+ fundingBaseSymbol: '.XBTINT8H'
457
+ fundingQuoteSymbol: '.USDTINT8H'
458
+ fundingRateSymbol: '.XBTUSDTMFPI8H'
459
+ indexSymbol: '.KXBTUSDT'
460
+ settlementSymbol: ''
461
+ status: 'Open'
462
+ fundingFeeRate: 3.8e-5
463
+ predictedFundingFeeRate: 9.6e-5
464
+ fundingRateGranularity: 28800000
465
+ openInterest: '9295921'
466
+ turnoverOf24h: 5.94135187191124e8
467
+ volumeOf24h: 15131.243
468
+ markPrice: 39995.94
469
+ indexPrice: 39999.2
470
+ lastTradePrice: 39996.6
471
+ nextFundingRateTime: 10561278
472
+ maxLeverage: 125
473
+ sourceExchanges: ['okex', 'binance', 'kucoin', 'bybit', 'bitget', 'bitmart', 'gateio']
474
+ premiumsSymbol1M: '.XBTUSDTMPI'
475
+ premiumsSymbol8H: '.XBTUSDTMPI8H'
476
+ fundingBaseSymbol1M: '.XBTINT'
477
+ fundingQuoteSymbol1M: '.USDTINT'
478
+ lowPrice: 38560.0
479
+ highPrice: 40253.0
480
+ priceChgPct: 0.0132
481
+ priceChg: 523.4
482
+ }
483
+ }
@@ -48,6 +48,7 @@ import { WooxRealTimeFeed } from './woox'
48
48
  import { BlockchainComRealTimeFeed } from './blockchaincom'
49
49
  import { BinanceEuropeanOptionsRealTimeFeed } from './binanceeuropeanoptions'
50
50
  import { OkexSpreadsRealTimeFeed } from './okexspreads'
51
+ import { KucoinFuturesRealTimeFeed } from './kucoinfutures'
51
52
 
52
53
  export * from './realtimefeed'
53
54
 
@@ -106,7 +107,8 @@ const realTimeFeedsMap: {
106
107
  'woo-x': WooxRealTimeFeed,
107
108
  'blockchain-com': BlockchainComRealTimeFeed,
108
109
  'binance-european-options': BinanceEuropeanOptionsRealTimeFeed,
109
- 'okex-spreads': OkexSpreadsRealTimeFeed
110
+ 'okex-spreads': OkexSpreadsRealTimeFeed,
111
+ 'kucoin-futures': KucoinFuturesRealTimeFeed
110
112
  }
111
113
 
112
114
  export function getRealTimeFeedFactory(exchange: Exchange): RealTimeFeed {
@@ -0,0 +1,135 @@
1
+ import { Writable } from 'stream'
2
+ import { httpClient, getRandomString, wait } from '../handy'
3
+ import { Filter } from '../types'
4
+ import { MultiConnectionRealTimeFeedBase, PoolingClientBase, RealTimeFeedBase } from './realtimefeed'
5
+
6
+ const kucoinHttpOptions = {
7
+ timeout: 10 * 1000,
8
+ retry: {
9
+ limit: 10,
10
+ statusCodes: [418, 429, 500, 403],
11
+ maxRetryAfter: 120 * 1000
12
+ }
13
+ }
14
+
15
+ export class KucoinFuturesRealTimeFeed extends MultiConnectionRealTimeFeedBase {
16
+ private _httpURL = 'https://api-futures.kucoin.com/api'
17
+
18
+ protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
19
+ const wsFilters = filters.filter((f) => f.channel !== 'contract/details')
20
+
21
+ if (wsFilters.length > 0) {
22
+ yield new KucoinFuturesSingleConnectionRealTimeFeed(exchange, wsFilters, this._httpURL, timeoutIntervalMS, onError)
23
+ }
24
+
25
+ const contractDetailsFilters = filters.filter((f) => f.channel === 'contract/details')
26
+ if (contractDetailsFilters.length > 0) {
27
+ yield new KucoinFuturesContractDetailsClient(exchange, this._httpURL)
28
+ }
29
+ }
30
+ }
31
+
32
+ export class KucoinFuturesSingleConnectionRealTimeFeed extends RealTimeFeedBase {
33
+ constructor(
34
+ exchange: string,
35
+ filters: Filter<string>[],
36
+ private readonly _httpURL: string,
37
+ timeoutIntervalMS: number | undefined,
38
+ onError?: (error: Error) => void
39
+ ) {
40
+ super(exchange, filters, timeoutIntervalMS, onError)
41
+ }
42
+ protected wssURL = ''
43
+
44
+ protected async getWebSocketUrl() {
45
+ const response = (await httpClient.post(`${this._httpURL}/v1/bullet-public`, { retry: 3, timeout: 10000 }).json()) as any
46
+
47
+ return `${response.data.instanceServers[0].endpoint}?token=${response.data.token}&connectId=${getRandomString()}`
48
+ }
49
+
50
+ protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
51
+ return filters
52
+ .filter((f) => f.channel !== 'contractMarket/level2Snapshot')
53
+ .map((filter) => {
54
+ if (!filter.symbols || filter.symbols.length === 0) {
55
+ throw new Error('KucoinFuturesRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
56
+ }
57
+
58
+ return {
59
+ id: getRandomString(),
60
+ type: 'subscribe',
61
+ topic: `/${filter.channel}:${filter.symbols.join(',')}`,
62
+ response: true
63
+ }
64
+ })
65
+ }
66
+
67
+ protected async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) {
68
+ const depthSnapshotFilter = filters.find((f) => f.channel === 'contractMarket/level2Snapshot')
69
+ if (!depthSnapshotFilter) {
70
+ return
71
+ }
72
+
73
+ this.debug('requesting manual snapshots for: %s', depthSnapshotFilter.symbols)
74
+ for (let symbol of depthSnapshotFilter.symbols!) {
75
+ if (shouldCancel()) {
76
+ return
77
+ }
78
+
79
+ const depthSnapshotResponse = (await httpClient
80
+ .get(`${this._httpURL}/v1/level2/snapshot?symbol=${symbol}`, kucoinHttpOptions)
81
+ .json()) as any
82
+
83
+ const snapshot = {
84
+ type: 'message',
85
+ generated: true,
86
+ topic: `/contractMarket/level2Snapshot:${symbol}`,
87
+ subject: 'level2Snapshot',
88
+ ...depthSnapshotResponse
89
+ }
90
+
91
+ this.manualSnapshotsBuffer.push(snapshot)
92
+ }
93
+
94
+ this.debug('requested manual snapshots successfully for: %s ', depthSnapshotFilter.symbols)
95
+ }
96
+
97
+ protected messageIsError(message: any): boolean {
98
+ return message.type === 'error'
99
+ }
100
+
101
+ protected sendCustomPing = () => {
102
+ this.send({
103
+ id: new Date().valueOf().toString(),
104
+ type: 'ping'
105
+ })
106
+ }
107
+
108
+ protected messageIsHeartbeat(msg: any) {
109
+ return msg.type === 'pong'
110
+ }
111
+ }
112
+
113
+ class KucoinFuturesContractDetailsClient extends PoolingClientBase {
114
+ constructor(exchange: string, private readonly _httpURL: string) {
115
+ super(exchange, 6)
116
+ }
117
+
118
+ protected async poolDataToStream(outputStream: Writable) {
119
+ const openInterestResponse = (await httpClient.get(`${this._httpURL}/v1/contracts/active`, kucoinHttpOptions).json()) as any
120
+
121
+ for (const instrument of openInterestResponse.data) {
122
+ const openInterestMessage = {
123
+ topic: `/contract/details:${instrument.symbol}`,
124
+ type: 'message',
125
+ subject: 'contractDetails',
126
+ generated: true,
127
+ data: instrument
128
+ }
129
+
130
+ if (outputStream.writable) {
131
+ outputStream.write(openInterestMessage)
132
+ }
133
+ }
134
+ }
135
+ }