tardis-machine 18.0.1 → 18.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
  2. package/dist/ws/subscriptionsmappers.js +3 -1
  3. package/dist/ws/subscriptionsmappers.js.map +1 -1
  4. package/node_modules/tardis-dev/dist/consts.d.ts +3 -1
  5. package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
  6. package/node_modules/tardis-dev/dist/consts.js +17 -0
  7. package/node_modules/tardis-dev/dist/consts.js.map +1 -1
  8. package/node_modules/tardis-dev/dist/downloaddatasets.d.ts.map +1 -1
  9. package/node_modules/tardis-dev/dist/downloaddatasets.js +3 -1
  10. package/node_modules/tardis-dev/dist/downloaddatasets.js.map +1 -1
  11. package/node_modules/tardis-dev/dist/handy.d.ts +15 -1
  12. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  13. package/node_modules/tardis-dev/dist/handy.js +10 -5
  14. package/node_modules/tardis-dev/dist/handy.js.map +1 -1
  15. package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
  16. package/node_modules/tardis-dev/dist/mappers/aster.d.ts +16 -0
  17. package/node_modules/tardis-dev/dist/mappers/aster.d.ts.map +1 -0
  18. package/node_modules/tardis-dev/dist/mappers/aster.js +294 -0
  19. package/node_modules/tardis-dev/dist/mappers/aster.js.map +1 -0
  20. package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
  21. package/node_modules/tardis-dev/dist/mappers/binance.js +22 -22
  22. package/node_modules/tardis-dev/dist/mappers/binance.js.map +1 -1
  23. package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
  24. package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
  25. package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
  26. package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
  27. package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
  28. package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
  29. package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +1 -1
  30. package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
  31. package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
  32. package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +7 -7
  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/coinbaseinternational.d.ts +2 -2
  36. package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
  37. package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
  38. package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
  39. package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +2 -2
  40. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  41. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  42. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  43. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  44. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  45. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  46. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  47. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +4 -4
  48. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts.map +1 -1
  49. package/node_modules/tardis-dev/dist/mappers/hyperliquid.js +15 -4
  50. package/node_modules/tardis-dev/dist/mappers/hyperliquid.js.map +1 -1
  51. package/node_modules/tardis-dev/dist/mappers/index.d.ts +180 -162
  52. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  53. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  54. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  55. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  56. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  57. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +5 -5
  58. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  59. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  60. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  61. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  62. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  63. package/node_modules/tardis-dev/dist/mappers/registry.d.ts.map +1 -1
  64. package/node_modules/tardis-dev/dist/mappers/registry.js.map +1 -1
  65. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  66. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  67. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts +53 -0
  68. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts.map +1 -0
  69. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js +271 -0
  70. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js.map +1 -0
  71. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts +1 -4
  72. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
  73. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +36 -95
  74. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
  75. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  76. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +3 -0
  77. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  78. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  79. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +12 -3
  80. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  81. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts +14 -0
  82. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts.map +1 -0
  83. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js +72 -0
  84. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js.map +1 -0
  85. package/node_modules/tardis-dev/dist/replay.d.ts.map +1 -1
  86. package/node_modules/tardis-dev/dist/replay.js +6 -4
  87. package/node_modules/tardis-dev/dist/replay.js.map +1 -1
  88. package/node_modules/tardis-dev/dist/worker.d.ts.map +1 -1
  89. package/node_modules/tardis-dev/dist/worker.js +5 -2
  90. package/node_modules/tardis-dev/dist/worker.js.map +1 -1
  91. package/node_modules/tardis-dev/dist/zstd.d.ts +3 -0
  92. package/node_modules/tardis-dev/dist/zstd.d.ts.map +1 -0
  93. package/node_modules/tardis-dev/dist/zstd.js +122 -0
  94. package/node_modules/tardis-dev/dist/zstd.js.map +1 -0
  95. package/node_modules/tardis-dev/package.json +2 -2
  96. package/node_modules/tardis-dev/src/consts.ts +18 -0
  97. package/node_modules/tardis-dev/src/downloaddatasets.ts +3 -1
  98. package/node_modules/tardis-dev/src/handy.ts +13 -5
  99. package/node_modules/tardis-dev/src/mappers/aster.ts +438 -0
  100. package/node_modules/tardis-dev/src/mappers/binance.ts +23 -21
  101. package/node_modules/tardis-dev/src/mappers/bitfinex.ts +1 -2
  102. package/node_modules/tardis-dev/src/mappers/hyperliquid.ts +19 -5
  103. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  104. package/node_modules/tardis-dev/src/mappers/registry.ts +1 -2
  105. package/node_modules/tardis-dev/src/realtimefeeds/aster.ts +388 -0
  106. package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +59 -145
  107. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +3 -0
  108. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +12 -3
  109. package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
  110. package/node_modules/tardis-dev/src/replay.ts +6 -4
  111. package/node_modules/tardis-dev/src/worker.ts +5 -2
  112. package/node_modules/tardis-dev/src/zstd.ts +139 -0
  113. package/package.json +4 -4
  114. package/src/ws/subscriptionsmappers.ts +3 -1
@@ -0,0 +1,438 @@
1
+ import { debug } from '../debug.ts'
2
+ import { asNumberOrUndefined, CircularBuffer, lowerCaseSymbols } from '../handy.ts'
3
+ import { BookChange, BookTicker, DerivativeTicker, Liquidation, Trade } from '../types.ts'
4
+ import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
5
+ import { exchangeMappers, isRealTime } from './registry.ts'
6
+
7
+ export const asterMappers = exchangeMappers({
8
+ aster: {
9
+ trades: () => new AsterTradesMapper('aster'),
10
+ bookChanges: (localTimestamp) =>
11
+ new AsterBookChangeMapper('aster', {
12
+ ignoreBookSnapshotOverlapError: shouldIgnoreBookSnapshotOverlap(localTimestamp)
13
+ }),
14
+ bookTickers: () => new AsterBookTickerMapper('aster')
15
+ },
16
+ 'aster-futures': {
17
+ trades: () => new AsterTradesMapper('aster-futures'),
18
+ bookChanges: (localTimestamp) =>
19
+ new AsterFuturesBookChangeMapper({
20
+ ignoreBookSnapshotOverlapError: shouldIgnoreBookSnapshotOverlap(localTimestamp)
21
+ }),
22
+ derivativeTickers: () => new AsterFuturesDerivativeTickerMapper(),
23
+ liquidations: () => new AsterFuturesLiquidationsMapper(),
24
+ bookTickers: () => new AsterBookTickerMapper('aster-futures')
25
+ }
26
+ })
27
+
28
+ function shouldIgnoreBookSnapshotOverlap(date?: Date) {
29
+ if (process.env.IGNORE_BOOK_SNAPSHOT_OVERLAP_ERROR) {
30
+ return true
31
+ }
32
+
33
+ return isRealTime(date) === false
34
+ }
35
+
36
+ class AsterTradesMapper implements Mapper<'aster' | 'aster-futures', Trade> {
37
+ constructor(private readonly exchange: 'aster' | 'aster-futures') {}
38
+
39
+ canHandle(message: AsterMessage<any>) {
40
+ return message.stream?.endsWith('@trade') === true
41
+ }
42
+
43
+ getFilters(symbols?: string[]) {
44
+ return [{ channel: 'trade', symbols: lowerCaseSymbols(symbols) } as const]
45
+ }
46
+
47
+ *map({ data }: AsterMessage<AsterTradeData>, localTimestamp: Date) {
48
+ // Aster's Binance-compatible trade stream uses these X values for off-book
49
+ // activity, so exclude them from normalized public trades too.
50
+ const isOffBookTrade = data.X === 'INSURANCE_FUND' || data.X === 'ADL' || data.X === 'NA'
51
+ if (isOffBookTrade) {
52
+ return
53
+ }
54
+
55
+ const trade: Trade = {
56
+ type: 'trade',
57
+ symbol: data.s,
58
+ exchange: this.exchange,
59
+ id: String(data.t),
60
+ price: Number(data.p),
61
+ amount: Number(data.q),
62
+ side: data.m ? 'sell' : 'buy',
63
+ timestamp: new Date(data.T),
64
+ localTimestamp
65
+ }
66
+
67
+ yield trade
68
+ }
69
+ }
70
+
71
+ class AsterBookChangeMapper implements Mapper<'aster' | 'aster-futures', BookChange> {
72
+ protected readonly symbolToDepthInfoMapping: { [key: string]: LocalDepthInfo } = {}
73
+ protected readonly ignoreBookSnapshotOverlapError: boolean
74
+
75
+ constructor(
76
+ protected readonly exchange: 'aster' | 'aster-futures',
77
+ { ignoreBookSnapshotOverlapError }: { ignoreBookSnapshotOverlapError: boolean }
78
+ ) {
79
+ this.ignoreBookSnapshotOverlapError = ignoreBookSnapshotOverlapError
80
+ }
81
+
82
+ canHandle(message: AsterMessage<any>) {
83
+ return message.stream?.includes('@depth') === true
84
+ }
85
+
86
+ getFilters(symbols?: string[]) {
87
+ return [
88
+ { channel: 'depth', symbols: lowerCaseSymbols(symbols) } as const,
89
+ { channel: 'depthSnapshot', symbols: lowerCaseSymbols(symbols) } as const
90
+ ]
91
+ }
92
+
93
+ *map({ stream, data }: AsterMessage<AsterDepthData | AsterDepthSnapshotData>, localTimestamp: Date) {
94
+ const symbol = stream.split('@')[0].toUpperCase()
95
+
96
+ if (this.symbolToDepthInfoMapping[symbol] === undefined) {
97
+ this.symbolToDepthInfoMapping[symbol] = {
98
+ bufferedUpdates: new CircularBuffer<AsterDepthData>(2000)
99
+ }
100
+ }
101
+
102
+ const symbolDepthInfo = this.symbolToDepthInfoMapping[symbol]
103
+
104
+ if (data.lastUpdateId !== undefined) {
105
+ if (symbolDepthInfo.snapshotProcessed) {
106
+ return
107
+ }
108
+
109
+ symbolDepthInfo.lastUpdateId = data.lastUpdateId
110
+ symbolDepthInfo.snapshotProcessed = true
111
+ const bidsByPrice = new Map(data.bids.map((level) => [level[0], level]))
112
+ const asksByPrice = new Map(data.asks.map((level) => [level[0], level]))
113
+
114
+ for (const update of symbolDepthInfo.bufferedUpdates.items()) {
115
+ const bookChange = this.mapBookDepthUpdate(update, localTimestamp)
116
+ if (bookChange !== undefined) {
117
+ this.applyLevelUpdates(bidsByPrice, update.b)
118
+ this.applyLevelUpdates(asksByPrice, update.a)
119
+ }
120
+ }
121
+
122
+ symbolDepthInfo.bufferedUpdates.clear()
123
+
124
+ const bookChange: BookChange = {
125
+ type: 'book_change',
126
+ symbol,
127
+ exchange: this.exchange,
128
+ isSnapshot: true,
129
+ bids: [...bidsByPrice.values()].map(this.mapBookLevel),
130
+ asks: [...asksByPrice.values()].map(this.mapBookLevel),
131
+ timestamp: data.T !== undefined ? new Date(data.T) : localTimestamp,
132
+ localTimestamp
133
+ }
134
+
135
+ yield bookChange
136
+ } else if (symbolDepthInfo.snapshotProcessed) {
137
+ const bookChange = this.mapBookDepthUpdate(data as AsterDepthData, localTimestamp)
138
+ if (bookChange !== undefined) {
139
+ yield bookChange
140
+ }
141
+ } else {
142
+ symbolDepthInfo.bufferedUpdates.append(data as AsterDepthData)
143
+ }
144
+ }
145
+
146
+ protected mapBookDepthUpdate(depthUpdateData: AsterDepthData, localTimestamp: Date): BookChange | undefined {
147
+ const depthContext = this.symbolToDepthInfoMapping[depthUpdateData.s]!
148
+ const lastUpdateId = depthContext.lastUpdateId!
149
+
150
+ if (this.isOutdatedDepthUpdate(depthUpdateData, lastUpdateId)) {
151
+ return
152
+ }
153
+
154
+ if (!depthContext.validatedFirstUpdate) {
155
+ if (this.hasInitialSnapshotOverlap(depthUpdateData, lastUpdateId) || lastUpdateId === -1) {
156
+ depthContext.validatedFirstUpdate = true
157
+ } else if (this.ignoreBookSnapshotOverlapError) {
158
+ depthContext.validatedFirstUpdate = true
159
+ debug(this.getSnapshotOverlapError(depthUpdateData, lastUpdateId))
160
+ } else {
161
+ throw new Error(this.getSnapshotOverlapError(depthUpdateData, lastUpdateId))
162
+ }
163
+ } else if (depthUpdateData.pu !== lastUpdateId) {
164
+ throw new Error(
165
+ `Book depth update has a sequence gap, update ${JSON.stringify(
166
+ depthUpdateData
167
+ )}, lastUpdateId: ${lastUpdateId}, exchange ${this.exchange}`
168
+ )
169
+ }
170
+
171
+ depthContext.lastUpdateId = depthUpdateData.u
172
+
173
+ return {
174
+ type: 'book_change',
175
+ symbol: depthUpdateData.s,
176
+ exchange: this.exchange,
177
+ isSnapshot: false,
178
+ bids: depthUpdateData.b.map(this.mapBookLevel),
179
+ asks: depthUpdateData.a.map(this.mapBookLevel),
180
+ timestamp: new Date(depthUpdateData.E),
181
+ localTimestamp
182
+ }
183
+ }
184
+
185
+ protected isOutdatedDepthUpdate(depthUpdateData: AsterDepthData, lastUpdateId: number) {
186
+ return depthUpdateData.u <= lastUpdateId
187
+ }
188
+
189
+ protected hasInitialSnapshotOverlap(depthUpdateData: AsterDepthData, lastUpdateId: number) {
190
+ // Live and recorded Spot data use pu as the continuous sequence link. The
191
+ // first non-stale update is safe when its pu..u range contains the snapshot.
192
+ return depthUpdateData.pu <= lastUpdateId && depthUpdateData.u >= lastUpdateId
193
+ }
194
+
195
+ private applyLevelUpdates(levelsByPrice: Map<string, AsterBookLevel>, updates: AsterBookLevel[]) {
196
+ for (const level of updates) {
197
+ if (Number(level[1]) === 0) {
198
+ levelsByPrice.delete(level[0])
199
+ } else {
200
+ levelsByPrice.set(level[0], level)
201
+ }
202
+ }
203
+ }
204
+
205
+ private getSnapshotOverlapError(depthUpdateData: AsterDepthData, lastUpdateId: number) {
206
+ return `Book depth snapshot has no overlap with first update, update ${JSON.stringify(
207
+ depthUpdateData
208
+ )}, lastUpdateId: ${lastUpdateId}, exchange ${this.exchange}`
209
+ }
210
+
211
+ private mapBookLevel(level: AsterBookLevel) {
212
+ return {
213
+ price: Number(level[0]),
214
+ amount: Number(level[1])
215
+ }
216
+ }
217
+ }
218
+
219
+ class AsterBookTickerMapper implements Mapper<'aster' | 'aster-futures', BookTicker> {
220
+ constructor(private readonly exchange: 'aster' | 'aster-futures') {}
221
+
222
+ canHandle(message: AsterMessage<any>) {
223
+ return message.stream?.endsWith('@bookTicker') === true
224
+ }
225
+
226
+ getFilters(symbols?: string[]) {
227
+ return [{ channel: 'bookTicker', symbols: lowerCaseSymbols(symbols) } as const]
228
+ }
229
+
230
+ *map({ data }: AsterMessage<AsterBookTickerData>, localTimestamp: Date) {
231
+ const ticker: BookTicker = {
232
+ type: 'book_ticker',
233
+ symbol: data.s,
234
+ exchange: this.exchange,
235
+ askAmount: asNumberOrUndefined(data.A),
236
+ askPrice: asNumberOrUndefined(data.a),
237
+ bidPrice: asNumberOrUndefined(data.b),
238
+ bidAmount: asNumberOrUndefined(data.B),
239
+ timestamp: data.E !== undefined ? new Date(data.E) : localTimestamp,
240
+ localTimestamp
241
+ }
242
+
243
+ yield ticker
244
+ }
245
+ }
246
+
247
+ class AsterFuturesBookChangeMapper extends AsterBookChangeMapper {
248
+ constructor({ ignoreBookSnapshotOverlapError }: { ignoreBookSnapshotOverlapError: boolean }) {
249
+ super('aster-futures', { ignoreBookSnapshotOverlapError })
250
+ }
251
+
252
+ protected override isOutdatedDepthUpdate(depthUpdateData: AsterDepthData, lastUpdateId: number) {
253
+ return depthUpdateData.u < lastUpdateId
254
+ }
255
+
256
+ protected override hasInitialSnapshotOverlap(depthUpdateData: AsterDepthData, lastUpdateId: number) {
257
+ // Futures normally contains the snapshot id in U..u, but live and recorded
258
+ // data also continues it directly with pu == lastUpdateId. Exact-id partial
259
+ // depth and bookTicker comparisons confirmed that neither form skips a state.
260
+ return (depthUpdateData.U <= lastUpdateId && depthUpdateData.u >= lastUpdateId) || depthUpdateData.pu === lastUpdateId
261
+ }
262
+ }
263
+
264
+ class AsterFuturesDerivativeTickerMapper implements Mapper<'aster-futures', DerivativeTicker> {
265
+ private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
266
+
267
+ canHandle(message: AsterMessage<any>) {
268
+ return (
269
+ message.stream?.includes('@markPrice') === true ||
270
+ message.stream?.endsWith('@ticker') === true ||
271
+ message.stream?.endsWith('@openInterest') === true
272
+ )
273
+ }
274
+
275
+ getFilters(symbols?: string[]) {
276
+ const normalizedSymbols = lowerCaseSymbols(symbols)
277
+
278
+ return [
279
+ { channel: 'markPrice', symbols: normalizedSymbols } as const,
280
+ { channel: 'ticker', symbols: normalizedSymbols } as const,
281
+ { channel: 'openInterest', symbols: normalizedSymbols } as const
282
+ ]
283
+ }
284
+
285
+ *map(
286
+ { data }: AsterMessage<AsterFuturesMarkPriceData | AsterFuturesTickerData | AsterFuturesOpenInterestData>,
287
+ localTimestamp: Date
288
+ ): IterableIterator<DerivativeTicker> {
289
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo('s' in data ? data.s : data.symbol, 'aster-futures')
290
+
291
+ if ('e' in data) {
292
+ if (data.e === 'markPriceUpdate') {
293
+ pendingTickerInfo.updateMarkPrice(Number(data.p))
294
+ if (data.i !== undefined) {
295
+ pendingTickerInfo.updateIndexPrice(Number(data.i))
296
+ }
297
+ if (data.r !== '' && data.T !== 0) {
298
+ pendingTickerInfo.updateFundingRate(Number(data.r))
299
+ pendingTickerInfo.updateFundingTimestamp(new Date(data.T))
300
+ }
301
+ pendingTickerInfo.updateTimestamp(new Date(data.E))
302
+ }
303
+
304
+ if (data.e === '24hrTicker') {
305
+ pendingTickerInfo.updateLastPrice(Number(data.c))
306
+ pendingTickerInfo.updateTimestamp(new Date(data.E))
307
+ }
308
+ } else if ('openInterest' in data) {
309
+ pendingTickerInfo.updateOpenInterest(Number(data.openInterest))
310
+ pendingTickerInfo.updateTimestamp(new Date(data.time))
311
+ }
312
+
313
+ if (pendingTickerInfo.hasChanged()) {
314
+ yield pendingTickerInfo.getSnapshot(localTimestamp)
315
+ }
316
+ }
317
+ }
318
+
319
+ class AsterFuturesLiquidationsMapper implements Mapper<'aster-futures', Liquidation> {
320
+ canHandle(message: AsterMessage<any>) {
321
+ return message.stream?.endsWith('@forceOrder') === true
322
+ }
323
+
324
+ getFilters(symbols?: string[]) {
325
+ return [{ channel: 'forceOrder', symbols: lowerCaseSymbols(symbols) } as const]
326
+ }
327
+
328
+ *map({ data }: AsterMessage<AsterFuturesForceOrderData>, localTimestamp: Date) {
329
+ const order = data.o
330
+ if (order.X !== 'FILLED') {
331
+ return
332
+ }
333
+
334
+ const liquidation: Liquidation = {
335
+ type: 'liquidation',
336
+ symbol: order.s,
337
+ exchange: 'aster-futures',
338
+ id: undefined,
339
+ price: Number(order.p),
340
+ amount: Number(order.z),
341
+ side: order.S === 'SELL' ? 'sell' : 'buy',
342
+ timestamp: new Date(order.T),
343
+ localTimestamp
344
+ }
345
+
346
+ yield liquidation
347
+ }
348
+ }
349
+
350
+ type AsterMessage<T> = {
351
+ stream: string
352
+ data: T
353
+ }
354
+
355
+ type AsterTradeData = {
356
+ e: 'trade'
357
+ s: string
358
+ t: number
359
+ p: string
360
+ q: string
361
+ T: number
362
+ m: boolean
363
+ X?: 'INSURANCE_FUND' | 'MARKET' | 'ADL' | 'NA'
364
+ }
365
+
366
+ type AsterDepthData = {
367
+ lastUpdateId: undefined
368
+ E: number
369
+ T: number
370
+ s: string
371
+ U: number
372
+ u: number
373
+ pu: number
374
+ b: AsterBookLevel[]
375
+ a: AsterBookLevel[]
376
+ }
377
+
378
+ type AsterDepthSnapshotData = {
379
+ lastUpdateId: number
380
+ bids: AsterBookLevel[]
381
+ asks: AsterBookLevel[]
382
+ T?: number
383
+ }
384
+
385
+ type AsterBookLevel = [string, string]
386
+
387
+ type LocalDepthInfo = {
388
+ bufferedUpdates: CircularBuffer<AsterDepthData>
389
+ snapshotProcessed?: boolean
390
+ lastUpdateId?: number
391
+ validatedFirstUpdate?: boolean
392
+ }
393
+
394
+ type AsterBookTickerData = {
395
+ u: number
396
+ s: string
397
+ b: string
398
+ B: string
399
+ a: string
400
+ A: string
401
+ E?: number
402
+ }
403
+
404
+ type AsterFuturesTickerData = {
405
+ e: '24hrTicker'
406
+ E: number
407
+ s: string
408
+ c: string
409
+ }
410
+
411
+ type AsterFuturesMarkPriceData = {
412
+ e: 'markPriceUpdate'
413
+ E: number
414
+ s: string
415
+ p: string
416
+ i?: string
417
+ r: string
418
+ T: number
419
+ }
420
+
421
+ type AsterFuturesForceOrderData = {
422
+ e: 'forceOrder'
423
+ E: number
424
+ o: {
425
+ s: string
426
+ S: 'BUY' | 'SELL'
427
+ p: string
428
+ X: string
429
+ z: string
430
+ T: number
431
+ }
432
+ }
433
+
434
+ type AsterFuturesOpenInterestData = {
435
+ symbol: string
436
+ openInterest: string
437
+ time: number
438
+ }
@@ -164,28 +164,15 @@ class BinanceBookChangeMapper implements Mapper<
164
164
  // mark given symbol depth info that has snapshot processed
165
165
  symbolDepthInfo.lastUpdateId = binanceDepthSnapshotData.lastUpdateId
166
166
  symbolDepthInfo.snapshotProcessed = true
167
+ const bidsByPrice = new Map(binanceDepthSnapshotData.bids.map((level) => [level[0], level]))
168
+ const asksByPrice = new Map(binanceDepthSnapshotData.asks.map((level) => [level[0], level]))
167
169
 
168
170
  // if there were any depth updates buffered, let's proccess those by adding to or updating the initial snapshot
169
171
  for (const update of symbolDepthInfo.bufferedUpdates.items()) {
170
172
  const bookChange = this.mapBookDepthUpdate(update, localTimestamp)
171
173
  if (bookChange !== undefined) {
172
- for (const bid of update.b) {
173
- const matchingBid = binanceDepthSnapshotData.bids.find((b) => b[0] === bid[0])
174
- if (matchingBid !== undefined) {
175
- matchingBid[1] = bid[1]
176
- } else {
177
- binanceDepthSnapshotData.bids.push(bid)
178
- }
179
- }
180
-
181
- for (const ask of update.a) {
182
- const matchingAsk = binanceDepthSnapshotData.asks.find((a) => a[0] === ask[0])
183
- if (matchingAsk !== undefined) {
184
- matchingAsk[1] = ask[1]
185
- } else {
186
- binanceDepthSnapshotData.asks.push(ask)
187
- }
188
- }
174
+ this.applyLevelUpdates(bidsByPrice, update.b)
175
+ this.applyLevelUpdates(asksByPrice, update.a)
189
176
  }
190
177
  }
191
178
 
@@ -197,8 +184,8 @@ class BinanceBookChangeMapper implements Mapper<
197
184
  symbol,
198
185
  exchange: this.exchange,
199
186
  isSnapshot: true,
200
- bids: binanceDepthSnapshotData.bids.map(this.mapBookLevel),
201
- asks: binanceDepthSnapshotData.asks.map(this.mapBookLevel),
187
+ bids: [...bidsByPrice.values()].map(this.mapBookLevel),
188
+ asks: [...asksByPrice.values()].map(this.mapBookLevel),
202
189
  timestamp: binanceDepthSnapshotData.T !== undefined ? fromMicroSecondsToDate(binanceDepthSnapshotData.T) : localTimestamp,
203
190
  localTimestamp
204
191
  }
@@ -266,6 +253,16 @@ class BinanceBookChangeMapper implements Mapper<
266
253
  const amount = Number(level[1])
267
254
  return { price, amount }
268
255
  }
256
+
257
+ private applyLevelUpdates(levelsByPrice: Map<string, BinanceBookLevel>, updates: BinanceBookLevel[]) {
258
+ for (const level of updates) {
259
+ if (Number(level[1]) === 0) {
260
+ levelsByPrice.delete(level[0])
261
+ } else {
262
+ levelsByPrice.set(level[0], level)
263
+ }
264
+ }
265
+ }
269
266
  }
270
267
 
271
268
  class BinanceFuturesBookChangeMapper extends BinanceBookChangeMapper implements Mapper<'binance-futures' | 'binance-delivery', BookChange> {
@@ -281,9 +278,14 @@ class BinanceFuturesBookChangeMapper extends BinanceBookChangeMapper implements
281
278
  return
282
279
  }
283
280
 
284
- // The first processed should have U <= lastUpdateId AND u >= lastUpdateId
281
+ // Binance documents a first update that contains the snapshot boundary. Live
282
+ // USD-M and COIN-M streams can instead continue it directly with pu == lastUpdateId.
283
+ // Both relations join the buffered stream to the snapshot without an update gap.
285
284
  if (!depthContext.validatedFirstUpdate) {
286
- if (binanceDepthUpdateData.U <= lastUpdateId && binanceDepthUpdateData.u >= lastUpdateId) {
285
+ const containsSnapshotBoundary = binanceDepthUpdateData.U <= lastUpdateId && binanceDepthUpdateData.u >= lastUpdateId
286
+ const continuesSnapshotDirectly = binanceDepthUpdateData.pu === lastUpdateId
287
+
288
+ if (containsSnapshotBoundary || continuesSnapshotDirectly) {
287
289
  depthContext.validatedFirstUpdate = true
288
290
  } else {
289
291
  const message = `Book depth snaphot has no overlap with first update, update ${JSON.stringify(
@@ -434,8 +434,7 @@ type BitfinexBooks = [number, BitfinexBookLevel | BitfinexBookLevel[], number, n
434
434
  type BitfinexStatusMessage = [number, (number | undefined)[], number, number] | BitfinexHeartbeat
435
435
 
436
436
  type BitfinexLiquidation =
437
- | [number, ['pos', number, number, null, string, number, number, null, number, number, null, number][]]
438
- | BitfinexHeartbeat
437
+ [number, ['pos', number, number, null, string, number, number, null, number, number, null, number][]] | BitfinexHeartbeat
439
438
 
440
439
  type BitfinexTicker =
441
440
  | [
@@ -1,11 +1,16 @@
1
1
  import { BookChange, BookTicker, DerivativeTicker, Trade } from '../types.ts'
2
2
  import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
3
- import { exchangeMappers } from './registry.ts'
3
+ import { exchangeMappers, mapper } from './registry.ts'
4
+
5
+ const HYPERLIQUID_FAST_BOOK_SWITCH_DATE = new Date('2026-06-17T00:00:00.000Z')
4
6
 
5
7
  export const hyperliquidMappers = exchangeMappers({
6
8
  hyperliquid: {
7
9
  trades: () => new HyperliquidTradesMapper(),
8
- bookChanges: () => new HyperliquidBookChangeMapper(),
10
+ bookChanges: mapper([
11
+ { until: HYPERLIQUID_FAST_BOOK_SWITCH_DATE, use: () => new HyperliquidBookChangeMapper('l2Book') },
12
+ { use: () => new HyperliquidBookChangeMapper('fastBook') }
13
+ ]),
9
14
  derivativeTickers: () => new HyperliquidDerivativeTickerMapper(),
10
15
  bookTickers: () => new HyperliquidBookTickerMapper()
11
16
  }
@@ -78,8 +83,14 @@ function mapHyperliquidLevel(level: HyperliquidWsLevel) {
78
83
  }
79
84
  }
80
85
  class HyperliquidBookChangeMapper implements Mapper<'hyperliquid', BookChange> {
86
+ constructor(private readonly filterChannel: HyperliquidBookChannel) {}
87
+
81
88
  canHandle(message: HyperliquidWsBookMessage) {
82
- return message.channel === 'l2Book'
89
+ // Hyperliquid returns channel=l2Book for both live feeds and marks only the fast feed with data.fast=true.
90
+ // The recorder rewrites that channel to fastBook so historical data can distinguish the two feeds.
91
+ const isFastBook = message.channel === 'fastBook' || (message.channel === 'l2Book' && message.data.fast === true)
92
+
93
+ return this.filterChannel === 'fastBook' ? isFastBook : message.channel === 'l2Book' && isFastBook === false
83
94
  }
84
95
 
85
96
  getFilters(symbols?: string[]) {
@@ -87,7 +98,7 @@ class HyperliquidBookChangeMapper implements Mapper<'hyperliquid', BookChange> {
87
98
 
88
99
  return [
89
100
  {
90
- channel: 'l2Book',
101
+ channel: this.filterChannel,
91
102
  symbols
92
103
  }
93
104
  ]
@@ -206,14 +217,17 @@ type HyperliquidTradeMessage = {
206
217
  }
207
218
 
208
219
  type HyperliquidWsBookMessage = {
209
- channel: 'l2Book'
220
+ channel: HyperliquidBookChannel
210
221
  data: {
211
222
  coin: 'ATOM'
212
223
  time: 1730160007687
213
224
  levels: [HyperliquidWsLevel[], HyperliquidWsLevel[]]
225
+ fast?: boolean
214
226
  }
215
227
  }
216
228
 
229
+ type HyperliquidBookChannel = 'l2Book' | 'fastBook'
230
+
217
231
  type HyperliquidWsLevel = {
218
232
  px: string // price
219
233
  sz: string // size
@@ -1,5 +1,6 @@
1
1
  import { BookChange, BookTicker, DerivativeTicker, Exchange, Liquidation, NormalizedData, OptionSummary, Trade } from '../types.ts'
2
2
  import { ascendexMappers } from './ascendex.ts'
3
+ import { asterMappers } from './aster.ts'
3
4
  import { binanceMappers } from './binance.ts'
4
5
  import { binanceDexMappers } from './binancedex.ts'
5
6
  import { binanceEuropeanOptionsMappers } from './binanceeuropeanoptions.ts'
@@ -55,6 +56,7 @@ type Normalizer<M extends MapperCollection, U extends NormalizedData> = (<T exte
55
56
 
56
57
  const registeredMappers = mergeExchangeMappers(
57
58
  ascendexMappers,
59
+ asterMappers,
58
60
  binanceMappers,
59
61
  binanceDexMappers,
60
62
  binanceEuropeanOptionsMappers,
@@ -13,8 +13,7 @@ export type MapperCreator<T extends Exchange = any, U extends NormalizedData = N
13
13
  export type MapperCollection = Record<string, MapperCreator>
14
14
  export type MapperFactory<T extends Exchange = any, U extends NormalizedData = NormalizedData> = (localTimestamp?: Date) => Mapper<T, U>
15
15
  export type MapperDefinition<T extends Exchange = any, U extends NormalizedData = NormalizedData> =
16
- | MapperCreator<T, U>
17
- | MapperFactory<T, U>
16
+ MapperCreator<T, U> | MapperFactory<T, U>
18
17
  export type ExchangeMapperDefinitions = Partial<Record<MapperKind, MapperDefinition>>
19
18
  export type ExchangeMappers = Partial<Record<Exchange, Partial<Record<MapperKind, MapperCreator>>>>
20
19
  export type ExchangeMapperDefinitionMap = Partial<Record<Exchange, ExchangeMapperDefinitions>>