tardis-machine 18.3.8 → 18.4.1

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 (100) hide show
  1. package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
  2. package/dist/ws/subscriptionsmappers.js +7 -0
  3. package/dist/ws/subscriptionsmappers.js.map +1 -1
  4. package/node_modules/tardis-dev/dist/consts.d.ts +7 -5
  5. package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
  6. package/node_modules/tardis-dev/dist/consts.js +33 -5
  7. package/node_modules/tardis-dev/dist/consts.js.map +1 -1
  8. package/node_modules/tardis-dev/dist/handy.d.ts +1 -0
  9. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  10. package/node_modules/tardis-dev/dist/handy.js +19 -10
  11. package/node_modules/tardis-dev/dist/handy.js.map +1 -1
  12. package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
  13. package/node_modules/tardis-dev/dist/mappers/aster.d.ts +8 -8
  14. package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
  15. package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
  16. package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
  17. package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
  18. package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
  19. package/node_modules/tardis-dev/dist/mappers/bithumb.d.ts +2 -2
  20. package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
  21. package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
  22. package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +3 -2
  23. package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts.map +1 -1
  24. package/node_modules/tardis-dev/dist/mappers/bitstamp.js +49 -2
  25. package/node_modules/tardis-dev/dist/mappers/bitstamp.js.map +1 -1
  26. package/node_modules/tardis-dev/dist/mappers/bitvavo.d.ts +3 -3
  27. package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
  28. package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
  29. package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +26 -7
  30. package/node_modules/tardis-dev/dist/mappers/bybit.d.ts.map +1 -1
  31. package/node_modules/tardis-dev/dist/mappers/bybit.js +4 -0
  32. package/node_modules/tardis-dev/dist/mappers/bybit.js.map +1 -1
  33. package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
  34. package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
  35. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.d.ts +10 -0
  36. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.d.ts.map +1 -0
  37. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.js +144 -0
  38. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.js.map +1 -0
  39. package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
  40. package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
  41. package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
  42. package/node_modules/tardis-dev/dist/mappers/cryptofacilities.d.ts +2 -2
  43. package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
  44. package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +3 -3
  45. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  46. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  47. package/node_modules/tardis-dev/dist/mappers/dydxv4.js +19 -2
  48. package/node_modules/tardis-dev/dist/mappers/dydxv4.js.map +1 -1
  49. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  50. package/node_modules/tardis-dev/dist/mappers/ftx.js +1 -1
  51. package/node_modules/tardis-dev/dist/mappers/ftx.js.map +1 -1
  52. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  53. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  54. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  55. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  56. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +3 -3
  57. package/node_modules/tardis-dev/dist/mappers/index.d.ts +216 -183
  58. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  59. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  60. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  61. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  62. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  63. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +10 -10
  64. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  65. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  66. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  67. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  68. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  69. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  70. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  71. package/node_modules/tardis-dev/dist/realtimefeeds/bitstamp.d.ts +12 -2
  72. package/node_modules/tardis-dev/dist/realtimefeeds/bitstamp.d.ts.map +1 -1
  73. package/node_modules/tardis-dev/dist/realtimefeeds/bitstamp.js +36 -3
  74. package/node_modules/tardis-dev/dist/realtimefeeds/bitstamp.js.map +1 -1
  75. package/node_modules/tardis-dev/dist/realtimefeeds/bybit.d.ts +3 -0
  76. package/node_modules/tardis-dev/dist/realtimefeeds/bybit.d.ts.map +1 -1
  77. package/node_modules/tardis-dev/dist/realtimefeeds/bybit.js +3 -0
  78. package/node_modules/tardis-dev/dist/realtimefeeds/bybit.js.map +1 -1
  79. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.d.ts +32 -0
  80. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.d.ts.map +1 -0
  81. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js +83 -0
  82. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js.map +1 -0
  83. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  84. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +4 -1
  85. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  86. package/node_modules/tardis-dev/package.json +1 -1
  87. package/node_modules/tardis-dev/src/consts.ts +34 -5
  88. package/node_modules/tardis-dev/src/handy.ts +20 -11
  89. package/node_modules/tardis-dev/src/mappers/bitstamp.ts +101 -27
  90. package/node_modules/tardis-dev/src/mappers/bybit.ts +6 -2
  91. package/node_modules/tardis-dev/src/mappers/coinbasederivatives.ts +230 -0
  92. package/node_modules/tardis-dev/src/mappers/dydxv4.ts +20 -2
  93. package/node_modules/tardis-dev/src/mappers/ftx.ts +1 -1
  94. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  95. package/node_modules/tardis-dev/src/realtimefeeds/bitstamp.ts +50 -3
  96. package/node_modules/tardis-dev/src/realtimefeeds/bybit.ts +4 -0
  97. package/node_modules/tardis-dev/src/realtimefeeds/coinbasederivatives.ts +103 -0
  98. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +4 -1
  99. package/package.json +2 -2
  100. package/src/ws/subscriptionsmappers.ts +8 -0
@@ -1,6 +1,6 @@
1
1
  import { lowerCaseSymbols } from '../handy.ts'
2
- import { BookChange, Trade } from '../types.ts'
3
- import { Mapper } from './mapper.ts'
2
+ import { BookChange, DerivativeTicker, Trade } from '../types.ts'
3
+ import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
4
4
  import { exchangeMappers } from './registry.ts'
5
5
 
6
6
  // https://www.bitstamp.net/websocket/v2/
@@ -8,12 +8,13 @@ import { exchangeMappers } from './registry.ts'
8
8
  export const bitstampMappers = exchangeMappers({
9
9
  bitstamp: {
10
10
  trades: () => bitstampTradesMapper,
11
- bookChanges: () => new BitstampBookChangeMapper()
11
+ bookChanges: () => new BitstampBookChangeMapper(),
12
+ derivativeTickers: () => new BitstampDerivativeTickerMapper()
12
13
  }
13
14
  })
14
15
 
15
16
  const bitstampTradesMapper: Mapper<'bitstamp', Trade> = {
16
- canHandle(message: BitstampTrade | BitstampDiffOrderBook | BitstampDiffOrderBookSnapshot) {
17
+ canHandle(message: BitstampMessage) {
17
18
  if (message.data === undefined) {
18
19
  return false
19
20
  }
@@ -43,7 +44,7 @@ const bitstampTradesMapper: Mapper<'bitstamp', Trade> = {
43
44
  type: 'trade',
44
45
  symbol: symbol.toUpperCase(),
45
46
  exchange: 'bitstamp',
46
- id: String(bitstampTrade.id),
47
+ id: bitstampTrade.id_str ?? String(bitstampTrade.id),
47
48
  price: Number(bitstampTrade.price),
48
49
  amount: Number(bitstampTrade.amount),
49
50
  side: bitstampTrade.type === 0 ? 'buy' : 'sell',
@@ -53,10 +54,60 @@ const bitstampTradesMapper: Mapper<'bitstamp', Trade> = {
53
54
  }
54
55
  }
55
56
 
57
+ class BitstampDerivativeTickerMapper implements Mapper<'bitstamp', DerivativeTicker> {
58
+ private readonly _pendingTickerInfoHelper = new PendingTickerInfoHelper()
59
+
60
+ canHandle(message: BitstampMessage) {
61
+ if (message.data === undefined) {
62
+ return false
63
+ }
64
+ if (message.event === 'funding_rate_saved') {
65
+ return message.channel.startsWith('funding_rate_')
66
+ }
67
+ if (message.event === 'ticker') {
68
+ return message.channel.startsWith('ticker_') && message.channel.endsWith('-perp')
69
+ }
70
+ return message.event === 'trade' && message.channel.startsWith('live_trades_') && message.channel.endsWith('-perp')
71
+ }
72
+
73
+ getFilters(symbols?: string[]) {
74
+ symbols = lowerCaseSymbols(symbols)
75
+ return [
76
+ { channel: 'funding_rate', symbols } as const,
77
+ { channel: 'live_trades', symbols } as const,
78
+ { channel: 'ticker', symbols } as const
79
+ ]
80
+ }
81
+
82
+ *map(message: BitstampTrade | BitstampFundingRate | BitstampTicker, localTimestamp: Date): IterableIterator<DerivativeTicker> {
83
+ const symbol = message.channel.slice(message.channel.lastIndexOf('_') + 1).toUpperCase()
84
+ const ticker = this._pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'bitstamp')
85
+ if (message.event === 'trade') {
86
+ // Funding messages own emission and timestamps; trades only update lastPrice.
87
+ ticker.updateLastPrice(Number(message.data.price))
88
+ return
89
+ }
90
+ if (message.event === 'ticker') {
91
+ // Recorder-generated REST ticker, polled every 6 seconds; it only adds open interest to the funding-driven ticker.
92
+ ticker.updateOpenInterest(Number(message.data.open_interest))
93
+ return
94
+ }
95
+
96
+ ticker.updateFundingRate(Number(message.data.funding_rate))
97
+ ticker.updateFundingTimestamp(new Date(Number(message.data.next_funding_time) * 1000))
98
+ ticker.updateMarkPrice(Number(message.data.mark_price))
99
+ ticker.updateIndexPrice(Number(message.data.index_price))
100
+ ticker.updateTimestamp(new Date(Number(message.data.timestamp) * 1000))
101
+ if (ticker.hasChanged()) {
102
+ yield ticker.getSnapshot(localTimestamp)
103
+ }
104
+ }
105
+ }
106
+
56
107
  class BitstampBookChangeMapper implements Mapper<'bitstamp', BookChange> {
57
108
  private readonly _symbolToDepthInfoMapping: { [key: string]: LocalDepthInfo } = {}
58
109
 
59
- canHandle(message: BitstampTrade | BitstampDiffOrderBook | BitstampDiffOrderBookSnapshot) {
110
+ canHandle(message: BitstampMessage) {
60
111
  if (message.data === undefined) {
61
112
  return false
62
113
  }
@@ -176,44 +227,67 @@ class BitstampBookChangeMapper implements Mapper<'bitstamp', BookChange> {
176
227
  }
177
228
  }
178
229
 
179
- type BitstampTrade = {
180
- event: 'trade'
230
+ type BitstampMessage<Event extends string = string, Data = unknown> = {
231
+ event: Event
181
232
  channel: string
182
- data: {
233
+ data: Data
234
+ }
235
+
236
+ type BitstampTrade = BitstampMessage<
237
+ 'trade',
238
+ {
183
239
  microtimestamp: string
184
240
  amount: number
185
241
  price: number
186
242
  type: number
187
243
  id: number
244
+ id_str?: string
245
+ }
246
+ >
247
+ type BitstampTicker = BitstampMessage<
248
+ 'ticker',
249
+ {
250
+ market: string
251
+ open_interest: string
252
+ }
253
+ >
254
+ type BitstampFundingRate = BitstampMessage<
255
+ 'funding_rate_saved',
256
+ {
257
+ market: string
258
+ funding_rate: string
259
+ mark_price: string
260
+ index_price: string
261
+ timestamp: string
262
+ next_funding_time: string
188
263
  }
264
+ >
265
+
266
+ type LocalDepthInfo = {
267
+ bufferedUpdates: BitstampDiffOrderBook[]
268
+ snapshotProcessed?: boolean
269
+ lastUpdateTimestamp?: number
270
+ lastUpdateMicroTimestamp?: number
189
271
  }
190
- type BitstampBookLevel = [string, string]
191
272
 
192
- type BitstampDiffOrderBook = {
193
- data: {
194
- microtimestamp: string
273
+ type BitstampDiffOrderBook = BitstampMessage<
274
+ 'data',
275
+ {
195
276
  timestamp: string
277
+ microtimestamp: string
196
278
  bids: BitstampBookLevel[]
197
279
  asks: BitstampBookLevel[]
198
280
  }
199
- event: 'data'
200
- channel: string
201
- }
281
+ >
202
282
 
203
- type BitstampDiffOrderBookSnapshot = {
204
- event: 'snapshot'
205
- channel: string
206
- data: {
283
+ type BitstampDiffOrderBookSnapshot = BitstampMessage<
284
+ 'snapshot',
285
+ {
207
286
  timestamp: string
208
287
  microtimestamp?: string
209
288
  bids: BitstampBookLevel[]
210
289
  asks: BitstampBookLevel[]
211
290
  }
212
- }
291
+ >
213
292
 
214
- type LocalDepthInfo = {
215
- bufferedUpdates: BitstampDiffOrderBook[]
216
- snapshotProcessed?: boolean
217
- lastUpdateTimestamp?: number
218
- lastUpdateMicroTimestamp?: number
219
- }
293
+ type BitstampBookLevel = [string, string]
@@ -46,12 +46,16 @@ export const bybitMappers = exchangeMappers({
46
46
  trades: () => new BybitV5TradesMapper('bybit-options'),
47
47
  bookChanges: () => new BybitV5BookChangeMapper('bybit-options', { depth: 25 }),
48
48
  optionsSummary: () => new BybitV5OptionSummaryMapper()
49
+ },
50
+ 'bybit-spread': {
51
+ trades: () => new BybitV5TradesMapper('bybit-spread'),
52
+ bookChanges: () => new BybitV5BookChangeMapper('bybit-spread', { depth: 25 })
49
53
  }
50
54
  })
51
55
 
52
56
  // v5 https://bybit-exchange.github.io/docs/v5/ws/connect
53
57
 
54
- class BybitV5TradesMapper implements Mapper<'bybit' | 'bybit-spot' | 'bybit-options', Trade> {
58
+ class BybitV5TradesMapper implements Mapper<'bybit' | 'bybit-spot' | 'bybit-options' | 'bybit-spread', Trade> {
55
59
  constructor(private readonly _exchange: Exchange) {}
56
60
 
57
61
  canHandle(message: BybitV5Trade) {
@@ -90,7 +94,7 @@ class BybitV5TradesMapper implements Mapper<'bybit' | 'bybit-spot' | 'bybit-opti
90
94
  }
91
95
  }
92
96
 
93
- class BybitV5BookChangeMapper implements Mapper<'bybit' | 'bybit-spot' | 'bybit-options', BookChange> {
97
+ class BybitV5BookChangeMapper implements Mapper<'bybit' | 'bybit-spot' | 'bybit-options' | 'bybit-spread', BookChange> {
94
98
  private readonly _depth: number
95
99
 
96
100
  constructor(
@@ -0,0 +1,230 @@
1
+ import { asNumberOrUndefined, parseISODateWithMicroseconds, upperCaseSymbols } from '../handy.ts'
2
+ import { BookChange, BookTicker, DerivativeTicker, Trade } from '../types.ts'
3
+ import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
4
+ import { exchangeMappers } from './registry.ts'
5
+
6
+ export const coinbaseDerivativesMappers = exchangeMappers({
7
+ 'coinbase-derivatives': {
8
+ trades: () => new CoinbaseDerivativesTradesMapper(),
9
+ bookChanges: () => new CoinbaseDerivativesBookChangeMapper(),
10
+ bookTickers: () => new CoinbaseDerivativesBookTickerMapper(),
11
+ derivativeTickers: () => new CoinbaseDerivativesTickerMapper()
12
+ }
13
+ })
14
+
15
+ class CoinbaseDerivativesTradesMapper implements Mapper<'coinbase-derivatives', Trade> {
16
+ // Besides the subscribe snapshot, Coinbase resends recent-trade snapshots mid-connection, each followed by an update with the preceding older trades.
17
+ private readonly _snapshotTradeIds = new Map<string, number>()
18
+
19
+ canHandle(message: CoinbaseDerivativesTrade) {
20
+ return message.channel === 'market_trades'
21
+ }
22
+
23
+ getFilters(symbols?: string[]) {
24
+ return [{ channel: 'market_trades', symbols: upperCaseSymbols(symbols) } as const]
25
+ }
26
+
27
+ *map(message: CoinbaseDerivativesTrade, localTimestamp: Date): IterableIterator<Trade> {
28
+ for (const event of message.events) {
29
+ if (event.type === 'snapshot') {
30
+ for (const trade of event.trades) {
31
+ const tradeId = Number(trade.trade_id)
32
+ this._snapshotTradeIds.set(trade.product_id, Math.max(tradeId, this._snapshotTradeIds.get(trade.product_id) ?? tradeId))
33
+ }
34
+ continue
35
+ }
36
+
37
+ for (const trade of event.trades) {
38
+ if (Number(trade.trade_id) <= (this._snapshotTradeIds.get(trade.product_id) ?? 0)) {
39
+ continue
40
+ }
41
+
42
+ yield {
43
+ type: 'trade',
44
+ exchange: 'coinbase-derivatives',
45
+ symbol: trade.product_id,
46
+ id: trade.trade_id,
47
+ price: Number(trade.price),
48
+ amount: Number(trade.size),
49
+ side: trade.side === 'BUY' ? 'sell' : 'buy',
50
+ timestamp: parseISODateWithMicroseconds(trade.time),
51
+ localTimestamp
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
57
+
58
+ class CoinbaseDerivativesBookChangeMapper implements Mapper<'coinbase-derivatives', BookChange> {
59
+ canHandle(message: CoinbaseDerivativesBook) {
60
+ return message.channel === 'l2_data'
61
+ }
62
+
63
+ getFilters(symbols?: string[]) {
64
+ return [{ channel: 'l2_data', symbols: upperCaseSymbols(symbols) } as const]
65
+ }
66
+
67
+ *map(message: CoinbaseDerivativesBook, localTimestamp: Date): IterableIterator<BookChange> {
68
+ for (const event of message.events) {
69
+ yield {
70
+ type: 'book_change',
71
+ exchange: 'coinbase-derivatives',
72
+ symbol: event.product_id,
73
+ isSnapshot: event.type === 'snapshot',
74
+ bids: event.updates.filter((u) => u.side === 'bid').map(this.mapBookLevel),
75
+ asks: event.updates.filter((u) => u.side === 'offer').map(this.mapBookLevel),
76
+ timestamp: parseISODateWithMicroseconds(
77
+ event.updates.length > 0 ? event.updates[event.updates.length - 1].event_time : message.timestamp
78
+ ),
79
+ localTimestamp
80
+ }
81
+ }
82
+ }
83
+
84
+ private mapBookLevel(update: CoinbaseDerivativesBook['events'][number]['updates'][number]) {
85
+ return {
86
+ price: Number(update.price_level),
87
+ amount: Number(update.new_quantity)
88
+ }
89
+ }
90
+ }
91
+
92
+ class CoinbaseDerivativesBookTickerMapper implements Mapper<'coinbase-derivatives', BookTicker> {
93
+ canHandle(message: CoinbaseDerivativesTicker) {
94
+ return message.channel === 'ticker'
95
+ }
96
+
97
+ getFilters(symbols?: string[]) {
98
+ return [{ channel: 'ticker', symbols: upperCaseSymbols(symbols) } as const]
99
+ }
100
+
101
+ *map(message: CoinbaseDerivativesTicker, localTimestamp: Date): IterableIterator<BookTicker> {
102
+ for (const event of message.events)
103
+ for (const ticker of event.tickers) {
104
+ const bidPrice = asNumberOrUndefined(ticker.best_bid)
105
+ const askPrice = asNumberOrUndefined(ticker.best_ask)
106
+
107
+ if (bidPrice === undefined && askPrice === undefined) {
108
+ continue
109
+ }
110
+
111
+ yield {
112
+ type: 'book_ticker',
113
+ exchange: 'coinbase-derivatives',
114
+ symbol: ticker.product_id,
115
+ bidPrice,
116
+ bidAmount: asNumberOrUndefined(ticker.best_bid_quantity),
117
+ askPrice,
118
+ askAmount: asNumberOrUndefined(ticker.best_ask_quantity),
119
+ timestamp: parseISODateWithMicroseconds(message.timestamp),
120
+ localTimestamp
121
+ }
122
+ }
123
+ }
124
+ }
125
+
126
+ class CoinbaseDerivativesTickerMapper implements Mapper<'coinbase-derivatives', DerivativeTicker> {
127
+ private readonly pending = new PendingTickerInfoHelper()
128
+
129
+ canHandle(message: CoinbaseDerivativesProducts | CoinbaseDerivativesTicker) {
130
+ return message.channel === 'products' || message.channel === 'ticker'
131
+ }
132
+
133
+ getFilters(symbols?: string[]) {
134
+ return [
135
+ { channel: 'products', symbols: upperCaseSymbols(symbols) } as const,
136
+ { channel: 'ticker', symbols: upperCaseSymbols(symbols) } as const
137
+ ]
138
+ }
139
+
140
+ *map(message: CoinbaseDerivativesProducts | CoinbaseDerivativesTicker, localTimestamp: Date) {
141
+ if (message.channel === 'ticker') {
142
+ for (const event of message.events)
143
+ for (const ticker of event.tickers) {
144
+ const info = this.pending.getPendingTickerInfo(ticker.product_id, 'coinbase-derivatives')
145
+ info.updateLastPrice(asNumberOrUndefined(ticker.price))
146
+ info.updateTimestamp(parseISODateWithMicroseconds(message.timestamp))
147
+ if (info.hasChanged()) {
148
+ yield info.getSnapshot(localTimestamp)
149
+ }
150
+ }
151
+ return
152
+ }
153
+
154
+ for (const product of message.events) {
155
+ const info = this.pending.getPendingTickerInfo(product.product_id, 'coinbase-derivatives')
156
+
157
+ const details = product.future_product_details
158
+ info.updateOpenInterest(asNumberOrUndefined(details.open_interest))
159
+ info.updateFundingRate(asNumberOrUndefined(details.funding_rate))
160
+ info.updateIndexPrice(asNumberOrUndefined(details.index_price))
161
+ // funding_time identifies the published rate, not the next funding event. No mark price is supplied.
162
+ info.updateTimestamp(localTimestamp)
163
+
164
+ if (info.hasChanged()) {
165
+ yield info.getSnapshot(localTimestamp)
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ type CoinbaseDerivativesMessage<Channel extends string, Event> = {
172
+ channel: Channel
173
+ events: Event[]
174
+ }
175
+
176
+ type CoinbaseDerivativesTrade = CoinbaseDerivativesMessage<
177
+ 'market_trades',
178
+ {
179
+ type: CoinbaseDerivativesMessageType
180
+ trades: {
181
+ product_id: string
182
+ trade_id: string
183
+ price: string
184
+ size: string
185
+ side: 'BUY' | 'SELL'
186
+ time: string
187
+ }[]
188
+ }
189
+ >
190
+ type CoinbaseDerivativesBook = CoinbaseDerivativesMessage<
191
+ 'l2_data',
192
+ {
193
+ type: CoinbaseDerivativesMessageType
194
+ product_id: string
195
+ updates: {
196
+ side: 'bid' | 'offer'
197
+ price_level: string
198
+ new_quantity: string
199
+ event_time: string
200
+ }[]
201
+ }
202
+ > & { timestamp: string }
203
+
204
+ type CoinbaseDerivativesTicker = CoinbaseDerivativesMessage<
205
+ 'ticker',
206
+ {
207
+ tickers: {
208
+ product_id: string
209
+ price: string
210
+ best_bid?: string
211
+ best_ask?: string
212
+ best_bid_quantity?: string
213
+ best_ask_quantity?: string
214
+ }[]
215
+ }
216
+ > & { timestamp: string }
217
+
218
+ type CoinbaseDerivativesProducts = CoinbaseDerivativesMessage<
219
+ 'products',
220
+ {
221
+ product_id: string
222
+ future_product_details: {
223
+ open_interest: string
224
+ funding_rate: string
225
+ index_price: string
226
+ }
227
+ }
228
+ > & { generated: true }
229
+
230
+ type CoinbaseDerivativesMessageType = 'snapshot' | 'update'
@@ -111,6 +111,8 @@ class DydxV4BookChangeMapper implements Mapper<'dydx-v4', BookChange> {
111
111
 
112
112
  class DydxV4DerivativeTickerMapper implements Mapper<'dydx-v4', DerivativeTicker> {
113
113
  private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
114
+ // Oracle prices also cover price-only markets such as USDT-USD, so a price waits here until its market appears in a snapshot, trading update or trade.
115
+ private readonly _unconfirmedOraclePrices = new Map<string, number>()
114
116
 
115
117
  canHandle(message: DydxMarketsSnapshot | DyDxMarketsUpdate | DyDxTrade) {
116
118
  return message.channel === 'v4_markets' || (message.channel === 'v4_trades' && message.type === 'channel_data')
@@ -133,7 +135,7 @@ class DydxV4DerivativeTickerMapper implements Mapper<'dydx-v4', DerivativeTicker
133
135
 
134
136
  *map(message: DydxMarketsSnapshot | DyDxMarketsUpdate | DyDxTrade, localTimestamp: Date): IterableIterator<DerivativeTicker> {
135
137
  if (message.channel === 'v4_trades') {
136
- const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.id, 'dydx-v4')
138
+ const pendingTickerInfo = this._getMarketTickerInfo(message.id)
137
139
  pendingTickerInfo.updateLastPrice(Number(message.contents.trades[message.contents.trades.length - 1].price))
138
140
 
139
141
  return
@@ -143,7 +145,7 @@ class DydxV4DerivativeTickerMapper implements Mapper<'dydx-v4', DerivativeTicker
143
145
  for (const key in contents) {
144
146
  const marketInfo = (contents as any)[key] as DydxMarketsSnapshotContent | DydxMarketTradeUpdate
145
147
 
146
- const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(key, 'dydx-v4')
148
+ const pendingTickerInfo = this._getMarketTickerInfo(key)
147
149
 
148
150
  if (marketInfo.oraclePrice !== undefined) {
149
151
  pendingTickerInfo.updateMarkPrice(Number(marketInfo.oraclePrice))
@@ -167,6 +169,12 @@ class DydxV4DerivativeTickerMapper implements Mapper<'dydx-v4', DerivativeTicker
167
169
  if (message.type === 'channel_data' && message.contents.oraclePrices !== undefined) {
168
170
  for (const key in message.contents.oraclePrices) {
169
171
  const oraclePriceInfo = (message.contents.oraclePrices as any)[key] as OraclePriceInfo
172
+ if (this.pendingTickerInfoHelper.hasPendingTickerInfo(key) === false) {
173
+ if (oraclePriceInfo.oraclePrice !== undefined) {
174
+ this._unconfirmedOraclePrices.set(key, Number(oraclePriceInfo.oraclePrice))
175
+ }
176
+ continue
177
+ }
170
178
  const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(key, 'dydx-v4')
171
179
  if (oraclePriceInfo.oraclePrice !== undefined) {
172
180
  pendingTickerInfo.updateMarkPrice(Number(oraclePriceInfo.oraclePrice))
@@ -179,6 +187,16 @@ class DydxV4DerivativeTickerMapper implements Mapper<'dydx-v4', DerivativeTicker
179
187
  }
180
188
  }
181
189
  }
190
+
191
+ private _getMarketTickerInfo(market: string) {
192
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(market, 'dydx-v4')
193
+ const oraclePrice = this._unconfirmedOraclePrices.get(market)
194
+ if (oraclePrice !== undefined) {
195
+ pendingTickerInfo.updateMarkPrice(oraclePrice)
196
+ this._unconfirmedOraclePrices.delete(market)
197
+ }
198
+ return pendingTickerInfo
199
+ }
182
200
  }
183
201
 
184
202
  class DydxV4LiquidationsMapper implements Mapper<'dydx-v4', Liquidation> {
@@ -132,7 +132,7 @@ class FTXDerivativeTickerMapper implements Mapper<'ftx', DerivativeTicker> {
132
132
  return [
133
133
  {
134
134
  channel: 'instrument',
135
- symbols: symbols !== undefined ? symbols.filter((s) => s.includes('/') === false) : undefined
135
+ symbols
136
136
  } as const
137
137
  ]
138
138
  }
@@ -16,6 +16,7 @@ import { blockchainComMappers } from './blockchaincom.ts'
16
16
  import { bullishMappers } from './bullish.ts'
17
17
  import { bybitMappers } from './bybit.ts'
18
18
  import { coinbaseMappers } from './coinbase.ts'
19
+ import { coinbaseDerivativesMappers } from './coinbasederivatives.ts'
19
20
  import { coinbaseInternationalMappers } from './coinbaseinternational.ts'
20
21
  import { coinflexMappers } from './coinflex.ts'
21
22
  import { cryptoComMappers } from './cryptocom.ts'
@@ -74,6 +75,7 @@ const registeredMappers = mergeExchangeMappers(
74
75
  bullishMappers,
75
76
  bybitMappers,
76
77
  coinbaseMappers,
78
+ coinbaseDerivativesMappers,
77
79
  coinbaseInternationalMappers,
78
80
  coinflexMappers,
79
81
  cryptoComMappers,
@@ -1,10 +1,57 @@
1
+ import { Writable } from 'stream'
1
2
  import { getJSON } from '../handy.ts'
2
3
  import { Filter } from '../types.ts'
3
- import { RealTimeFeedBase } from './realtimefeed.ts'
4
+ import { MultiConnectionRealTimeFeedBase, PoolingClientBase, RealTimeFeedBase } from './realtimefeed.ts'
4
5
 
5
- export class BitstampRealTimeFeed extends RealTimeFeedBase {
6
+ const BITSTAMP_HTTP_URL = 'https://www.bitstamp.net/api/v2'
7
+
8
+ export class BitstampRealTimeFeed extends MultiConnectionRealTimeFeedBase {
9
+ protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
10
+ const wsFilters = filters.filter((f) => f.channel !== 'ticker')
11
+ if (wsFilters.length > 0) {
12
+ yield new BitstampWebSocketRealTimeFeed(exchange, wsFilters, timeoutIntervalMS, onError)
13
+ }
14
+
15
+ const tickerFilters = filters.filter((f) => f.channel === 'ticker')
16
+ if (tickerFilters.length > 0) {
17
+ yield new BitstampTickerClient(
18
+ exchange,
19
+ tickerFilters.flatMap((f) => f.symbols ?? []),
20
+ onError
21
+ )
22
+ }
23
+ }
24
+ }
25
+
26
+ // Bitstamp WebSocket has no ticker channel; the recorder polls the REST ticker and stores it in this generated message shape.
27
+ class BitstampTickerClient extends PoolingClientBase {
28
+ constructor(
29
+ exchange: string,
30
+ private readonly _symbols: string[],
31
+ onError?: (error: Error) => void
32
+ ) {
33
+ super(exchange, 6, onError)
34
+ }
35
+
36
+ protected async poolDataToStream(outputStream: Writable) {
37
+ const { data: tickers } = await getJSON<{ market: string }[]>(`${BITSTAMP_HTTP_URL}/ticker/`, { timeout: 10000 })
38
+
39
+ for (const ticker of tickers) {
40
+ const symbol = ticker.market.replace('/', '').toLowerCase()
41
+ if (this._symbols.length > 0 && this._symbols.includes(symbol) === false) {
42
+ continue
43
+ }
44
+
45
+ if (outputStream.writable) {
46
+ outputStream.write({ data: ticker, channel: `ticker_${symbol}`, event: 'ticker', generated: true })
47
+ }
48
+ }
49
+ }
50
+ }
51
+
52
+ class BitstampWebSocketRealTimeFeed extends RealTimeFeedBase {
6
53
  protected wssURL = 'wss://ws.bitstamp.net'
7
- protected httpURL = 'https://www.bitstamp.net/api/v2'
54
+ protected httpURL = BITSTAMP_HTTP_URL
8
55
 
9
56
  protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
10
57
  return filters
@@ -90,6 +90,10 @@ export class BybitSpotRealTimeDataFeed extends BybitSingleConnectionRealTimeData
90
90
  protected wssURL: string = 'wss://stream.bybit.com/v5/public/spot'
91
91
  }
92
92
 
93
+ export class BybitSpreadRealTimeDataFeed extends BybitSingleConnectionRealTimeDataFeed {
94
+ protected wssURL: string = 'wss://stream.bybit.com/v5/public/spread'
95
+ }
96
+
93
97
  export class BybitOptionsRealTimeDataFeed extends BybitSingleConnectionRealTimeDataFeed {
94
98
  protected wssURL: string = 'wss://stream.bybit.com/v5/public/option'
95
99