tardis-dev 13.27.2 → 13.28.1

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,488 @@
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
+
87
+ if (!message.data) {
88
+ return
89
+ }
90
+
91
+ if (!kucoinSnapshotData.asks) {
92
+ kucoinSnapshotData.asks = []
93
+ }
94
+ if (!kucoinSnapshotData.bids) {
95
+ kucoinSnapshotData.bids = []
96
+ }
97
+
98
+ // mark given symbol depth info that has snapshot processed
99
+ symbolDepthInfo.lastUpdateId = Number(kucoinSnapshotData.sequence)
100
+ symbolDepthInfo.snapshotProcessed = true
101
+
102
+ // if there were any depth updates buffered, let's process those by adding to or updating the initial snapshot
103
+ for (const update of symbolDepthInfo.bufferedUpdates.items()) {
104
+ const bookChange = this.mapBookDepthUpdate(update, localTimestamp)
105
+
106
+ if (bookChange !== undefined) {
107
+ const mappedChange = this.mapChange(update.data.change)
108
+ if (mappedChange.price == 0) {
109
+ continue
110
+ }
111
+
112
+ const matchingSide = mappedChange.isBid ? kucoinSnapshotData.bids : kucoinSnapshotData.asks
113
+ const matchingLevel = matchingSide.find((b) => b[0] === mappedChange.price)
114
+
115
+ if (matchingLevel !== undefined) {
116
+ // remove empty level from snapshot
117
+ if (mappedChange.amount === 0) {
118
+ const index = matchingSide.findIndex((b) => b[0] === mappedChange.price)
119
+ if (index > -1) {
120
+ matchingSide.splice(index, 1)
121
+ }
122
+ } else {
123
+ matchingLevel[1] = mappedChange.amount
124
+ }
125
+ } else if (mappedChange.amount != 0) {
126
+ matchingSide.push([mappedChange.price, mappedChange.amount])
127
+ }
128
+ }
129
+ }
130
+
131
+ // remove all buffered updates
132
+ symbolDepthInfo.bufferedUpdates.clear()
133
+
134
+ const bookChange: BookChange = {
135
+ type: 'book_change',
136
+ symbol,
137
+ exchange: 'kucoin-futures',
138
+ isSnapshot: true,
139
+ bids: kucoinSnapshotData.bids.map(this.mapBookLevel),
140
+ asks: kucoinSnapshotData.asks.map(this.mapBookLevel),
141
+ timestamp: localTimestamp,
142
+ localTimestamp
143
+ }
144
+
145
+ yield bookChange
146
+ } else if (snapshotAlreadyProcessed) {
147
+ // snapshot was already processed let's map the message as normal book_change
148
+ const bookChange = this.mapBookDepthUpdate(message, localTimestamp)
149
+ if (bookChange !== undefined) {
150
+ yield bookChange
151
+ }
152
+ } else {
153
+ symbolDepthInfo.bufferedUpdates.append(message)
154
+ }
155
+ }
156
+
157
+ protected mapBookDepthUpdate(l2UpdateMessage: KucoinFuturesLevel2UpdateMessage, localTimestamp: Date): BookChange | undefined {
158
+ // we can safely assume here that depthContext and lastUpdateId aren't null here as this is method only works
159
+ // when we've already processed the snapshot
160
+ const symbol = l2UpdateMessage.topic.split(':')[1]
161
+ const depthContext = this.symbolToDepthInfoMapping[symbol]!
162
+ const lastUpdateId = depthContext.lastUpdateId!
163
+
164
+ // Drop any event where sequence is <= lastUpdateId in the snapshot
165
+ if (l2UpdateMessage.data.sequence <= lastUpdateId) {
166
+ return
167
+ }
168
+
169
+ // The first processed event should have sequence>lastUpdateId
170
+ if (!depthContext.validatedFirstUpdate) {
171
+ // if there is new instrument added it can have empty book at first and that's normal
172
+ const bookSnapshotIsEmpty = lastUpdateId == -1 || lastUpdateId == 0
173
+
174
+ if (l2UpdateMessage.data.sequence === lastUpdateId + 1 || bookSnapshotIsEmpty) {
175
+ depthContext.validatedFirstUpdate = true
176
+ } else {
177
+ const message = `Book depth snapshot has no overlap with first update, update ${JSON.stringify(
178
+ l2UpdateMessage
179
+ )}, lastUpdateId: ${lastUpdateId}, exchange kucoin-futures`
180
+ if (this.ignoreBookSnapshotOverlapError) {
181
+ depthContext.validatedFirstUpdate = true
182
+ debug(message)
183
+ } else {
184
+ throw new Error(message)
185
+ }
186
+ }
187
+ }
188
+
189
+ const change = this.mapChange(l2UpdateMessage.data.change)
190
+
191
+ return {
192
+ type: 'book_change',
193
+ symbol: symbol,
194
+ exchange: 'kucoin-futures',
195
+ isSnapshot: false,
196
+ bids: change.isBid
197
+ ? [
198
+ {
199
+ price: change.price,
200
+ amount: change.amount
201
+ }
202
+ ]
203
+ : [],
204
+ asks:
205
+ change.isBid === false
206
+ ? [
207
+ {
208
+ price: change.price,
209
+ amount: change.amount
210
+ }
211
+ ]
212
+ : [],
213
+ timestamp: new Date(l2UpdateMessage.data.timestamp),
214
+ localTimestamp: localTimestamp
215
+ }
216
+ }
217
+
218
+ private mapBookLevel(level: [number, number]) {
219
+ return { price: level[0], amount: level[1] }
220
+ }
221
+
222
+ private mapChange(change: string) {
223
+ const parts = change.split(',')
224
+ const isBid = parts[1] === 'buy'
225
+ const price = Number(parts[0])
226
+ const amount = Number(parts[2])
227
+
228
+ return { isBid, price, amount }
229
+ }
230
+ }
231
+
232
+ export class KucoinFuturesBookTickerMapper implements Mapper<'kucoin-futures', BookTicker> {
233
+ canHandle(message: KucoinFuturesTickerMessage) {
234
+ return message.type === 'message' && message.topic.startsWith('/contractMarket/tickerV2')
235
+ }
236
+
237
+ getFilters(symbols?: string[]) {
238
+ symbols = upperCaseSymbols(symbols)
239
+ return [
240
+ {
241
+ channel: 'contractMarket/tickerV2',
242
+ symbols
243
+ } as const
244
+ ]
245
+ }
246
+
247
+ *map(message: KucoinFuturesTickerMessage, localTimestamp: Date) {
248
+ const symbol = message.topic.split(':')[1]
249
+
250
+ const bookTicker: BookTicker = {
251
+ type: 'book_ticker',
252
+ symbol,
253
+ exchange: 'kucoin-futures',
254
+ askAmount: message.data.bestAskSize !== undefined && message.data.bestAskSize !== null ? message.data.bestAskSize : undefined,
255
+ askPrice:
256
+ message.data.bestAskPrice !== undefined && message.data.bestAskPrice !== null ? Number(message.data.bestAskPrice) : undefined,
257
+
258
+ bidPrice:
259
+ message.data.bestBidPrice !== undefined && message.data.bestBidPrice !== null ? Number(message.data.bestBidPrice) : undefined,
260
+
261
+ bidAmount: message.data.bestBidSize !== undefined && message.data.bestBidSize !== null ? message.data.bestBidSize : undefined,
262
+ timestamp: new Date(message.data.ts / 1000000),
263
+ localTimestamp: localTimestamp
264
+ }
265
+
266
+ yield bookTicker
267
+ }
268
+ }
269
+
270
+ export class KucoinFuturesDerivativeTickerMapper implements Mapper<'kucoin-futures', DerivativeTicker> {
271
+ private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
272
+ private readonly _lastPrices = new Map<string, number>()
273
+ private readonly _openInterests = new Map<string, number>()
274
+
275
+ canHandle(message: KucoinFuturesTickerMessage) {
276
+ return (
277
+ message.type === 'message' &&
278
+ (message.topic.startsWith('/contract/instrument') ||
279
+ message.topic.startsWith('/contractMarket/execution') ||
280
+ message.topic.startsWith('/contract/details'))
281
+ )
282
+ }
283
+
284
+ getFilters(symbols?: string[]) {
285
+ symbols = upperCaseSymbols(symbols)
286
+ return [
287
+ {
288
+ channel: 'contract/instrument',
289
+ symbols
290
+ } as const,
291
+ {
292
+ channel: 'contractMarket/execution',
293
+ symbols
294
+ } as const,
295
+ {
296
+ channel: 'contract/details',
297
+ symbols
298
+ } as const
299
+ ]
300
+ }
301
+
302
+ *map(message: KucoinFuturesInstrumentMessage | KucoinFuturesTradeMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
303
+ const symbol = message.topic.split(':')[1]
304
+
305
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'kucoin-futures')
306
+
307
+ if (message.subject === 'match') {
308
+ this._lastPrices.set(symbol, Number(message.data.price))
309
+ return
310
+ }
311
+
312
+ if (message.subject === 'contractDetails') {
313
+ this._openInterests.set(symbol, Number(message.data.openInterest))
314
+ return
315
+ }
316
+ const lastPrice = this._lastPrices.get(symbol)
317
+ const openInterest = this._openInterests.get(symbol)
318
+
319
+ if (message.subject === 'mark.index.price') {
320
+ pendingTickerInfo.updateIndexPrice(message.data.indexPrice)
321
+ pendingTickerInfo.updateMarkPrice(message.data.markPrice)
322
+ }
323
+
324
+ if (message.subject === 'funding.rate') {
325
+ pendingTickerInfo.updateTimestamp(new Date(message.data.timestamp))
326
+ pendingTickerInfo.updateFundingRate(message.data.fundingRate)
327
+ }
328
+
329
+ if (lastPrice !== undefined) {
330
+ pendingTickerInfo.updateLastPrice(lastPrice)
331
+ }
332
+
333
+ if (openInterest !== undefined) {
334
+ pendingTickerInfo.updateOpenInterest(openInterest)
335
+ }
336
+
337
+ if (pendingTickerInfo.hasChanged()) {
338
+ yield pendingTickerInfo.getSnapshot(localTimestamp)
339
+ }
340
+ }
341
+ }
342
+
343
+ type KucoinFuturesTradeMessage = {
344
+ topic: '/contractMarket/execution:COMPUSDTM'
345
+ type: 'message'
346
+ subject: 'match'
347
+ sn: 1694749771273
348
+ data: {
349
+ symbol: 'COMPUSDTM'
350
+ sequence: 1694749771273
351
+ makerUserId: '64b1a612d570b900017b7281'
352
+ side: 'buy' | 'sell'
353
+ size: 102
354
+ price: '57.75'
355
+ takerOrderId: '137974138051522560'
356
+ takerUserId: '61945720862a310001d6581e'
357
+ makerOrderId: '137974082376310784'
358
+ tradeId: '1694749771273'
359
+ ts: 1705708799996000000
360
+ }
361
+ }
362
+
363
+ type LocalDepthInfo = {
364
+ bufferedUpdates: CircularBuffer<KucoinFuturesLevel2UpdateMessage>
365
+ snapshotProcessed?: boolean
366
+ lastUpdateId?: number
367
+ validatedFirstUpdate?: boolean
368
+ }
369
+
370
+ type KucoinFuturesLevel2SnapshotMessage = {
371
+ type: 'message'
372
+ generated: true
373
+ topic: '/contractMarket/level2Snapshot:C98USDTM'
374
+ subject: 'level2Snapshot'
375
+ code: '200000'
376
+ data: {
377
+ sequence: 1694868048360
378
+ symbol: 'C98USDTM'
379
+ bids: [number, number][]
380
+ asks: [number, number][]
381
+ ts: 1705881597161000000
382
+ }
383
+ }
384
+
385
+ type KucoinFuturesLevel2UpdateMessage = {
386
+ topic: '/contractMarket/level2:C98USDTM'
387
+ type: 'message'
388
+ subject: 'level2'
389
+ sn: 1694868048361
390
+ data: { sequence: 1694868048361; change: '0.2353,buy,146'; timestamp: 1705881600096 }
391
+ }
392
+
393
+ type KucoinFuturesTickerMessage = {
394
+ topic: '/contractMarket/tickerV2:BCHUSDTM'
395
+ type: 'message'
396
+ subject: 'tickerV2'
397
+ sn: 1695158749093
398
+ data: {
399
+ symbol: 'BCHUSDTM'
400
+ sequence: 1695158749093
401
+ bestBidSize: 480
402
+ bestBidPrice: '236.76'
403
+ bestAskPrice: '236.77'
404
+ bestAskSize: 126
405
+ ts: 1705708800078000000
406
+ }
407
+ }
408
+
409
+ type KucoinFuturesInstrumentMessage =
410
+ | {
411
+ topic: '/contract/instrument:ENSUSDTM'
412
+ type: 'message'
413
+ subject: 'funding.rate'
414
+ data: { granularity: 60000; fundingRate: 0.000053; timestamp: 1705708800000 }
415
+ }
416
+ | {
417
+ topic: '/contract/instrument:XAIUSDTM'
418
+ type: 'message'
419
+ subject: 'mark.index.price'
420
+ data: { markPrice: 0.80694; indexPrice: 0.80695; granularity: 1000; timestamp: 1705881600000 }
421
+ }
422
+ | {
423
+ topic: '/contract/instrument:BAKEUSDTM'
424
+ type: 'message'
425
+ subject: 'funding.rate'
426
+ data: { granularity: 28800000; fundingRate: 0.000105; timestamp: 1705982400000 }
427
+ }
428
+ | {
429
+ topic: '/contract/details:XBTUSDTM'
430
+ type: 'message'
431
+ subject: 'contractDetails'
432
+ generated: true
433
+ data: {
434
+ symbol: 'XBTUSDTM'
435
+ rootSymbol: 'USDT'
436
+ type: 'FFWCSX'
437
+ firstOpenDate: 1585555200000
438
+ baseCurrency: 'XBT'
439
+ quoteCurrency: 'USDT'
440
+ settleCurrency: 'USDT'
441
+ maxOrderQty: 1000000
442
+ maxPrice: 1000000.0
443
+ lotSize: 1
444
+ tickSize: 0.1
445
+ indexPriceTickSize: 0.01
446
+ multiplier: 0.001
447
+ initialMargin: 0.008
448
+ maintainMargin: 0.004
449
+ maxRiskLimit: 25000
450
+ minRiskLimit: 25000
451
+ riskStep: 12500
452
+ makerFeeRate: 2.0e-4
453
+ takerFeeRate: 6.0e-4
454
+ takerFixFee: 0.0
455
+ makerFixFee: 0.0
456
+ isDeleverage: true
457
+ isQuanto: true
458
+ isInverse: false
459
+ markMethod: 'FairPrice'
460
+ fairMethod: 'FundingRate'
461
+ fundingBaseSymbol: '.XBTINT8H'
462
+ fundingQuoteSymbol: '.USDTINT8H'
463
+ fundingRateSymbol: '.XBTUSDTMFPI8H'
464
+ indexSymbol: '.KXBTUSDT'
465
+ settlementSymbol: ''
466
+ status: 'Open'
467
+ fundingFeeRate: 3.8e-5
468
+ predictedFundingFeeRate: 9.6e-5
469
+ fundingRateGranularity: 28800000
470
+ openInterest: '9295921'
471
+ turnoverOf24h: 5.94135187191124e8
472
+ volumeOf24h: 15131.243
473
+ markPrice: 39995.94
474
+ indexPrice: 39999.2
475
+ lastTradePrice: 39996.6
476
+ nextFundingRateTime: 10561278
477
+ maxLeverage: 125
478
+ sourceExchanges: ['okex', 'binance', 'kucoin', 'bybit', 'bitget', 'bitmart', 'gateio']
479
+ premiumsSymbol1M: '.XBTUSDTMPI'
480
+ premiumsSymbol8H: '.XBTUSDTMPI8H'
481
+ fundingBaseSymbol1M: '.XBTINT'
482
+ fundingQuoteSymbol1M: '.USDTINT'
483
+ lowPrice: 38560.0
484
+ highPrice: 40253.0
485
+ priceChgPct: 0.0132
486
+ priceChg: 523.4
487
+ }
488
+ }
@@ -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
+ }