tardis-dev 13.27.1 → 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.
- package/dist/consts.d.ts +2 -1
- package/dist/consts.d.ts.map +1 -1
- package/dist/consts.js +12 -1
- package/dist/consts.js.map +1 -1
- package/dist/mappers/bybit.d.ts +2 -2
- package/dist/mappers/bybitspot.d.ts +1 -1
- package/dist/mappers/cryptocom.d.ts +1 -1
- package/dist/mappers/huobi.d.ts +3 -3
- package/dist/mappers/index.d.ts +4 -4
- package/dist/mappers/index.d.ts.map +1 -1
- package/dist/mappers/index.js +10 -2
- package/dist/mappers/index.js.map +1 -1
- package/dist/mappers/kucoinfutures.d.ts +211 -0
- package/dist/mappers/kucoinfutures.d.ts.map +1 -0
- package/dist/mappers/kucoinfutures.js +294 -0
- package/dist/mappers/kucoinfutures.js.map +1 -0
- package/dist/realtimefeeds/index.d.ts.map +1 -1
- package/dist/realtimefeeds/index.js +3 -1
- package/dist/realtimefeeds/index.js.map +1 -1
- package/dist/realtimefeeds/kucoinfutures.d.ts +26 -0
- package/dist/realtimefeeds/kucoinfutures.d.ts.map +1 -0
- package/dist/realtimefeeds/kucoinfutures.js +115 -0
- package/dist/realtimefeeds/kucoinfutures.js.map +1 -0
- package/package.json +1 -1
- package/src/consts.ts +13 -1
- package/src/mappers/index.ts +16 -2
- package/src/mappers/kucoinfutures.ts +483 -0
- package/src/realtimefeeds/index.ts +3 -1
- package/src/realtimefeeds/kucoinfutures.ts +135 -0
|
@@ -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
|
+
}
|