tardis-dev 13.4.12 → 13.6.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 +4 -1
- package/dist/consts.d.ts.map +1 -1
- package/dist/consts.js +11 -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 +257 -0
- package/dist/mappers/cryptocom.d.ts.map +1 -0
- package/dist/mappers/cryptocom.js +193 -0
- package/dist/mappers/cryptocom.js.map +1 -0
- 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 +16 -4
- package/dist/mappers/index.js.map +1 -1
- package/dist/mappers/kucoin.d.ts +109 -0
- package/dist/mappers/kucoin.d.ts.map +1 -0
- package/dist/mappers/kucoin.js +216 -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/cryptocom.d.ts +10 -0
- package/dist/realtimefeeds/cryptocom.d.ts.map +1 -0
- package/dist/realtimefeeds/cryptocom.js +49 -0
- package/dist/realtimefeeds/cryptocom.js.map +1 -0
- package/dist/realtimefeeds/cryptocomderivatives.d.ts +10 -0
- package/dist/realtimefeeds/cryptocomderivatives.d.ts.map +1 -0
- package/dist/realtimefeeds/cryptocomderivatives.js +51 -0
- package/dist/realtimefeeds/cryptocomderivatives.js.map +1 -0
- 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 +7 -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/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +11 -0
- package/dist/replay.js.map +1 -1
- package/package.json +1 -1
- package/src/consts.ts +14 -2
- package/src/handy.ts +4 -0
- package/src/mappers/cryptocom.ts +395 -0
- package/src/mappers/index.ts +16 -4
- package/src/mappers/kucoin.ts +311 -0
- package/src/realtimefeeds/binance.ts +1 -1
- package/src/realtimefeeds/cryptocom.ts +48 -0
- package/src/realtimefeeds/cryptocomderivatives.ts +50 -0
- package/src/realtimefeeds/ftx.ts +1 -1
- package/src/realtimefeeds/huobi.ts +2 -2
- package/src/realtimefeeds/index.ts +7 -1
- package/src/realtimefeeds/kucoin.ts +77 -0
- package/src/realtimefeeds/realtimefeed.ts +8 -3
- package/src/replay.ts +13 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { BookPriceLevel } from '..'
|
|
2
|
+
import { CircularBuffer, upperCaseSymbols } from '../handy'
|
|
3
|
+
import { BookChange, Exchange, BookTicker, Trade } 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) {}
|
|
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
|
+
|
|
177
|
+
throw new Error(message)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const bids = l2UpdateMessage.data.changes.bids.map(this.mapBookLevel).filter(this.nonZeroLevels)
|
|
181
|
+
const asks = l2UpdateMessage.data.changes.asks.map(this.mapBookLevel).filter(this.nonZeroLevels)
|
|
182
|
+
|
|
183
|
+
if (bids.length === 0 && asks.length === 0) {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
type: 'book_change',
|
|
189
|
+
symbol: l2UpdateMessage.data.symbol,
|
|
190
|
+
exchange: this._exchange,
|
|
191
|
+
isSnapshot: false,
|
|
192
|
+
bids,
|
|
193
|
+
asks,
|
|
194
|
+
timestamp: localTimestamp,
|
|
195
|
+
localTimestamp: localTimestamp
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private mapBookLevel(level: [string, string, string?]) {
|
|
200
|
+
const price = Number(level[0])
|
|
201
|
+
const amount = Number(level[1])
|
|
202
|
+
return { price, amount }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private nonZeroLevels(level: BookPriceLevel) {
|
|
206
|
+
return level.price > 0
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export class KucoinBookTickerMapper implements Mapper<'kucoin', BookTicker> {
|
|
211
|
+
constructor(protected readonly _exchange: Exchange) {}
|
|
212
|
+
|
|
213
|
+
canHandle(message: KucoinTickerMessage) {
|
|
214
|
+
return message.type === 'message' && message.topic.startsWith('/market/ticker')
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
getFilters(symbols?: string[]) {
|
|
218
|
+
symbols = upperCaseSymbols(symbols)
|
|
219
|
+
return [
|
|
220
|
+
{
|
|
221
|
+
channel: 'market/ticker',
|
|
222
|
+
symbols
|
|
223
|
+
} as const
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
*map(message: KucoinTickerMessage, localTimestamp: Date) {
|
|
228
|
+
const symbol = message.topic.split(':')[1]
|
|
229
|
+
|
|
230
|
+
const bookTicker: BookTicker = {
|
|
231
|
+
type: 'book_ticker',
|
|
232
|
+
symbol,
|
|
233
|
+
exchange: this._exchange,
|
|
234
|
+
askAmount: message.data.bestAskSize !== undefined && message.data.bestAskSize !== null ? Number(message.data.bestAskSize) : undefined,
|
|
235
|
+
askPrice: message.data.bestAsk !== undefined && message.data.bestAsk !== null ? Number(message.data.bestAsk) : undefined,
|
|
236
|
+
bidPrice: message.data.bestBid !== undefined && message.data.bestBid !== null ? Number(message.data.bestBid) : undefined,
|
|
237
|
+
bidAmount: message.data.bestBidSize !== undefined && message.data.bestBidSize !== null ? Number(message.data.bestBidSize) : undefined,
|
|
238
|
+
timestamp: new Date(message.data.time),
|
|
239
|
+
localTimestamp: localTimestamp
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
yield bookTicker
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
type KucoinTickerMessage = {
|
|
247
|
+
type: 'message'
|
|
248
|
+
topic: '/market/ticker:ADA-USDT'
|
|
249
|
+
subject: 'trade.ticker'
|
|
250
|
+
data: {
|
|
251
|
+
bestAsk: '0.549931'
|
|
252
|
+
bestAskSize: '966.4756'
|
|
253
|
+
bestBid: '0.549824'
|
|
254
|
+
bestBidSize: '1050'
|
|
255
|
+
price: '0.549825'
|
|
256
|
+
sequence: '1623526404099'
|
|
257
|
+
size: '1'
|
|
258
|
+
time: 1660608019871
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
type KucoinTradeMessage = {
|
|
263
|
+
type: 'message'
|
|
264
|
+
topic: '/market/match:BTC-USDT'
|
|
265
|
+
subject: 'trade.l3match'
|
|
266
|
+
data: {
|
|
267
|
+
symbol: 'BTC-USDT'
|
|
268
|
+
side: 'sell'
|
|
269
|
+
type: 'match'
|
|
270
|
+
makerOrderId: '62fadde41add68000167fb58'
|
|
271
|
+
sequence: '1636276321894'
|
|
272
|
+
size: '0.00001255'
|
|
273
|
+
price: '24093.9'
|
|
274
|
+
takerOrderId: '62faddfff0476c0001c86c71'
|
|
275
|
+
time: '1660608000026914990'
|
|
276
|
+
tradeId: '62fade002e113d292303a18b'
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
type LocalDepthInfo = {
|
|
281
|
+
bufferedUpdates: CircularBuffer<KucoinLevel2UpdateMessage>
|
|
282
|
+
snapshotProcessed?: boolean
|
|
283
|
+
lastUpdateId?: number
|
|
284
|
+
validatedFirstUpdate?: boolean
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
type KucoinLevel2SnapshotMessage = {
|
|
288
|
+
type: 'message'
|
|
289
|
+
generated: true
|
|
290
|
+
topic: '/market/level2Snapshot:BTC-USDT'
|
|
291
|
+
subject: 'trade.l2Snapshot'
|
|
292
|
+
code: '200000'
|
|
293
|
+
data: {
|
|
294
|
+
time: 1660608003710
|
|
295
|
+
sequence: '1636276324355'
|
|
296
|
+
bids: [string, string][]
|
|
297
|
+
asks: [string, string][]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
type KucoinLevel2UpdateMessage = {
|
|
302
|
+
type: 'message'
|
|
303
|
+
topic: '/market/level2:BTC-USDT'
|
|
304
|
+
subject: 'trade.l2update'
|
|
305
|
+
data: {
|
|
306
|
+
sequenceStart: 1636276324710
|
|
307
|
+
symbol: 'BTC-USDT'
|
|
308
|
+
changes: { asks: [string, string, string][]; bids: [string, string, string][] }
|
|
309
|
+
sequenceEnd: 1636276324710
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -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
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Filter } from '../types'
|
|
2
|
+
import { RealTimeFeedBase } from './realtimefeed'
|
|
3
|
+
|
|
4
|
+
export class CryptoComRealTimeFeed extends RealTimeFeedBase {
|
|
5
|
+
protected wssURL = 'wss://stream.crypto.com/v2/market'
|
|
6
|
+
|
|
7
|
+
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
|
|
8
|
+
const channels = filters
|
|
9
|
+
.map((filter) => {
|
|
10
|
+
if (!filter.symbols || filter.symbols.length === 0) {
|
|
11
|
+
throw new Error('CryptoComRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return filter.symbols.map((symbol) => {
|
|
15
|
+
const suffix = filter.channel === 'book' ? '.150' : ''
|
|
16
|
+
return `${filter.channel}.${symbol}${suffix}`
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
.flatMap((s) => s)
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
id: 1,
|
|
24
|
+
method: 'subscribe',
|
|
25
|
+
nonce: new Date().valueOf(),
|
|
26
|
+
params: {
|
|
27
|
+
channels: channels
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
protected messageIsError(message: any): boolean {
|
|
34
|
+
return message.code !== undefined && message.code !== 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
protected onMessage(msg: any) {
|
|
38
|
+
if (msg.method === 'public/heartbeat') {
|
|
39
|
+
this.send({
|
|
40
|
+
id: msg.id,
|
|
41
|
+
method: 'public/respond-heartbeat'
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
protected messageIsHeartbeat(msg: any) {
|
|
46
|
+
return msg.method === 'public/heartbeat'
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Filter } from '../types'
|
|
2
|
+
import { RealTimeFeedBase } from './realtimefeed'
|
|
3
|
+
|
|
4
|
+
export class CryptoComDerivativesRealTimeFeed extends RealTimeFeedBase {
|
|
5
|
+
protected wssURL = 'wss://deriv-stream.crypto.com/v1/market'
|
|
6
|
+
|
|
7
|
+
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
|
|
8
|
+
const channels = filters
|
|
9
|
+
.map((filter) => {
|
|
10
|
+
if (!filter.symbols || filter.symbols.length === 0) {
|
|
11
|
+
throw new Error('CryptoComDerivativesRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return filter.symbols.map((symbol) => {
|
|
15
|
+
const suffix = filter.channel === 'book' ? '.50' : ''
|
|
16
|
+
return `${filter.channel}.${symbol}${suffix}`
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
.flatMap((s) => s)
|
|
20
|
+
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
id: 1,
|
|
24
|
+
method: 'subscribe',
|
|
25
|
+
nonce: new Date().valueOf(),
|
|
26
|
+
params: {
|
|
27
|
+
channels: channels,
|
|
28
|
+
book_subscription_type: 'SNAPSHOT_AND_UPDATE',
|
|
29
|
+
book_update_frequency: 5
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
protected messageIsError(message: any): boolean {
|
|
36
|
+
return message.code !== undefined && message.code !== 0 && message.code !== 40003
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
protected onMessage(msg: any) {
|
|
40
|
+
if (msg.method === 'public/heartbeat') {
|
|
41
|
+
this.send({
|
|
42
|
+
id: msg.id,
|
|
43
|
+
method: 'public/respond-heartbeat'
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
protected messageIsHeartbeat(msg: any) {
|
|
48
|
+
return msg.method === 'public/heartbeat'
|
|
49
|
+
}
|
|
50
|
+
}
|
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
|
|
@@ -42,6 +42,9 @@ import { SerumRealTimeFeed } from './serum'
|
|
|
42
42
|
import { StarAtlasRealTimeFeed } from './staratlas'
|
|
43
43
|
import { MangoRealTimeFeed } from './mango'
|
|
44
44
|
import { BybitSpotRealTimeFeed } from './bybitspot'
|
|
45
|
+
import { CryptoComRealTimeFeed } from './cryptocom'
|
|
46
|
+
import { CryptoComDerivativesRealTimeFeed } from './cryptocomderivatives'
|
|
47
|
+
import { KucoinRealTimeFeed } from './kucoin'
|
|
45
48
|
|
|
46
49
|
export * from './realtimefeed'
|
|
47
50
|
|
|
@@ -91,7 +94,10 @@ const realTimeFeedsMap: {
|
|
|
91
94
|
'star-atlas': StarAtlasRealTimeFeed,
|
|
92
95
|
'huobi-dm-options': HuobiDMOptionsRealTimeFeed,
|
|
93
96
|
mango: MangoRealTimeFeed,
|
|
94
|
-
'bybit-spot': BybitSpotRealTimeFeed
|
|
97
|
+
'bybit-spot': BybitSpotRealTimeFeed,
|
|
98
|
+
'crypto-com': CryptoComRealTimeFeed,
|
|
99
|
+
'crypto-com-derivatives': CryptoComDerivativesRealTimeFeed,
|
|
100
|
+
kucoin: KucoinRealTimeFeed
|
|
95
101
|
}
|
|
96
102
|
|
|
97
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
|
|
package/src/replay.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createReadStream } from 'fs-extra'
|
|
1
|
+
import { createReadStream, remove } from 'fs-extra'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import { EventEmitter } from 'stream'
|
|
4
4
|
import { Worker } from 'worker_threads'
|
|
@@ -166,6 +166,10 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
|
|
|
166
166
|
|
|
167
167
|
// remove slice key from the map as it's already processed
|
|
168
168
|
cachedSlicePaths.delete(sliceKey)
|
|
169
|
+
|
|
170
|
+
if (autoCleanup) {
|
|
171
|
+
await cleanupSlice(cachedSlicePath)
|
|
172
|
+
}
|
|
169
173
|
// move one minute forward
|
|
170
174
|
currentSliceDate.setUTCMinutes(currentSliceDate.getUTCMinutes() + 1)
|
|
171
175
|
}
|
|
@@ -209,6 +213,14 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
|
|
|
209
213
|
}
|
|
210
214
|
}
|
|
211
215
|
|
|
216
|
+
async function cleanupSlice(slicePath: string) {
|
|
217
|
+
try {
|
|
218
|
+
await remove(slicePath)
|
|
219
|
+
} catch (e) {
|
|
220
|
+
debug('cleanupSlice error %s %o', slicePath, e)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
212
224
|
// gracefully terminate worker
|
|
213
225
|
async function terminateWorker(worker: Worker, waitTimeout: number) {
|
|
214
226
|
let cancelWait = () => {}
|