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.
Files changed (53) hide show
  1. package/README.md +4 -4
  2. package/dist/consts.d.ts +1 -1
  3. package/dist/consts.js +1 -1
  4. package/dist/consts.js.map +1 -1
  5. package/dist/handy.d.ts +1 -0
  6. package/dist/handy.d.ts.map +1 -1
  7. package/dist/handy.js +18 -0
  8. package/dist/handy.js.map +1 -1
  9. package/dist/instrumentinfo.d.ts +1 -1
  10. package/dist/instrumentinfo.d.ts.map +1 -1
  11. package/dist/instrumentinfo.js +3 -1
  12. package/dist/instrumentinfo.js.map +1 -1
  13. package/dist/mappers/index.d.ts +3 -3
  14. package/dist/mappers/kraken.d.ts.map +1 -1
  15. package/dist/mappers/kraken.js +95 -8
  16. package/dist/mappers/kraken.js.map +1 -1
  17. package/dist/mappers/woox.d.ts +3 -3
  18. package/dist/mappers/woox.d.ts.map +1 -1
  19. package/dist/mappers/woox.js +255 -6
  20. package/dist/mappers/woox.js.map +1 -1
  21. package/dist/realtimefeeds/index.d.ts.map +1 -1
  22. package/dist/realtimefeeds/index.js +0 -2
  23. package/dist/realtimefeeds/index.js.map +1 -1
  24. package/dist/realtimefeeds/kraken.d.ts +1 -0
  25. package/dist/realtimefeeds/kraken.d.ts.map +1 -1
  26. package/dist/realtimefeeds/kraken.js +26 -15
  27. package/dist/realtimefeeds/kraken.js.map +1 -1
  28. package/dist/realtimefeeds/woox.d.ts +1 -0
  29. package/dist/realtimefeeds/woox.d.ts.map +1 -1
  30. package/dist/realtimefeeds/woox.js +41 -25
  31. package/dist/realtimefeeds/woox.js.map +1 -1
  32. package/dist/replay.d.ts.map +1 -1
  33. package/dist/replay.js +4 -6
  34. package/dist/replay.js.map +1 -1
  35. package/dist/stream.d.ts.map +1 -1
  36. package/dist/stream.js +2 -5
  37. package/dist/stream.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/consts.ts +1 -1
  40. package/src/handy.ts +22 -0
  41. package/src/instrumentinfo.ts +5 -2
  42. package/src/mappers/kraken.ts +162 -11
  43. package/src/mappers/woox.ts +371 -7
  44. package/src/realtimefeeds/index.ts +0 -2
  45. package/src/realtimefeeds/kraken.ts +26 -15
  46. package/src/realtimefeeds/woox.ts +63 -26
  47. package/src/replay.ts +4 -7
  48. package/src/stream.ts +2 -5
  49. package/dist/realtimefeeds/ascendex.d.ts +0 -11
  50. package/dist/realtimefeeds/ascendex.d.ts.map +0 -1
  51. package/dist/realtimefeeds/ascendex.js +0 -59
  52. package/dist/realtimefeeds/ascendex.js.map +0 -1
  53. package/src/realtimefeeds/ascendex.ts +0 -73
@@ -1,14 +1,27 @@
1
- import { upperCaseSymbols } from '../handy.ts'
2
- import { BookChange, BookTicker, DerivativeTicker, Exchange, FilterForExchange, Liquidation, Trade } from '../types.ts'
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: () => new WooxBookChangeMapper(),
10
- derivativeTickers: () => new WooxDerivativeTickerMapper(),
11
- bookTickers: () => new WooxBookTickerMapper()
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
- protected wssURL = 'wss://ws.kraken.com'
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.map((filter) => {
9
- if (!filter.symbols || filter.symbols.length === 0) {
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
- let depth = undefined
14
+ if (!this.channels.has(channel)) {
15
+ throw new Error(`KrakenRealTimeFeed unsupported channel ${channel}`)
16
+ }
14
17
 
15
- if (filter.channel === 'book') {
16
- depth = 1000
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
- event: 'subscribe',
21
- pair: filter.symbols,
22
- subscription: {
23
- name: filter.channel,
24
- depth
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 { wait } from '../handy.ts'
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.woo.org/ws/stream/OqdphuyCtYWxwzhxyLLjOWNdFP7sQt8RPWzmb5xY'
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
- return filters
12
+ const symbolToTopics = new Map<string, string[]>()
13
+
14
+ filters
10
15
  .filter((filter) => filter.channel !== 'orderbook')
11
- .map((filter) => {
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
- return filter.symbols.map((symbol) => {
17
- return {
18
- id: `${symbol}@${filter.channel}`,
19
- topic: `${symbol}@${filter.channel}`,
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
- .flatMap((f) => f)
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
- this.send({
41
- id: `${symbol}@orderbook`,
42
- event: 'request',
43
- params: {
44
- type: 'orderbook',
45
- symbol
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
- await wait(1)
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('sent orderbook requests for: %s', orderbookFilter.symbols)
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, filter)
328
+ yield* normalizeMessages(exchange, undefined, segmentMessages, segmentMappers, createMappers, withDisconnectMessages, segmentFilter)
332
329
  }
333
330
  }
334
331
  }