tardis-dev 13.5.0 → 13.6.2
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 +5 -2
- package/dist/consts.js.map +1 -1
- package/dist/handy.d.ts +1 -0
- package/dist/handy.d.ts.map +1 -1
- package/dist/handy.js +5 -1
- package/dist/handy.js.map +1 -1
- package/dist/mappers/bybit.d.ts +1 -1
- package/dist/mappers/bybitspot.d.ts +1 -1
- package/dist/mappers/cryptocom.d.ts +1 -1
- package/dist/mappers/deribit.d.ts.map +1 -1
- package/dist/mappers/deribit.js +2 -1
- package/dist/mappers/deribit.js.map +1 -1
- package/dist/mappers/huobi.d.ts +3 -3
- package/dist/mappers/index.d.ts +3 -3
- package/dist/mappers/index.d.ts.map +1 -1
- package/dist/mappers/index.js +7 -3
- package/dist/mappers/index.js.map +1 -1
- package/dist/mappers/kucoin.d.ts +110 -0
- package/dist/mappers/kucoin.d.ts.map +1 -0
- package/dist/mappers/kucoin.js +224 -0
- package/dist/mappers/kucoin.js.map +1 -0
- package/dist/realtimefeeds/binance.js +1 -1
- package/dist/realtimefeeds/binance.js.map +1 -1
- package/dist/realtimefeeds/ftx.js +1 -1
- package/dist/realtimefeeds/ftx.js.map +1 -1
- package/dist/realtimefeeds/huobi.js +2 -2
- package/dist/realtimefeeds/huobi.js.map +1 -1
- 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/kucoin.d.ts +13 -0
- package/dist/realtimefeeds/kucoin.d.ts.map +1 -0
- package/dist/realtimefeeds/kucoin.js +70 -0
- package/dist/realtimefeeds/kucoin.js.map +1 -0
- package/dist/realtimefeeds/realtimefeed.d.ts +1 -0
- package/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
- package/dist/realtimefeeds/realtimefeed.js +6 -2
- package/dist/realtimefeeds/realtimefeed.js.map +1 -1
- package/package.json +1 -1
- package/src/consts.ts +6 -2
- package/src/handy.ts +4 -0
- package/src/mappers/deribit.ts +5 -1
- package/src/mappers/index.ts +7 -3
- package/src/mappers/kucoin.ts +315 -0
- package/src/realtimefeeds/binance.ts +1 -1
- package/src/realtimefeeds/ftx.ts +1 -1
- package/src/realtimefeeds/huobi.ts +2 -2
- package/src/realtimefeeds/index.ts +3 -1
- package/src/realtimefeeds/kucoin.ts +77 -0
- package/src/realtimefeeds/realtimefeed.ts +8 -3
package/src/consts.ts
CHANGED
|
@@ -44,7 +44,8 @@ export const EXCHANGES = [
|
|
|
44
44
|
'huobi-dm-options',
|
|
45
45
|
'star-atlas',
|
|
46
46
|
'crypto-com',
|
|
47
|
-
'crypto-com-derivatives'
|
|
47
|
+
'crypto-com-derivatives',
|
|
48
|
+
'kucoin'
|
|
48
49
|
] as const
|
|
49
50
|
|
|
50
51
|
const BINANCE_CHANNELS = ['trade', 'aggTrade', 'ticker', 'depth', 'depthSnapshot', 'bookTicker', 'recentTrades', 'borrowInterest'] as const
|
|
@@ -380,6 +381,8 @@ const CRYPTO_COM_CHANNELS = ['trade', 'book', 'ticker']
|
|
|
380
381
|
|
|
381
382
|
const CRYPTO_COM_DERIVATIVES = ['trade', 'book', 'ticker', 'settlement', 'index', 'mark', 'funding']
|
|
382
383
|
|
|
384
|
+
const KUCOIN_CHANNELS = ['market/ticker', 'market/snapshot', 'market/level2', 'market/match', 'market/level2Snapshot']
|
|
385
|
+
|
|
383
386
|
export const EXCHANGE_CHANNELS_INFO = {
|
|
384
387
|
bitmex: BITMEX_CHANNELS,
|
|
385
388
|
coinbase: COINBASE_CHANNELS,
|
|
@@ -426,5 +429,6 @@ export const EXCHANGE_CHANNELS_INFO = {
|
|
|
426
429
|
'huobi-dm-options': HUOBI_DM_OPTIONS_CHANNELS,
|
|
427
430
|
mango: MANGO_CHANNELS,
|
|
428
431
|
'crypto-com': CRYPTO_COM_CHANNELS,
|
|
429
|
-
'crypto-com-derivatives': CRYPTO_COM_DERIVATIVES
|
|
432
|
+
'crypto-com-derivatives': CRYPTO_COM_DERIVATIVES,
|
|
433
|
+
kucoin: KUCOIN_CHANNELS
|
|
430
434
|
}
|
package/src/handy.ts
CHANGED
|
@@ -23,6 +23,10 @@ export function wait(delayMS: number) {
|
|
|
23
23
|
})
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function getRandomString() {
|
|
27
|
+
return crypto.randomBytes(24).toString('hex')
|
|
28
|
+
}
|
|
29
|
+
|
|
26
30
|
export function formatDateToPath(date: Date) {
|
|
27
31
|
const year = date.getUTCFullYear()
|
|
28
32
|
const month = doubleDigit(date.getUTCMonth() + 1)
|
package/src/mappers/deribit.ts
CHANGED
|
@@ -151,7 +151,11 @@ export class DeribitOptionSummaryMapper implements Mapper<'deribit', OptionSumma
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
// options ticker has greeks
|
|
154
|
-
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
channel.startsWith('ticker') &&
|
|
157
|
+
(message.params.data.instrument_name.endsWith('-P') || message.params.data.instrument_name.endsWith('-C'))
|
|
158
|
+
)
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
*map(message: DeribitOptionTickerMessage, localTimestamp: Date) {
|
package/src/mappers/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ import {
|
|
|
69
69
|
HuobiTradesMapper
|
|
70
70
|
} from './huobi'
|
|
71
71
|
import { krakenBookChangeMapper, krakenBookTickerMapper, krakenTradesMapper } from './kraken'
|
|
72
|
+
import { KucoinBookChangeMapper, KucoinBookTickerMapper, KucoinTradesMapper } from './kucoin'
|
|
72
73
|
import { Mapper } from './mapper'
|
|
73
74
|
import {
|
|
74
75
|
OkexBookChangeMapper,
|
|
@@ -171,7 +172,8 @@ const tradesMappers = {
|
|
|
171
172
|
mango: () => new SerumTradesMapper('mango'),
|
|
172
173
|
'bybit-spot': () => new BybitSpotTradesMapper('bybit-spot'),
|
|
173
174
|
'crypto-com': () => new CryptoComTradesMapper('crypto-com'),
|
|
174
|
-
'crypto-com-derivatives': () => new CryptoComTradesMapper('crypto-com-derivatives')
|
|
175
|
+
'crypto-com-derivatives': () => new CryptoComTradesMapper('crypto-com-derivatives'),
|
|
176
|
+
kucoin: () => new KucoinTradesMapper('kucoin')
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
const bookChangeMappers = {
|
|
@@ -241,7 +243,8 @@ const bookChangeMappers = {
|
|
|
241
243
|
'star-atlas': () => new SerumBookChangeMapper('star-atlas'),
|
|
242
244
|
mango: () => new SerumBookChangeMapper('mango'),
|
|
243
245
|
'crypto-com': () => new CryptoComBookChangeMapper('crypto-com'),
|
|
244
|
-
'crypto-com-derivatives': () => new CryptoComBookChangeMapper('crypto-com-derivatives')
|
|
246
|
+
'crypto-com-derivatives': () => new CryptoComBookChangeMapper('crypto-com-derivatives'),
|
|
247
|
+
kucoin: (localTimestamp: Date) => new KucoinBookChangeMapper('kucoin', isRealTime(localTimestamp) === false)
|
|
245
248
|
}
|
|
246
249
|
|
|
247
250
|
const derivativeTickersMappers = {
|
|
@@ -349,7 +352,8 @@ const bookTickersMappers = {
|
|
|
349
352
|
'gate-io-futures': () => new GateIOFuturesBookTickerMapper('gate-io-futures'),
|
|
350
353
|
'bybit-spot': () => new BybitSpotBookTickerMapper('bybit-spot'),
|
|
351
354
|
'crypto-com': () => new CryptoComBookTickerMapper('crypto-com'),
|
|
352
|
-
'crypto-com-derivatives': () => new CryptoComBookTickerMapper('crypto-com-derivatives')
|
|
355
|
+
'crypto-com-derivatives': () => new CryptoComBookTickerMapper('crypto-com-derivatives'),
|
|
356
|
+
kucoin: () => new KucoinBookTickerMapper('kucoin')
|
|
353
357
|
}
|
|
354
358
|
|
|
355
359
|
export const normalizeTrades = <T extends keyof typeof tradesMappers>(exchange: T, localTimestamp: Date): Mapper<T, Trade> => {
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import { debug } from '../debug'
|
|
2
|
+
import { CircularBuffer, upperCaseSymbols } from '../handy'
|
|
3
|
+
import { BookChange, Exchange, BookTicker, Trade, BookPriceLevel } from '../types'
|
|
4
|
+
import { Mapper } from './mapper'
|
|
5
|
+
|
|
6
|
+
export class KucoinTradesMapper implements Mapper<'kucoin', Trade> {
|
|
7
|
+
constructor(private readonly _exchange: Exchange) {}
|
|
8
|
+
canHandle(message: KucoinTradeMessage) {
|
|
9
|
+
return message.type === 'message' && message.topic.startsWith('/market/match')
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
getFilters(symbols?: string[]) {
|
|
13
|
+
symbols = upperCaseSymbols(symbols)
|
|
14
|
+
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
channel: 'market/match',
|
|
18
|
+
symbols
|
|
19
|
+
} as const
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
*map(message: KucoinTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
|
|
24
|
+
const kucoinTrade = message.data
|
|
25
|
+
|
|
26
|
+
const timestamp = new Date(Number(kucoinTrade.time.slice(0, 13)))
|
|
27
|
+
|
|
28
|
+
timestamp.μs = Number(kucoinTrade.time.slice(13, 16))
|
|
29
|
+
|
|
30
|
+
yield {
|
|
31
|
+
type: 'trade',
|
|
32
|
+
symbol: kucoinTrade.symbol,
|
|
33
|
+
exchange: this._exchange,
|
|
34
|
+
id: kucoinTrade.tradeId,
|
|
35
|
+
price: Number(kucoinTrade.price),
|
|
36
|
+
amount: Number(kucoinTrade.size),
|
|
37
|
+
side: kucoinTrade.side === 'sell' ? 'sell' : 'buy',
|
|
38
|
+
timestamp,
|
|
39
|
+
localTimestamp
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class KucoinBookChangeMapper implements Mapper<'kucoin', BookChange> {
|
|
45
|
+
protected readonly symbolToDepthInfoMapping: {
|
|
46
|
+
[key: string]: LocalDepthInfo
|
|
47
|
+
} = {}
|
|
48
|
+
|
|
49
|
+
constructor(protected readonly _exchange: Exchange, private readonly ignoreBookSnapshotOverlapError: boolean) {}
|
|
50
|
+
|
|
51
|
+
canHandle(message: KucoinLevel2SnapshotMessage | KucoinLevel2UpdateMessage) {
|
|
52
|
+
return message.type === 'message' && message.topic.startsWith('/market/level2')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getFilters(symbols?: string[]) {
|
|
56
|
+
symbols = upperCaseSymbols(symbols)
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
channel: 'market/level2',
|
|
60
|
+
symbols
|
|
61
|
+
} as const,
|
|
62
|
+
{
|
|
63
|
+
channel: 'market/level2Snapshot',
|
|
64
|
+
symbols
|
|
65
|
+
} as const
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
*map(message: KucoinLevel2SnapshotMessage | KucoinLevel2UpdateMessage, localTimestamp: Date) {
|
|
70
|
+
const symbol = message.topic.split(':')[1]
|
|
71
|
+
|
|
72
|
+
if (this.symbolToDepthInfoMapping[symbol] === undefined) {
|
|
73
|
+
this.symbolToDepthInfoMapping[symbol] = {
|
|
74
|
+
bufferedUpdates: new CircularBuffer<KucoinLevel2UpdateMessage>(2000)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const symbolDepthInfo = this.symbolToDepthInfoMapping[symbol]
|
|
79
|
+
const snapshotAlreadyProcessed = symbolDepthInfo.snapshotProcessed
|
|
80
|
+
|
|
81
|
+
// first check if received message is snapshot and process it as such if it is
|
|
82
|
+
if (message.subject === 'trade.l2Snapshot') {
|
|
83
|
+
// if we've already received 'manual' snapshot, ignore if there is another one
|
|
84
|
+
if (snapshotAlreadyProcessed) {
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
// produce snapshot book_change
|
|
88
|
+
const kucoinSnapshotData = message.data
|
|
89
|
+
|
|
90
|
+
// mark given symbol depth info that has snapshot processed
|
|
91
|
+
symbolDepthInfo.lastUpdateId = Number(kucoinSnapshotData.sequence)
|
|
92
|
+
symbolDepthInfo.snapshotProcessed = true
|
|
93
|
+
|
|
94
|
+
// if there were any depth updates buffered, let's proccess those by adding to or updating the initial snapshot
|
|
95
|
+
for (const update of symbolDepthInfo.bufferedUpdates.items()) {
|
|
96
|
+
const bookChange = this.mapBookDepthUpdate(update, localTimestamp)
|
|
97
|
+
if (bookChange !== undefined) {
|
|
98
|
+
for (const bid of update.data.changes.bids) {
|
|
99
|
+
if (bid[0] == '0') {
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
const matchingBid = kucoinSnapshotData.bids.find((b) => b[0] === bid[0])
|
|
103
|
+
if (matchingBid !== undefined) {
|
|
104
|
+
matchingBid[1] = bid[1]
|
|
105
|
+
} else {
|
|
106
|
+
kucoinSnapshotData.bids.push([bid[0], bid[1]])
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const ask of update.data.changes.asks) {
|
|
111
|
+
if (ask[0] == '0') {
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const matchingAsk = kucoinSnapshotData.asks.find((a) => a[0] === ask[0])
|
|
116
|
+
if (matchingAsk !== undefined) {
|
|
117
|
+
matchingAsk[1] = ask[1]
|
|
118
|
+
} else {
|
|
119
|
+
kucoinSnapshotData.asks.push([ask[0], ask[1]])
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// remove all buffered updates
|
|
126
|
+
symbolDepthInfo.bufferedUpdates.clear()
|
|
127
|
+
|
|
128
|
+
const bookChange: BookChange = {
|
|
129
|
+
type: 'book_change',
|
|
130
|
+
symbol,
|
|
131
|
+
exchange: this._exchange,
|
|
132
|
+
isSnapshot: true,
|
|
133
|
+
bids: kucoinSnapshotData.bids.map(this.mapBookLevel),
|
|
134
|
+
asks: kucoinSnapshotData.asks.map(this.mapBookLevel),
|
|
135
|
+
timestamp: localTimestamp,
|
|
136
|
+
localTimestamp
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
yield bookChange
|
|
140
|
+
} else if (snapshotAlreadyProcessed) {
|
|
141
|
+
// snapshot was already processed let's map the message as normal book_change
|
|
142
|
+
const bookChange = this.mapBookDepthUpdate(message, localTimestamp)
|
|
143
|
+
if (bookChange !== undefined) {
|
|
144
|
+
yield bookChange
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
symbolDepthInfo.bufferedUpdates.append(message)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
protected mapBookDepthUpdate(l2UpdateMessage: KucoinLevel2UpdateMessage, localTimestamp: Date): BookChange | undefined {
|
|
152
|
+
// we can safely assume here that depthContext and lastUpdateId aren't null here as this is method only works
|
|
153
|
+
// when we've already processed the snapshot
|
|
154
|
+
const depthContext = this.symbolToDepthInfoMapping[l2UpdateMessage.data.symbol]!
|
|
155
|
+
const lastUpdateId = depthContext.lastUpdateId!
|
|
156
|
+
|
|
157
|
+
// Drop any event where sequenceEnd is <= lastUpdateId in the snapshot
|
|
158
|
+
if (l2UpdateMessage.data.sequenceEnd <= lastUpdateId) {
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The first processed event should have sequenceStart <= lastUpdateId+1 AND sequenceEnd >= lastUpdateId+1.
|
|
163
|
+
if (!depthContext.validatedFirstUpdate) {
|
|
164
|
+
// if there is new instrument added it can have empty book at first and that's normal
|
|
165
|
+
const bookSnapshotIsEmpty = lastUpdateId == -1 || lastUpdateId == 0
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
(l2UpdateMessage.data.sequenceStart <= lastUpdateId + 1 && l2UpdateMessage.data.sequenceEnd >= lastUpdateId + 1) ||
|
|
169
|
+
bookSnapshotIsEmpty
|
|
170
|
+
) {
|
|
171
|
+
depthContext.validatedFirstUpdate = true
|
|
172
|
+
} else {
|
|
173
|
+
const message = `Book depth snapshot has no overlap with first update, update ${JSON.stringify(
|
|
174
|
+
l2UpdateMessage
|
|
175
|
+
)}, lastUpdateId: ${lastUpdateId}, exchange ${this._exchange}`
|
|
176
|
+
if (this.ignoreBookSnapshotOverlapError) {
|
|
177
|
+
depthContext.validatedFirstUpdate = true
|
|
178
|
+
debug(message)
|
|
179
|
+
} else {
|
|
180
|
+
throw new Error(message)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const bids = l2UpdateMessage.data.changes.bids.map(this.mapBookLevel).filter(this.nonZeroLevels)
|
|
185
|
+
const asks = l2UpdateMessage.data.changes.asks.map(this.mapBookLevel).filter(this.nonZeroLevels)
|
|
186
|
+
|
|
187
|
+
if (bids.length === 0 && asks.length === 0) {
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
type: 'book_change',
|
|
193
|
+
symbol: l2UpdateMessage.data.symbol,
|
|
194
|
+
exchange: this._exchange,
|
|
195
|
+
isSnapshot: false,
|
|
196
|
+
bids,
|
|
197
|
+
asks,
|
|
198
|
+
timestamp: localTimestamp,
|
|
199
|
+
localTimestamp: localTimestamp
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private mapBookLevel(level: [string, string, string?]) {
|
|
204
|
+
const price = Number(level[0])
|
|
205
|
+
const amount = Number(level[1])
|
|
206
|
+
return { price, amount }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private nonZeroLevels(level: BookPriceLevel) {
|
|
210
|
+
return level.price > 0
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export class KucoinBookTickerMapper implements Mapper<'kucoin', BookTicker> {
|
|
215
|
+
constructor(protected readonly _exchange: Exchange) {}
|
|
216
|
+
|
|
217
|
+
canHandle(message: KucoinTickerMessage) {
|
|
218
|
+
return message.type === 'message' && message.topic.startsWith('/market/ticker')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getFilters(symbols?: string[]) {
|
|
222
|
+
symbols = upperCaseSymbols(symbols)
|
|
223
|
+
return [
|
|
224
|
+
{
|
|
225
|
+
channel: 'market/ticker',
|
|
226
|
+
symbols
|
|
227
|
+
} as const
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
*map(message: KucoinTickerMessage, localTimestamp: Date) {
|
|
232
|
+
const symbol = message.topic.split(':')[1]
|
|
233
|
+
|
|
234
|
+
const bookTicker: BookTicker = {
|
|
235
|
+
type: 'book_ticker',
|
|
236
|
+
symbol,
|
|
237
|
+
exchange: this._exchange,
|
|
238
|
+
askAmount: message.data.bestAskSize !== undefined && message.data.bestAskSize !== null ? Number(message.data.bestAskSize) : undefined,
|
|
239
|
+
askPrice: message.data.bestAsk !== undefined && message.data.bestAsk !== null ? Number(message.data.bestAsk) : undefined,
|
|
240
|
+
bidPrice: message.data.bestBid !== undefined && message.data.bestBid !== null ? Number(message.data.bestBid) : undefined,
|
|
241
|
+
bidAmount: message.data.bestBidSize !== undefined && message.data.bestBidSize !== null ? Number(message.data.bestBidSize) : undefined,
|
|
242
|
+
timestamp: new Date(message.data.time),
|
|
243
|
+
localTimestamp: localTimestamp
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
yield bookTicker
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
type KucoinTickerMessage = {
|
|
251
|
+
type: 'message'
|
|
252
|
+
topic: '/market/ticker:ADA-USDT'
|
|
253
|
+
subject: 'trade.ticker'
|
|
254
|
+
data: {
|
|
255
|
+
bestAsk: '0.549931'
|
|
256
|
+
bestAskSize: '966.4756'
|
|
257
|
+
bestBid: '0.549824'
|
|
258
|
+
bestBidSize: '1050'
|
|
259
|
+
price: '0.549825'
|
|
260
|
+
sequence: '1623526404099'
|
|
261
|
+
size: '1'
|
|
262
|
+
time: 1660608019871
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
type KucoinTradeMessage = {
|
|
267
|
+
type: 'message'
|
|
268
|
+
topic: '/market/match:BTC-USDT'
|
|
269
|
+
subject: 'trade.l3match'
|
|
270
|
+
data: {
|
|
271
|
+
symbol: 'BTC-USDT'
|
|
272
|
+
side: 'sell'
|
|
273
|
+
type: 'match'
|
|
274
|
+
makerOrderId: '62fadde41add68000167fb58'
|
|
275
|
+
sequence: '1636276321894'
|
|
276
|
+
size: '0.00001255'
|
|
277
|
+
price: '24093.9'
|
|
278
|
+
takerOrderId: '62faddfff0476c0001c86c71'
|
|
279
|
+
time: '1660608000026914990'
|
|
280
|
+
tradeId: '62fade002e113d292303a18b'
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
type LocalDepthInfo = {
|
|
285
|
+
bufferedUpdates: CircularBuffer<KucoinLevel2UpdateMessage>
|
|
286
|
+
snapshotProcessed?: boolean
|
|
287
|
+
lastUpdateId?: number
|
|
288
|
+
validatedFirstUpdate?: boolean
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
type KucoinLevel2SnapshotMessage = {
|
|
292
|
+
type: 'message'
|
|
293
|
+
generated: true
|
|
294
|
+
topic: '/market/level2Snapshot:BTC-USDT'
|
|
295
|
+
subject: 'trade.l2Snapshot'
|
|
296
|
+
code: '200000'
|
|
297
|
+
data: {
|
|
298
|
+
time: 1660608003710
|
|
299
|
+
sequence: '1636276324355'
|
|
300
|
+
bids: [string, string][]
|
|
301
|
+
asks: [string, string][]
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
type KucoinLevel2UpdateMessage = {
|
|
306
|
+
type: 'message'
|
|
307
|
+
topic: '/market/level2:BTC-USDT'
|
|
308
|
+
subject: 'trade.l2update'
|
|
309
|
+
data: {
|
|
310
|
+
sequenceStart: 1636276324710
|
|
311
|
+
symbol: 'BTC-USDT'
|
|
312
|
+
changes: { asks: [string, string, string][]; bids: [string, string, string][] }
|
|
313
|
+
sequenceEnd: 1636276324710
|
|
314
|
+
}
|
|
315
|
+
}
|
|
@@ -39,7 +39,7 @@ class BinanceFuturesOpenInterestClient extends PoolingClientBase {
|
|
|
39
39
|
|
|
40
40
|
protected async poolDataToStream(outputStream: Writable) {
|
|
41
41
|
for (const instruments of batch(this._instruments, 10)) {
|
|
42
|
-
await Promise.
|
|
42
|
+
await Promise.allSettled(
|
|
43
43
|
instruments.map(async (instrument) => {
|
|
44
44
|
if (outputStream.destroyed) {
|
|
45
45
|
return
|
package/src/realtimefeeds/ftx.ts
CHANGED
|
@@ -85,7 +85,7 @@ class FTXInstrumentInfoClient extends PoolingClientBase {
|
|
|
85
85
|
|
|
86
86
|
protected async poolDataToStream(outputStream: Writable) {
|
|
87
87
|
for (const instruments of batch(this._instruments, 10)) {
|
|
88
|
-
await Promise.
|
|
88
|
+
await Promise.allSettled(
|
|
89
89
|
instruments.map(async (instrument) => {
|
|
90
90
|
if (outputStream.destroyed) {
|
|
91
91
|
return
|
|
@@ -241,7 +241,7 @@ class HuobiOpenInterestClient extends PoolingClientBase {
|
|
|
241
241
|
|
|
242
242
|
protected async poolDataToStream(outputStream: Writable) {
|
|
243
243
|
for (const instruments of batch(this._instruments, 10)) {
|
|
244
|
-
await Promise.
|
|
244
|
+
await Promise.allSettled(
|
|
245
245
|
instruments.map(async (instrument) => {
|
|
246
246
|
if (outputStream.destroyed) {
|
|
247
247
|
return
|
|
@@ -311,7 +311,7 @@ class HuobiOptionsIndexClient extends PoolingClientBase {
|
|
|
311
311
|
|
|
312
312
|
protected async poolDataToStream(outputStream: Writable) {
|
|
313
313
|
for (const instruments of batch(this._instruments, 10)) {
|
|
314
|
-
await Promise.
|
|
314
|
+
await Promise.allSettled(
|
|
315
315
|
instruments.map(async (instrument) => {
|
|
316
316
|
if (outputStream.destroyed) {
|
|
317
317
|
return
|
|
@@ -44,6 +44,7 @@ import { MangoRealTimeFeed } from './mango'
|
|
|
44
44
|
import { BybitSpotRealTimeFeed } from './bybitspot'
|
|
45
45
|
import { CryptoComRealTimeFeed } from './cryptocom'
|
|
46
46
|
import { CryptoComDerivativesRealTimeFeed } from './cryptocomderivatives'
|
|
47
|
+
import { KucoinRealTimeFeed } from './kucoin'
|
|
47
48
|
|
|
48
49
|
export * from './realtimefeed'
|
|
49
50
|
|
|
@@ -95,7 +96,8 @@ const realTimeFeedsMap: {
|
|
|
95
96
|
mango: MangoRealTimeFeed,
|
|
96
97
|
'bybit-spot': BybitSpotRealTimeFeed,
|
|
97
98
|
'crypto-com': CryptoComRealTimeFeed,
|
|
98
|
-
'crypto-com-derivatives': CryptoComDerivativesRealTimeFeed
|
|
99
|
+
'crypto-com-derivatives': CryptoComDerivativesRealTimeFeed,
|
|
100
|
+
kucoin: KucoinRealTimeFeed
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
export function getRealTimeFeedFactory(exchange: Exchange): RealTimeFeed {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { httpClient, getRandomString, wait } from '../handy'
|
|
2
|
+
import { Filter } from '../types'
|
|
3
|
+
import { RealTimeFeedBase } from './realtimefeed'
|
|
4
|
+
|
|
5
|
+
export class KucoinRealTimeFeed extends RealTimeFeedBase {
|
|
6
|
+
protected wssURL = ''
|
|
7
|
+
private _httpURL = 'https://api.kucoin.com/api'
|
|
8
|
+
|
|
9
|
+
protected async getWebSocketUrl() {
|
|
10
|
+
const response = (await httpClient.post(`${this._httpURL}/v1/bullet-public`, { retry: 3, timeout: 10000 }).json()) as any
|
|
11
|
+
|
|
12
|
+
return `${response.data.instanceServers[0].endpoint}?token=${response.data.token}&connectId=${getRandomString()}`
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
|
|
16
|
+
return filters
|
|
17
|
+
.filter((f) => f.channel !== 'market/level2Snapshot')
|
|
18
|
+
.map((filter) => {
|
|
19
|
+
if (!filter.symbols || filter.symbols.length === 0) {
|
|
20
|
+
throw new Error('KucoinRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
id: getRandomString(),
|
|
25
|
+
type: 'subscribe',
|
|
26
|
+
topic: `/${filter.channel}:${filter.symbols.join(',')}`,
|
|
27
|
+
privateChannel: false,
|
|
28
|
+
response: true
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) {
|
|
34
|
+
const depthSnapshotFilter = filters.find((f) => f.channel === 'market/level2Snapshot')
|
|
35
|
+
if (!depthSnapshotFilter) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.debug('requesting manual snapshots for: %s', depthSnapshotFilter.symbols)
|
|
40
|
+
for (let symbol of depthSnapshotFilter.symbols!) {
|
|
41
|
+
if (shouldCancel()) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const depthSnapshotResponse = (await httpClient
|
|
46
|
+
.get(`${this._httpURL}/v1/market/orderbook/level2_100?symbol=${symbol}`, { timeout: 10000 })
|
|
47
|
+
.json()) as any
|
|
48
|
+
|
|
49
|
+
const snapshot = {
|
|
50
|
+
type: 'message',
|
|
51
|
+
generated: true,
|
|
52
|
+
topic: `/market/level2Snapshot:${symbol}`,
|
|
53
|
+
subject: 'trade.l2Snapshot',
|
|
54
|
+
...depthSnapshotResponse
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
this.manualSnapshotsBuffer.push(snapshot)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.debug('requested manual snapshots successfully for: %s ', depthSnapshotFilter.symbols)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
protected messageIsError(message: any): boolean {
|
|
64
|
+
return message.type === 'error'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
protected sendCustomPing = () => {
|
|
68
|
+
this.send({
|
|
69
|
+
id: new Date().valueOf().toString(),
|
|
70
|
+
type: 'ping'
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
protected messageIsHeartbeat(msg: any) {
|
|
75
|
+
return msg.type === 'pong'
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -58,6 +58,13 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
protected async getWebSocketUrl() {
|
|
62
|
+
const wssUrlOverride = process.env[`WSS_URL_${this._exchange.toUpperCase()}`]
|
|
63
|
+
const finalWssUrl = wssUrlOverride !== undefined ? wssUrlOverride : this.wssURL
|
|
64
|
+
|
|
65
|
+
return finalWssUrl
|
|
66
|
+
}
|
|
67
|
+
|
|
61
68
|
private async *_stream() {
|
|
62
69
|
let staleConnectionTimerId
|
|
63
70
|
let pingTimerId
|
|
@@ -66,9 +73,7 @@ export abstract class RealTimeFeedBase implements RealTimeFeedIterable {
|
|
|
66
73
|
while (true) {
|
|
67
74
|
try {
|
|
68
75
|
const subscribeMessages = this.mapToSubscribeMessages(this._filters)
|
|
69
|
-
|
|
70
|
-
const wssUrlOverride = process.env[`WSS_URL_${this._exchange.toUpperCase()}`]
|
|
71
|
-
const finalWssUrl = wssUrlOverride !== undefined ? wssUrlOverride : this.wssURL
|
|
76
|
+
const finalWssUrl = await this.getWebSocketUrl()
|
|
72
77
|
|
|
73
78
|
this.debug('(connection id: %d) estabilishing connection to %s', this._connectionId, finalWssUrl)
|
|
74
79
|
|