tardis-dev 16.6.0 → 16.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/README.md +4 -4
- package/dist/consts.d.ts +1 -1
- package/dist/consts.js +1 -1
- 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 +18 -0
- package/dist/handy.js.map +1 -1
- package/dist/instrumentinfo.d.ts +1 -1
- package/dist/instrumentinfo.d.ts.map +1 -1
- package/dist/instrumentinfo.js +3 -1
- package/dist/instrumentinfo.js.map +1 -1
- package/dist/mappers/index.d.ts +3 -3
- package/dist/mappers/kraken.d.ts.map +1 -1
- package/dist/mappers/kraken.js +95 -8
- package/dist/mappers/kraken.js.map +1 -1
- package/dist/mappers/woox.d.ts +3 -3
- package/dist/mappers/woox.d.ts.map +1 -1
- package/dist/mappers/woox.js +255 -6
- package/dist/mappers/woox.js.map +1 -1
- package/dist/realtimefeeds/index.d.ts.map +1 -1
- package/dist/realtimefeeds/index.js +0 -2
- package/dist/realtimefeeds/index.js.map +1 -1
- package/dist/realtimefeeds/kraken.d.ts +1 -0
- package/dist/realtimefeeds/kraken.d.ts.map +1 -1
- package/dist/realtimefeeds/kraken.js +26 -15
- package/dist/realtimefeeds/kraken.js.map +1 -1
- package/dist/realtimefeeds/woox.d.ts +1 -0
- package/dist/realtimefeeds/woox.d.ts.map +1 -1
- package/dist/realtimefeeds/woox.js +41 -25
- package/dist/realtimefeeds/woox.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +4 -6
- package/dist/replay.js.map +1 -1
- package/dist/stream.d.ts.map +1 -1
- package/dist/stream.js +2 -5
- package/dist/stream.js.map +1 -1
- package/package.json +1 -1
- package/src/consts.ts +1 -1
- package/src/handy.ts +22 -0
- package/src/instrumentinfo.ts +5 -2
- package/src/mappers/kraken.ts +162 -11
- package/src/mappers/woox.ts +371 -7
- package/src/realtimefeeds/index.ts +0 -2
- package/src/realtimefeeds/kraken.ts +26 -15
- package/src/realtimefeeds/woox.ts +63 -26
- package/src/replay.ts +4 -7
- package/src/stream.ts +2 -5
- package/dist/realtimefeeds/ascendex.d.ts +0 -11
- package/dist/realtimefeeds/ascendex.d.ts.map +0 -1
- package/dist/realtimefeeds/ascendex.js +0 -59
- package/dist/realtimefeeds/ascendex.js.map +0 -1
- package/src/realtimefeeds/ascendex.ts +0 -73
package/src/mappers/woox.ts
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
|
-
import { upperCaseSymbols } from '../handy.ts'
|
|
2
|
-
import { BookChange, BookTicker, DerivativeTicker,
|
|
1
|
+
import { asNumberOrUndefined, upperCaseSymbols } from '../handy.ts'
|
|
2
|
+
import { BookChange, BookTicker, DerivativeTicker, Trade } from '../types.ts'
|
|
3
3
|
import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
|
|
4
|
-
import { exchangeMappers } from './registry.ts'
|
|
4
|
+
import { exchangeMappers, mapper } from './registry.ts'
|
|
5
5
|
|
|
6
|
+
const WOOX_V3_SWITCH_DATE = new Date('2026-06-29T22:02:00.000Z')
|
|
7
|
+
|
|
8
|
+
// WOO X kept Tardis channel names during the V3 rollout, but changed raw topic layout and payload fields.
|
|
9
|
+
// Keep old and V3 mappers separate so replay uses the schema that was actually recorded at the message time.
|
|
6
10
|
export const wooxMappers = exchangeMappers({
|
|
7
11
|
'woo-x': {
|
|
8
|
-
trades: () => wooxTradesMapper,
|
|
9
|
-
bookChanges: (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
trades: mapper([{ until: WOOX_V3_SWITCH_DATE, use: () => wooxTradesMapper }, { use: () => wooxV3TradesMapper }]),
|
|
13
|
+
bookChanges: mapper([
|
|
14
|
+
{ until: WOOX_V3_SWITCH_DATE, use: () => new WooxBookChangeMapper() },
|
|
15
|
+
{ use: () => new WooxV3BookChangeMapper() }
|
|
16
|
+
]),
|
|
17
|
+
derivativeTickers: mapper([
|
|
18
|
+
{ until: WOOX_V3_SWITCH_DATE, use: () => new WooxDerivativeTickerMapper() },
|
|
19
|
+
{ use: () => new WooxV3DerivativeTickerMapper() }
|
|
20
|
+
]),
|
|
21
|
+
bookTickers: mapper([
|
|
22
|
+
{ until: WOOX_V3_SWITCH_DATE, use: () => new WooxBookTickerMapper() },
|
|
23
|
+
{ use: () => new WooxV3BookTickerMapper() }
|
|
24
|
+
])
|
|
12
25
|
}
|
|
13
26
|
})
|
|
14
27
|
|
|
@@ -49,6 +62,41 @@ const wooxTradesMapper: Mapper<'woo-x', Trade> = {
|
|
|
49
62
|
}
|
|
50
63
|
}
|
|
51
64
|
|
|
65
|
+
const wooxV3TradesMapper: Mapper<'woo-x', Trade> = {
|
|
66
|
+
canHandle(message: WooxV3TradeMessage) {
|
|
67
|
+
return message.topic !== undefined && message.topic.startsWith('trade@')
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
getFilters(symbols?: string[]) {
|
|
71
|
+
symbols = upperCaseSymbols(symbols)
|
|
72
|
+
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
channel: 'trade',
|
|
76
|
+
symbols
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
*map(message: WooxV3TradeMessage, localTimestamp: Date): IterableIterator<Trade> {
|
|
82
|
+
if (message.data.src === 0) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
yield {
|
|
87
|
+
type: 'trade',
|
|
88
|
+
symbol: message.data.s,
|
|
89
|
+
exchange: 'woo-x',
|
|
90
|
+
id: undefined,
|
|
91
|
+
price: Number(message.data.px),
|
|
92
|
+
amount: Number(message.data.sx),
|
|
93
|
+
side: message.data.sd === 'SELL' ? 'sell' : 'buy',
|
|
94
|
+
timestamp: new Date(message.data.ts),
|
|
95
|
+
localTimestamp
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
52
100
|
class WooxBookChangeMapper implements Mapper<'woo-x', BookChange> {
|
|
53
101
|
private readonly _symbolToDepthInfoMapping: { [key: string]: LocalDepthInfo } = {}
|
|
54
102
|
|
|
@@ -159,6 +207,129 @@ class WooxBookChangeMapper implements Mapper<'woo-x', BookChange> {
|
|
|
159
207
|
}
|
|
160
208
|
}
|
|
161
209
|
|
|
210
|
+
class WooxV3BookChangeMapper implements Mapper<'woo-x', BookChange> {
|
|
211
|
+
private readonly _symbolToDepthInfoMapping: { [key: string]: WooxV3LocalDepthInfo } = {}
|
|
212
|
+
|
|
213
|
+
canHandle(message: WooxV3OrderbookMessage | WooxV3OrderbookupdateMessage) {
|
|
214
|
+
if (message.topic === undefined) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (message.topic.startsWith('orderbook@')) {
|
|
219
|
+
return 'generated' in message && message.generated === true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return message.topic.startsWith('orderbookupdate@')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
getFilters(symbols?: string[]) {
|
|
226
|
+
symbols = upperCaseSymbols(symbols)
|
|
227
|
+
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
channel: 'orderbook',
|
|
231
|
+
symbols
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
channel: 'orderbookupdate',
|
|
235
|
+
symbols
|
|
236
|
+
}
|
|
237
|
+
]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
*map(message: WooxV3OrderbookMessage | WooxV3OrderbookupdateMessage, localTimestamp: Date): IterableIterator<BookChange> {
|
|
241
|
+
if ('generated' in message) {
|
|
242
|
+
const symbol = message.data.s ?? message.topic.split('@')[1]
|
|
243
|
+
const timestamp = message.data.ts ?? message.ts
|
|
244
|
+
|
|
245
|
+
if (this._symbolToDepthInfoMapping[symbol] === undefined) {
|
|
246
|
+
this._symbolToDepthInfoMapping[symbol] = {
|
|
247
|
+
bufferedUpdates: []
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const symbolDepthInfo = this._symbolToDepthInfoMapping[symbol]
|
|
252
|
+
|
|
253
|
+
yield {
|
|
254
|
+
type: 'book_change',
|
|
255
|
+
symbol,
|
|
256
|
+
exchange: 'woo-x',
|
|
257
|
+
isSnapshot: true,
|
|
258
|
+
bids: message.data.bids.map(this._mapSnapshotLevel),
|
|
259
|
+
asks: message.data.asks.map(this._mapSnapshotLevel),
|
|
260
|
+
timestamp: new Date(timestamp),
|
|
261
|
+
localTimestamp
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
symbolDepthInfo.lastUpdateTimestamp = timestamp
|
|
265
|
+
symbolDepthInfo.snapshotProcessed = true
|
|
266
|
+
|
|
267
|
+
for (const update of symbolDepthInfo.bufferedUpdates) {
|
|
268
|
+
const bookChange = this._mapBookDepthUpdate(update, localTimestamp, symbolDepthInfo, symbol)
|
|
269
|
+
if (bookChange !== undefined) {
|
|
270
|
+
yield bookChange
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
symbolDepthInfo.bufferedUpdates = []
|
|
274
|
+
return
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const symbol = message.data.s
|
|
278
|
+
|
|
279
|
+
if (this._symbolToDepthInfoMapping[symbol] === undefined) {
|
|
280
|
+
this._symbolToDepthInfoMapping[symbol] = {
|
|
281
|
+
bufferedUpdates: []
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const symbolDepthInfo = this._symbolToDepthInfoMapping[symbol]
|
|
286
|
+
const snapshotAlreadyProcessed = symbolDepthInfo.snapshotProcessed
|
|
287
|
+
|
|
288
|
+
if (snapshotAlreadyProcessed) {
|
|
289
|
+
const bookChange = this._mapBookDepthUpdate(message, localTimestamp, symbolDepthInfo, symbol)
|
|
290
|
+
if (bookChange !== undefined) {
|
|
291
|
+
yield bookChange
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
symbolDepthInfo.bufferedUpdates.push(message)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private _mapBookDepthUpdate(
|
|
299
|
+
wooxBookUpdate: WooxV3OrderbookupdateMessage,
|
|
300
|
+
localTimestamp: Date,
|
|
301
|
+
depthInfo: WooxV3LocalDepthInfo,
|
|
302
|
+
symbol: string
|
|
303
|
+
): BookChange | undefined {
|
|
304
|
+
if (wooxBookUpdate.data.prevTs < depthInfo.lastUpdateTimestamp!) {
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
type: 'book_change',
|
|
310
|
+
symbol,
|
|
311
|
+
exchange: 'woo-x',
|
|
312
|
+
isSnapshot: false,
|
|
313
|
+
bids: wooxBookUpdate.data.bids.map(this._mapUpdateLevel),
|
|
314
|
+
asks: wooxBookUpdate.data.asks.map(this._mapUpdateLevel),
|
|
315
|
+
timestamp: new Date(wooxBookUpdate.data.ts),
|
|
316
|
+
localTimestamp
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private _mapSnapshotLevel(level: WooxV3OrderbookSnapshotLevel): { price: number; amount: number } {
|
|
321
|
+
if (Array.isArray(level)) {
|
|
322
|
+
return { price: Number(level[0]), amount: Number(level[1]) }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return { price: Number(level.price), amount: Number(level.quantity) }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private _mapUpdateLevel(level: WooxV3BookLevel) {
|
|
329
|
+
return { price: Number(level[0]), amount: Number(level[1]) }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
162
333
|
class WooxDerivativeTickerMapper implements Mapper<'woo-x', DerivativeTicker> {
|
|
163
334
|
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
|
|
164
335
|
private readonly _indexPrices = new Map<string, number>()
|
|
@@ -246,6 +417,99 @@ class WooxDerivativeTickerMapper implements Mapper<'woo-x', DerivativeTicker> {
|
|
|
246
417
|
}
|
|
247
418
|
}
|
|
248
419
|
|
|
420
|
+
class WooxV3DerivativeTickerMapper implements Mapper<'woo-x', DerivativeTicker> {
|
|
421
|
+
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
|
|
422
|
+
private readonly _indexPrices = new Map<string, number>()
|
|
423
|
+
|
|
424
|
+
canHandle(message: WooxV3TradeMessage | WooxV3EstFundingRate | WooxV3MarkPrice | WooxV3IndexPrice | WooxV3OpenInterest) {
|
|
425
|
+
if (message.topic === undefined) {
|
|
426
|
+
return false
|
|
427
|
+
}
|
|
428
|
+
const symbol = (message.data && message.data.s) || ''
|
|
429
|
+
const isPerp = symbol.startsWith('PERP_')
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
(message.topic.startsWith('trade@') && isPerp) ||
|
|
433
|
+
(message.topic.startsWith('markprice@') && isPerp) ||
|
|
434
|
+
(message.topic.startsWith('estfundingrate@') && isPerp) ||
|
|
435
|
+
message.topic.startsWith('indexprice@') ||
|
|
436
|
+
(message.topic.startsWith('openinterest@') && isPerp)
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getFilters(symbols?: string[]) {
|
|
441
|
+
symbols = upperCaseSymbols(symbols)
|
|
442
|
+
const spotSymbols = symbols !== undefined ? symbols.map((s) => s.replace('PERP_', 'SPOT_')) : []
|
|
443
|
+
|
|
444
|
+
return [
|
|
445
|
+
{
|
|
446
|
+
channel: 'trade',
|
|
447
|
+
symbols
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
channel: 'markprice',
|
|
451
|
+
symbols
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
channel: 'estfundingrate',
|
|
455
|
+
symbols
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
channel: 'openinterest',
|
|
459
|
+
symbols
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
channel: 'indexprice',
|
|
463
|
+
symbols: spotSymbols
|
|
464
|
+
}
|
|
465
|
+
]
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
*map(message: WooxV3TradeMessage | WooxV3EstFundingRate | WooxV3MarkPrice | WooxV3IndexPrice | WooxV3OpenInterest, localTimestamp: Date) {
|
|
469
|
+
if (message.topic.startsWith('indexprice@')) {
|
|
470
|
+
const data = message.data as WooxV3IndexPrice['data']
|
|
471
|
+
this._indexPrices.set(data.s.replace('SPOT_', 'PERP_'), Number(data.px))
|
|
472
|
+
} else {
|
|
473
|
+
const symbol = message.data.s
|
|
474
|
+
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'woo-x')
|
|
475
|
+
|
|
476
|
+
const lastIndexPrice = this._indexPrices.get(symbol)
|
|
477
|
+
if (lastIndexPrice !== undefined) {
|
|
478
|
+
pendingTickerInfo.updateIndexPrice(lastIndexPrice)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (message.topic.startsWith('markprice@')) {
|
|
482
|
+
const data = message.data as WooxV3MarkPrice['data']
|
|
483
|
+
pendingTickerInfo.updateMarkPrice(Number(data.px))
|
|
484
|
+
pendingTickerInfo.updateTimestamp(new Date(message.data.ts))
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (message.topic.startsWith('trade@')) {
|
|
488
|
+
const data = message.data as WooxV3TradeMessage['data']
|
|
489
|
+
pendingTickerInfo.updateLastPrice(Number(data.px))
|
|
490
|
+
pendingTickerInfo.updateTimestamp(new Date(message.data.ts))
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (message.topic.startsWith('estfundingrate@')) {
|
|
494
|
+
const data = message.data as WooxV3EstFundingRate['data']
|
|
495
|
+
pendingTickerInfo.updateFundingRate(Number(data.r))
|
|
496
|
+
pendingTickerInfo.updateFundingTimestamp(new Date(Number(data.ft)))
|
|
497
|
+
pendingTickerInfo.updateTimestamp(new Date(message.data.ts))
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (message.topic.startsWith('openinterest@')) {
|
|
501
|
+
const data = message.data as WooxV3OpenInterest['data']
|
|
502
|
+
pendingTickerInfo.updateOpenInterest(Number(data.oi))
|
|
503
|
+
pendingTickerInfo.updateTimestamp(new Date(message.data.ts))
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (pendingTickerInfo.hasChanged()) {
|
|
507
|
+
yield pendingTickerInfo.getSnapshot(localTimestamp)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
249
513
|
class WooxBookTickerMapper implements Mapper<'woo-x', BookTicker> {
|
|
250
514
|
canHandle(message: WooxTradeMessage) {
|
|
251
515
|
return message.topic !== undefined && message.topic.endsWith('@bbo')
|
|
@@ -282,6 +546,42 @@ class WooxBookTickerMapper implements Mapper<'woo-x', BookTicker> {
|
|
|
282
546
|
}
|
|
283
547
|
}
|
|
284
548
|
|
|
549
|
+
class WooxV3BookTickerMapper implements Mapper<'woo-x', BookTicker> {
|
|
550
|
+
canHandle(message: WooxV3BBOMessage) {
|
|
551
|
+
return message.topic !== undefined && message.topic.startsWith('bbo@')
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
getFilters(symbols?: string[]) {
|
|
555
|
+
symbols = upperCaseSymbols(symbols)
|
|
556
|
+
|
|
557
|
+
return [
|
|
558
|
+
{
|
|
559
|
+
channel: 'bbo',
|
|
560
|
+
symbols
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
*map(wooxBBOMessage: WooxV3BBOMessage, localTimestamp: Date) {
|
|
566
|
+
const wooxBookTicker = wooxBBOMessage.data
|
|
567
|
+
|
|
568
|
+
const ticker: BookTicker = {
|
|
569
|
+
type: 'book_ticker',
|
|
570
|
+
symbol: wooxBookTicker.s,
|
|
571
|
+
exchange: 'woo-x',
|
|
572
|
+
askAmount: asNumberOrUndefined(wooxBookTicker.aq),
|
|
573
|
+
askPrice: asNumberOrUndefined(wooxBookTicker.ap),
|
|
574
|
+
|
|
575
|
+
bidPrice: asNumberOrUndefined(wooxBookTicker.bp),
|
|
576
|
+
bidAmount: asNumberOrUndefined(wooxBookTicker.bq),
|
|
577
|
+
timestamp: new Date(wooxBookTicker.ts),
|
|
578
|
+
localTimestamp: localTimestamp
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
yield ticker
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
285
585
|
type LocalDepthInfo = {
|
|
286
586
|
bufferedUpdates: WooxOrderbookupdateMessage[]
|
|
287
587
|
snapshotProcessed?: boolean
|
|
@@ -294,6 +594,12 @@ type WooxTradeMessage = {
|
|
|
294
594
|
data: { symbol: 'PERP_GALA_USDT'; price: 0.048756; size: 4109; side: 'SELL'; source: 0 }
|
|
295
595
|
}
|
|
296
596
|
|
|
597
|
+
type WooxV3TradeMessage = {
|
|
598
|
+
topic: string
|
|
599
|
+
ts: number
|
|
600
|
+
data: { s: string; px: string; sx: string; sd: 'BUY' | 'SELL'; src: number; ts: number }
|
|
601
|
+
}
|
|
602
|
+
|
|
297
603
|
type WooxOrderbookupdateMessage = {
|
|
298
604
|
topic: 'PERP_BTC_USDT@orderbookupdate'
|
|
299
605
|
ts: 1674432000020
|
|
@@ -318,19 +624,77 @@ type WooxOrderbookMessage = {
|
|
|
318
624
|
}
|
|
319
625
|
}
|
|
320
626
|
|
|
627
|
+
type WooxV3LocalDepthInfo = {
|
|
628
|
+
bufferedUpdates: WooxV3OrderbookupdateMessage[]
|
|
629
|
+
snapshotProcessed?: boolean
|
|
630
|
+
lastUpdateTimestamp?: number
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
type WooxV3BookLevel = [string, string]
|
|
634
|
+
|
|
635
|
+
type WooxV3OrderbookupdateMessage = {
|
|
636
|
+
topic: string
|
|
637
|
+
ts: number
|
|
638
|
+
data: {
|
|
639
|
+
s: string
|
|
640
|
+
prevTs: number
|
|
641
|
+
ts: number
|
|
642
|
+
asks: WooxV3BookLevel[]
|
|
643
|
+
bids: WooxV3BookLevel[]
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
type WooxV3OrderbookRestSnapshotLevel = {
|
|
648
|
+
price: string
|
|
649
|
+
quantity: string
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
type WooxV3OrderbookSnapshotLevel = WooxV3BookLevel | WooxV3OrderbookRestSnapshotLevel
|
|
653
|
+
|
|
654
|
+
type WooxV3OrderbookMessage = {
|
|
655
|
+
topic: string
|
|
656
|
+
ts: number
|
|
657
|
+
generated: true
|
|
658
|
+
data: {
|
|
659
|
+
s?: string
|
|
660
|
+
ts?: number
|
|
661
|
+
asks: WooxV3OrderbookSnapshotLevel[]
|
|
662
|
+
bids: WooxV3OrderbookSnapshotLevel[]
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
321
666
|
type WooxBBOMessage = {
|
|
322
667
|
topic: 'SPOT_ETH_USDT@bbo'
|
|
323
668
|
ts: 1674431999996
|
|
324
669
|
data: { symbol: 'SPOT_ETH_USDT'; ask: 1627.61; askSize: 38.3755; bid: 1627.26; bidSize: 20.424926 }
|
|
325
670
|
}
|
|
326
671
|
|
|
672
|
+
type WooxV3BBOMessage = {
|
|
673
|
+
topic: string
|
|
674
|
+
ts: number
|
|
675
|
+
data: { s: string; ts: number; ap?: string; aq?: string; bp?: string; bq?: string }
|
|
676
|
+
}
|
|
677
|
+
|
|
327
678
|
type WooxMarkPrice = { topic: 'PERP_BTC_USDT@markprice'; ts: 1674432000007; data: { symbol: 'PERP_BTC_USDT'; price: 22711.11 } }
|
|
328
679
|
|
|
680
|
+
type WooxV3MarkPrice = { topic: string; ts: number; data: { s: string; ts: number; px: string } }
|
|
681
|
+
|
|
329
682
|
type WooxEstFundingRate = {
|
|
330
683
|
topic: 'PERP_BTC_USDT@estfundingrate'
|
|
331
684
|
ts: 1674432059002
|
|
332
685
|
data: { symbol: 'PERP_BTC_USDT'; fundingRate: 0.00000782; fundingTs: 1674435600005 }
|
|
333
686
|
}
|
|
334
687
|
|
|
688
|
+
type WooxV3EstFundingRate = {
|
|
689
|
+
topic: string
|
|
690
|
+
ts: number
|
|
691
|
+
data: { s: string; ts: number; r: string; ft: number | string }
|
|
692
|
+
}
|
|
693
|
+
|
|
335
694
|
type WooxIndexPrice = { topic: 'SPOT_BTC_USDT@indexprice'; ts: 1674432000024; data: { symbol: 'SPOT_BTC_USDT'; price: 22708.44 } }
|
|
695
|
+
|
|
696
|
+
type WooxV3IndexPrice = { topic: string; ts: number; data: { s: string; ts: number; px: string } }
|
|
697
|
+
|
|
336
698
|
type WooxOpenInterest = { topic: 'PERP_BTC_USDT@openinterest'; ts: 1674432013624; data: { symbol: 'PERP_BTC_USDT'; openInterest: 83.2241 } }
|
|
699
|
+
|
|
700
|
+
type WooxV3OpenInterest = { topic: string; ts: number; data: { s: string; ts: number; oi: string } }
|
|
@@ -35,7 +35,6 @@ import { GateIOFuturesRealTimeFeed } from './gateiofutures.ts'
|
|
|
35
35
|
import { PoloniexRealTimeFeed } from './poloniex.ts'
|
|
36
36
|
import { CoinflexRealTimeFeed } from './coinflex.ts'
|
|
37
37
|
import { UpbitRealTimeFeed } from './upbit.ts'
|
|
38
|
-
import { AscendexRealTimeFeed } from './ascendex.ts'
|
|
39
38
|
import { DydxRealTimeFeed } from './dydx.ts'
|
|
40
39
|
import { SerumRealTimeFeed } from './serum.ts'
|
|
41
40
|
import { StarAtlasRealTimeFeed } from './staratlas.ts'
|
|
@@ -100,7 +99,6 @@ const realTimeFeedsMap: {
|
|
|
100
99
|
poloniex: PoloniexRealTimeFeed,
|
|
101
100
|
coinflex: CoinflexRealTimeFeed,
|
|
102
101
|
upbit: UpbitRealTimeFeed,
|
|
103
|
-
ascendex: AscendexRealTimeFeed,
|
|
104
102
|
dydx: DydxRealTimeFeed,
|
|
105
103
|
serum: SerumRealTimeFeed,
|
|
106
104
|
'star-atlas': StarAtlasRealTimeFeed,
|
|
@@ -2,36 +2,47 @@ import { Filter } from '../types.ts'
|
|
|
2
2
|
import { RealTimeFeedBase } from './realtimefeed.ts'
|
|
3
3
|
|
|
4
4
|
export class KrakenRealTimeFeed extends RealTimeFeedBase {
|
|
5
|
-
|
|
5
|
+
private readonly channels = new Set(['trade', 'book', 'ticker', 'instrument'])
|
|
6
|
+
protected wssURL = 'wss://ws.kraken.com/v2'
|
|
6
7
|
|
|
7
8
|
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
|
|
8
|
-
return filters.
|
|
9
|
-
if (!
|
|
9
|
+
return filters.flatMap(({ channel, symbols }): any[] => {
|
|
10
|
+
if (channel !== 'instrument' && (!symbols || symbols.length === 0)) {
|
|
10
11
|
throw new Error('KrakenRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
if (!this.channels.has(channel)) {
|
|
15
|
+
throw new Error(`KrakenRealTimeFeed unsupported channel ${channel}`)
|
|
16
|
+
}
|
|
14
17
|
|
|
15
|
-
if (
|
|
16
|
-
|
|
18
|
+
if (channel === 'instrument') {
|
|
19
|
+
return [
|
|
20
|
+
{
|
|
21
|
+
method: 'subscribe',
|
|
22
|
+
params: { channel, include_tokenized_assets: true }
|
|
23
|
+
}
|
|
24
|
+
]
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
return
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
method: 'subscribe',
|
|
30
|
+
params: {
|
|
31
|
+
channel,
|
|
32
|
+
symbol: symbols,
|
|
33
|
+
...(channel === 'ticker' ? { event_trigger: 'bbo' } : {}),
|
|
34
|
+
...(channel === 'book' ? { depth: 1000 } : {})
|
|
35
|
+
}
|
|
25
36
|
}
|
|
26
|
-
|
|
37
|
+
]
|
|
27
38
|
})
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
protected messageIsError(message: any): boolean {
|
|
31
|
-
return message.errorMessage !== undefined
|
|
42
|
+
return message.errorMessage !== undefined || message.success === false
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
protected messageIsHeartbeat(message: any): boolean {
|
|
35
|
-
return message.event === 'heartbeat'
|
|
46
|
+
return message.event === 'heartbeat' || message.channel === 'heartbeat'
|
|
36
47
|
}
|
|
37
48
|
}
|
|
@@ -1,63 +1,73 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getJSON } from '../handy.ts'
|
|
2
2
|
import { Filter } from '../types.ts'
|
|
3
3
|
import { RealTimeFeedBase } from './realtimefeed.ts'
|
|
4
4
|
|
|
5
|
+
const ORDER_BOOK_DEPTH = 50
|
|
6
|
+
|
|
5
7
|
export class WooxRealTimeFeed extends RealTimeFeedBase {
|
|
6
|
-
protected wssURL = 'wss://wss.
|
|
8
|
+
protected wssURL = 'wss://wss.woox.io/v3/public'
|
|
9
|
+
protected readonly httpURL = 'https://api.woox.io'
|
|
7
10
|
|
|
8
11
|
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
|
|
9
|
-
|
|
12
|
+
const symbolToTopics = new Map<string, string[]>()
|
|
13
|
+
|
|
14
|
+
filters
|
|
10
15
|
.filter((filter) => filter.channel !== 'orderbook')
|
|
11
|
-
.
|
|
16
|
+
.forEach((filter) => {
|
|
12
17
|
if (!filter.symbols || filter.symbols.length === 0) {
|
|
13
18
|
throw new Error('WooxRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
|
|
14
19
|
}
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
event: 'subscribe'
|
|
21
|
-
}
|
|
21
|
+
filter.symbols.forEach((symbol) => {
|
|
22
|
+
const topics = symbolToTopics.get(symbol) ?? []
|
|
23
|
+
topics.push(buildTopic(symbol, filter.channel))
|
|
24
|
+
symbolToTopics.set(symbol, topics)
|
|
22
25
|
})
|
|
23
26
|
})
|
|
24
|
-
|
|
27
|
+
|
|
28
|
+
return Array.from(symbolToTopics.entries()).map(([symbol, topics]) => ({
|
|
29
|
+
id: symbol,
|
|
30
|
+
cmd: 'subscribe',
|
|
31
|
+
params: topics
|
|
32
|
+
}))
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
protected async provideManualSnapshots(filters: Filter<string>[], shouldCancel: () => boolean) {
|
|
28
36
|
const orderbookFilter = filters.find((f) => f.channel === 'orderbook')
|
|
29
|
-
if (!orderbookFilter) {
|
|
37
|
+
if (!orderbookFilter?.symbols) {
|
|
30
38
|
return
|
|
31
39
|
}
|
|
32
40
|
|
|
33
|
-
await wait(200)
|
|
34
|
-
|
|
35
41
|
for (let symbol of orderbookFilter.symbols!) {
|
|
36
42
|
if (shouldCancel()) {
|
|
37
43
|
return
|
|
38
44
|
}
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
})
|
|
46
|
+
const response = await getJSON<WooxOrderBookSnapshotResponse>(
|
|
47
|
+
`${this.httpURL}/v3/public/orderbook?symbol=${symbol}&maxLevel=${ORDER_BOOK_DEPTH}`
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
if (response.data.success === false) {
|
|
51
|
+
throw new Error(`Woox orderbook snapshot request failed: ${JSON.stringify(response.data)}`)
|
|
52
|
+
}
|
|
48
53
|
|
|
49
|
-
|
|
54
|
+
this.manualSnapshotsBuffer.push({
|
|
55
|
+
topic: buildTopic(symbol, 'orderbook'),
|
|
56
|
+
ts: response.data.timestamp,
|
|
57
|
+
generated: true,
|
|
58
|
+
data: response.data.data
|
|
59
|
+
})
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
this.debug('
|
|
62
|
+
this.debug('fetched orderbook snapshots for: %s', orderbookFilter.symbols)
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
protected messageIsError(message: any): boolean {
|
|
56
|
-
return message.success === false || message.errorMsg !== undefined
|
|
66
|
+
return message.success === false || message.errorMsg !== undefined || message.cmd === 'ERROR'
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
protected messageIsHeartbeat(message: any): boolean {
|
|
60
|
-
return message.event === 'ping'
|
|
70
|
+
return message.event === 'ping' || message.cmd === 'PING'
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
protected onMessage(msg: any) {
|
|
@@ -66,5 +76,32 @@ export class WooxRealTimeFeed extends RealTimeFeedBase {
|
|
|
66
76
|
event: 'ping'
|
|
67
77
|
})
|
|
68
78
|
}
|
|
79
|
+
if (msg.cmd === 'PING') {
|
|
80
|
+
this.send({
|
|
81
|
+
cmd: 'PONG'
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildTopic(symbol: string, channel: string) {
|
|
88
|
+
if (channel === 'orderbook' || channel === 'orderbookupdate') {
|
|
89
|
+
return `${channel}@${symbol}@${ORDER_BOOK_DEPTH}`
|
|
69
90
|
}
|
|
91
|
+
|
|
92
|
+
return `${channel}@${symbol}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type WooxOrderBookSnapshotResponse = {
|
|
96
|
+
success: boolean
|
|
97
|
+
timestamp: number
|
|
98
|
+
data: {
|
|
99
|
+
asks: WooxOrderBookSnapshotLevel[]
|
|
100
|
+
bids: WooxOrderBookSnapshotLevel[]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type WooxOrderBookSnapshotLevel = {
|
|
105
|
+
price: string
|
|
106
|
+
quantity: string
|
|
70
107
|
}
|
package/src/replay.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { BinarySplitStream } from './binarysplit.ts'
|
|
|
7
7
|
import { clearCacheSync } from './clearcache.ts'
|
|
8
8
|
import { EXCHANGES, EXCHANGE_CHANNELS_INFO } from './consts.ts'
|
|
9
9
|
import { debug } from './debug.ts'
|
|
10
|
-
import { addDays, getFilters, normalizeMessages, parseAsUTCDate, wait } from './handy.ts'
|
|
10
|
+
import { addDays, createNormalizedSymbolFilter, getFilters, normalizeMessages, parseAsUTCDate, wait } from './handy.ts'
|
|
11
11
|
import { MapperFactory, normalizeBookChanges } from './mappers/index.ts'
|
|
12
12
|
import { getOptions } from './options.ts'
|
|
13
13
|
import { Disconnect, Exchange, FilterForExchange } from './types.ts'
|
|
@@ -285,14 +285,10 @@ export function replayNormalized<T extends Exchange, U extends MapperFactory<T,
|
|
|
285
285
|
|
|
286
286
|
// filter normalized messages by symbol as some exchanges do not provide server side filtering so we could end up with messages
|
|
287
287
|
// for symbols we've not requested for
|
|
288
|
-
const upperCaseSymbols = symbols !== undefined ? symbols.map((s) => s.toUpperCase()) : undefined
|
|
289
|
-
const filter = (symbol: string) => {
|
|
290
|
-
return upperCaseSymbols === undefined || upperCaseSymbols.length === 0 || upperCaseSymbols.includes(symbol)
|
|
291
|
-
}
|
|
292
|
-
|
|
293
288
|
const segments = getReplayNormalizedSegments(exchange, normalizers, fromDate, toDate)
|
|
294
289
|
|
|
295
290
|
if (segments.length <= 1) {
|
|
291
|
+
const filter = createNormalizedSymbolFilter(symbols, filters)
|
|
296
292
|
const messages = replay({
|
|
297
293
|
exchange,
|
|
298
294
|
from,
|
|
@@ -315,6 +311,7 @@ export function replayNormalized<T extends Exchange, U extends MapperFactory<T,
|
|
|
315
311
|
const { from: segmentFrom, to: segmentTo } = segments[i]
|
|
316
312
|
const segmentMappers = i === 0 ? mappers : createMappers(segmentFrom)
|
|
317
313
|
const segmentFilters = getFilters(segmentMappers, symbols)
|
|
314
|
+
const segmentFilter = createNormalizedSymbolFilter(symbols, segmentFilters)
|
|
318
315
|
|
|
319
316
|
const segmentMessages = replay({
|
|
320
317
|
exchange,
|
|
@@ -328,7 +325,7 @@ export function replayNormalized<T extends Exchange, U extends MapperFactory<T,
|
|
|
328
325
|
waitWhenDataNotYetAvailable
|
|
329
326
|
})
|
|
330
327
|
|
|
331
|
-
yield* normalizeMessages(exchange, undefined, segmentMessages, segmentMappers, createMappers, withDisconnectMessages,
|
|
328
|
+
yield* normalizeMessages(exchange, undefined, segmentMessages, segmentMappers, createMappers, withDisconnectMessages, segmentFilter)
|
|
332
329
|
}
|
|
333
330
|
}
|
|
334
331
|
}
|