tardis-machine 18.4.0 → 18.4.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 (83) hide show
  1. package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
  2. package/dist/ws/subscriptionsmappers.js +9 -0
  3. package/dist/ws/subscriptionsmappers.js.map +1 -1
  4. package/node_modules/tardis-dev/dist/consts.d.ts +4 -3
  5. package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
  6. package/node_modules/tardis-dev/dist/consts.js +6 -1
  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 +2 -2
  23. package/node_modules/tardis-dev/dist/mappers/bitvavo.d.ts +3 -3
  24. package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
  25. package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
  26. package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +10 -10
  27. package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
  28. package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
  29. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.d.ts +10 -0
  30. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.d.ts.map +1 -0
  31. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.js +144 -0
  32. package/node_modules/tardis-dev/dist/mappers/coinbasederivatives.js.map +1 -0
  33. package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
  34. package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
  35. package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
  36. package/node_modules/tardis-dev/dist/mappers/cryptofacilities.d.ts +2 -2
  37. package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
  38. package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +3 -3
  39. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  40. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  41. package/node_modules/tardis-dev/dist/mappers/dydxv4.js +19 -2
  42. package/node_modules/tardis-dev/dist/mappers/dydxv4.js.map +1 -1
  43. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  44. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  45. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  46. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  47. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  48. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +3 -3
  49. package/node_modules/tardis-dev/dist/mappers/index.d.ts +198 -186
  50. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  51. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  52. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  53. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  54. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  55. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +10 -10
  56. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  57. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  58. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  59. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  60. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  61. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  62. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  63. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
  64. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +6 -1
  65. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
  66. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.d.ts +32 -0
  67. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.d.ts.map +1 -0
  68. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js +83 -0
  69. package/node_modules/tardis-dev/dist/realtimefeeds/coinbasederivatives.js.map +1 -0
  70. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  71. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +2 -0
  72. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  73. package/node_modules/tardis-dev/package.json +1 -1
  74. package/node_modules/tardis-dev/src/consts.ts +6 -1
  75. package/node_modules/tardis-dev/src/handy.ts +20 -11
  76. package/node_modules/tardis-dev/src/mappers/coinbasederivatives.ts +230 -0
  77. package/node_modules/tardis-dev/src/mappers/dydxv4.ts +20 -2
  78. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  79. package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +7 -1
  80. package/node_modules/tardis-dev/src/realtimefeeds/coinbasederivatives.ts +103 -0
  81. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +2 -0
  82. package/package.json +2 -2
  83. package/src/ws/subscriptionsmappers.ts +11 -0
@@ -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> {
@@ -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,
@@ -437,7 +437,13 @@ export class BinanceFuturesRealTimeFeed extends BinanceRealTimeFeedBase {
437
437
 
438
438
  protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
439
439
  const wsFilters = filters.filter(
440
- (f) => f.channel !== 'openInterest' && f.channel !== 'recentTrades' && f.channel !== 'fundingInfo' && f.channel !== 'insuranceBalance'
440
+ (f) =>
441
+ f.channel !== 'openInterest' &&
442
+ f.channel !== 'recentTrades' &&
443
+ f.channel !== 'fundingInfo' &&
444
+ f.channel !== 'fundingRate' &&
445
+ f.channel !== 'insuranceBalance' &&
446
+ f.channel !== 'tradingSession'
441
447
  )
442
448
 
443
449
  const publicWsFilters = wsFilters.filter((f) => BINANCE_FUTURES_PUBLIC_CHANNELS.has(f.channel))
@@ -0,0 +1,103 @@
1
+ import { Writable } from 'node:stream'
2
+ import { batch, getJSON, optimizeFilters } from '../handy.ts'
3
+ import { Filter } from '../types.ts'
4
+ import { MultiConnectionRealTimeFeedBase, PoolingClientBase, RealTimeFeedBase } from './realtimefeed.ts'
5
+
6
+ export class CoinbaseDerivativesRealTimeFeed extends MultiConnectionRealTimeFeedBase {
7
+ protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
8
+ const wsFilters = optimizeFilters(filters.filter((f) => f.channel !== 'products'))
9
+ for (const filter of wsFilters) {
10
+ if (!['heartbeats', 'subscriptions'].includes(filter.channel) && !filter.symbols?.length) {
11
+ throw new Error('Coinbase derivatives WebSocket subscriptions require explicit symbols')
12
+ }
13
+ }
14
+
15
+ const symbols = [...new Set(wsFilters.flatMap((f) => f.symbols ?? []))]
16
+ const groups: Iterable<string[]> = symbols.length > 0 ? batch(symbols, 25) : wsFilters.length > 0 ? [[]] : []
17
+ for (const group of groups) {
18
+ const groupedFilters = wsFilters
19
+ .map((f) => ({
20
+ ...f,
21
+ symbols: f.symbols?.filter((s) => group.includes(s))
22
+ }))
23
+ .filter((f) => f.symbols?.length || ['heartbeats', 'subscriptions'].includes(f.channel))
24
+ yield new CoinbaseDerivativesSingleConnectionRealTimeFeed(exchange, groupedFilters, timeoutIntervalMS, onError)
25
+ }
26
+
27
+ const productFilters = filters.filter((f) => f.channel === 'products')
28
+ if (productFilters.length > 0) {
29
+ const allProducts = productFilters.some((f) => !f.symbols?.length)
30
+ yield new CoinbaseDerivativesProductsClient(
31
+ exchange,
32
+ allProducts ? undefined : new Set(productFilters.flatMap((f) => f.symbols!)),
33
+ onError
34
+ )
35
+ }
36
+ }
37
+ }
38
+
39
+ class CoinbaseDerivativesSingleConnectionRealTimeFeed extends RealTimeFeedBase {
40
+ protected wssURL = 'wss://advanced-trade-ws.coinbase.com'
41
+ private sequence?: number
42
+
43
+ protected mapToSubscribeMessages(filters: Filter<string>[]) {
44
+ return [
45
+ {
46
+ type: 'subscribe',
47
+ channel: 'heartbeats'
48
+ },
49
+ ...filters
50
+ .filter((f) => !['heartbeats', 'subscriptions'].includes(f.channel))
51
+ .map((f) => ({
52
+ type: 'subscribe',
53
+ channel: f.channel === 'l2_data' ? 'level2' : f.channel,
54
+ product_ids: f.symbols
55
+ }))
56
+ ]
57
+ }
58
+
59
+ protected async onConnected() {
60
+ this.sequence = undefined
61
+ }
62
+
63
+ protected messageIsError(message: { type?: string }) {
64
+ return message.type === 'error'
65
+ }
66
+
67
+ protected onMessage(message: { sequence_num?: number }) {
68
+ if (message.sequence_num === undefined) return
69
+ if (this.sequence !== undefined && message.sequence_num !== this.sequence + 1) {
70
+ throw new Error(`Coinbase derivatives sequence gap: ${this.sequence} -> ${message.sequence_num}`)
71
+ }
72
+ this.sequence = message.sequence_num
73
+ }
74
+ }
75
+
76
+ class CoinbaseDerivativesProductsClient extends PoolingClientBase {
77
+ constructor(
78
+ exchange: string,
79
+ private readonly symbols: Set<string> | undefined,
80
+ onError?: (error: Error) => void
81
+ ) {
82
+ super(exchange, 5, onError)
83
+ }
84
+
85
+ protected async poolDataToStream(output: Writable) {
86
+ for (let offset = 0; !output.destroyed; offset += 100) {
87
+ const { data } = await getJSON<{ products: Product[]; pagination: { has_next: boolean } }>(
88
+ `https://api.coinbase.com/api/v3/brokerage/market/products?product_type=FUTURE&limit=100&offset=${offset}`
89
+ )
90
+
91
+ for (const product of data.products) {
92
+ if (output.destroyed) return
93
+
94
+ if (this.symbols === undefined || this.symbols.has(product.product_id)) {
95
+ output.write({ channel: 'products', generated: true, events: [product] })
96
+ }
97
+ }
98
+ if (!data.pagination.has_next) return
99
+ }
100
+ }
101
+ }
102
+
103
+ type Product = { product_id: string }
@@ -15,6 +15,7 @@ import { BitstampRealTimeFeed } from './bitstamp.ts'
15
15
  import { BitvavoRealTimeFeed } from './bitvavo.ts'
16
16
  import { BybitOptionsRealTimeDataFeed, BybitRealTimeDataFeed, BybitSpotRealTimeDataFeed, BybitSpreadRealTimeDataFeed } from './bybit.ts'
17
17
  import { CoinbaseRealTimeFeed } from './coinbase.ts'
18
+ import { CoinbaseDerivativesRealTimeFeed } from './coinbasederivatives.ts'
18
19
  import { CryptofacilitiesRealTimeFeed } from './cryptofacilities.ts'
19
20
  import { DeribitRealTimeDataFeed } from './deribit.ts'
20
21
  import { FtxRealTimeFeed, FtxUSRealTimeFeed } from './ftx.ts'
@@ -78,6 +79,7 @@ const realTimeFeedsMap: {
78
79
  bitstamp: BitstampRealTimeFeed,
79
80
  bitvavo: BitvavoRealTimeFeed,
80
81
  coinbase: CoinbaseRealTimeFeed,
82
+ 'coinbase-derivatives': CoinbaseDerivativesRealTimeFeed,
81
83
  cryptofacilities: CryptofacilitiesRealTimeFeed,
82
84
  deribit: DeribitRealTimeDataFeed,
83
85
  ftx: FtxRealTimeFeed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tardis-machine",
3
- "version": "18.4.0",
3
+ "version": "18.4.2",
4
4
  "engines": {
5
5
  "node": ">=24.19"
6
6
  },
@@ -76,7 +76,7 @@
76
76
  "license": "MPL-2.0",
77
77
  "dependencies": {
78
78
  "debug": "^4.4.3",
79
- "tardis-dev": "^18.4.0",
79
+ "tardis-dev": "^18.4.2",
80
80
  "ws": "^8.21.3"
81
81
  },
82
82
  "bundleDependencies": true,
@@ -58,6 +58,12 @@ const coinbaseMaper: SubscriptionMapper = {
58
58
  }
59
59
  }
60
60
 
61
+ // https://docs.cdp.coinbase.com/coinbase-app/advanced-trade-apis/websocket/websocket-overview
62
+ const coinbaseDerivativesMapper: SubscriptionMapper = {
63
+ canHandle: (message: any) => message.type === 'subscribe',
64
+ map: (message: any) => [{ channel: message.channel === 'level2' ? 'l2_data' : message.channel, symbols: message.product_ids }]
65
+ }
66
+
61
67
  // https://docs.deribit.com/v2/#subscription-management
62
68
  const deribitMapper: SubscriptionMapper = {
63
69
  canHandle: (message: any) => {
@@ -220,6 +226,10 @@ const binanceMapper: SubscriptionMapper = {
220
226
  const lastSeparator = param.lastIndexOf('@')
221
227
  const firstSeparator = param.indexOf('@')
222
228
 
229
+ if (firstSeparator === -1) {
230
+ return { channel: param, symbols: undefined }
231
+ }
232
+
223
233
  return {
224
234
  channel: param.slice(firstSeparator + 1, lastSeparator == firstSeparator ? undefined : lastSeparator),
225
235
  symbols: [param.slice(0, firstSeparator)]
@@ -811,6 +821,7 @@ const polymarketMapper: SubscriptionMapper = {
811
821
  export const subscriptionsMappers: Record<Exchange, SubscriptionMapper> = {
812
822
  bitmex: bitmexMapper,
813
823
  coinbase: coinbaseMaper,
824
+ 'coinbase-derivatives': coinbaseDerivativesMapper,
814
825
  deribit: deribitMapper,
815
826
  cryptofacilities: cryptofacilitiesMapper,
816
827
  bitstamp: bitstampMapper,