tardis-machine 18.1.0 → 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 (93) 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/handy.d.ts +13 -0
  9. package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
  10. package/node_modules/tardis-dev/dist/handy.js +5 -1
  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 +16 -0
  14. package/node_modules/tardis-dev/dist/mappers/aster.d.ts.map +1 -0
  15. package/node_modules/tardis-dev/dist/mappers/aster.js +294 -0
  16. package/node_modules/tardis-dev/dist/mappers/aster.js.map +1 -0
  17. package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
  18. package/node_modules/tardis-dev/dist/mappers/binance.js +22 -22
  19. package/node_modules/tardis-dev/dist/mappers/binance.js.map +1 -1
  20. package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
  21. package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
  22. package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
  23. package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
  24. package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
  25. package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
  26. package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +1 -1
  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 +7 -7
  30. package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
  31. package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
  32. package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
  33. package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
  34. package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
  35. package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
  36. package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +2 -2
  37. package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
  38. package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
  39. package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
  40. package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
  41. package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
  42. package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
  43. package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
  44. package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +3 -3
  45. package/node_modules/tardis-dev/dist/mappers/index.d.ts +179 -161
  46. package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
  47. package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
  48. package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
  49. package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
  50. package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
  51. package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +5 -5
  52. package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
  53. package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
  54. package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
  55. package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
  56. package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
  57. package/node_modules/tardis-dev/dist/mappers/registry.d.ts.map +1 -1
  58. package/node_modules/tardis-dev/dist/mappers/registry.js.map +1 -1
  59. package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
  60. package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
  61. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts +53 -0
  62. package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts.map +1 -0
  63. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js +271 -0
  64. package/node_modules/tardis-dev/dist/realtimefeeds/aster.js.map +1 -0
  65. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts +1 -4
  66. package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
  67. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +36 -95
  68. package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
  69. package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
  70. package/node_modules/tardis-dev/dist/realtimefeeds/index.js +3 -0
  71. package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
  72. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
  73. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +12 -3
  74. package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
  75. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts +14 -0
  76. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts.map +1 -0
  77. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js +72 -0
  78. package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js.map +1 -0
  79. package/node_modules/tardis-dev/package.json +2 -2
  80. package/node_modules/tardis-dev/src/consts.ts +18 -0
  81. package/node_modules/tardis-dev/src/handy.ts +6 -1
  82. package/node_modules/tardis-dev/src/mappers/aster.ts +438 -0
  83. package/node_modules/tardis-dev/src/mappers/binance.ts +23 -21
  84. package/node_modules/tardis-dev/src/mappers/bitfinex.ts +1 -2
  85. package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
  86. package/node_modules/tardis-dev/src/mappers/registry.ts +1 -2
  87. package/node_modules/tardis-dev/src/realtimefeeds/aster.ts +388 -0
  88. package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +59 -145
  89. package/node_modules/tardis-dev/src/realtimefeeds/index.ts +3 -0
  90. package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +12 -3
  91. package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
  92. package/package.json +3 -3
  93. package/src/ws/subscriptionsmappers.ts +3 -1
@@ -1,4 +1,6 @@
1
1
  export const EXCHANGES = [
2
+ 'aster',
3
+ 'aster-futures',
2
4
  'bitmex',
3
5
  'deribit',
4
6
  'binance-futures',
@@ -64,6 +66,20 @@ export const EXCHANGES = [
64
66
  ] as const
65
67
 
66
68
  const BINANCE_CHANNELS = ['trade', 'aggTrade', 'ticker', 'depth', 'depthSnapshot', 'bookTicker', 'recentTrades', 'borrowInterest'] as const
69
+
70
+ const ASTER_CHANNELS = ['trade', 'aggTrade', 'ticker', 'depth', 'depthSnapshot', 'bookTicker'] as const
71
+ const ASTER_FUTURES_CHANNELS = [
72
+ 'trade',
73
+ 'aggTrade',
74
+ 'ticker',
75
+ 'depth',
76
+ 'depthSnapshot',
77
+ 'bookTicker',
78
+ 'markPrice',
79
+ 'forceOrder',
80
+ 'assetIndex',
81
+ 'openInterest'
82
+ ] as const
67
83
  const BINANCE_DEX_CHANNELS = ['trades', 'marketDiff', 'depthSnapshot', 'ticker'] as const
68
84
  const BITFINEX_CHANNELS = ['trades', 'book', 'raw_book', 'ticker'] as const
69
85
 
@@ -558,6 +574,8 @@ const POLYMARKET_CHANNELS = [
558
574
  ] as const
559
575
 
560
576
  export const EXCHANGE_CHANNELS_INFO = {
577
+ aster: ASTER_CHANNELS,
578
+ 'aster-futures': ASTER_FUTURES_CHANNELS,
561
579
  bitmex: BITMEX_CHANNELS,
562
580
  coinbase: COINBASE_CHANNELS,
563
581
  'coinbase-international': COINBASE_INTERNATIONAL_CHANNELS,
@@ -76,13 +76,18 @@ export class HttpError extends Error {
76
76
  }
77
77
  }
78
78
 
79
- class HttpClientError extends Error {
79
+ export class HttpClientError extends Error {
80
+ public readonly status: number
81
+ public readonly retryAfterMS: number | undefined
82
+
80
83
  constructor(
81
84
  public readonly response: HttpResponse,
82
85
  public readonly method: string,
83
86
  public readonly url: string
84
87
  ) {
85
88
  super(`HTTP ${method} ${url} failed with status ${response.statusCode}`)
89
+ this.status = response.statusCode
90
+ this.retryAfterMS = getRetryAfterDelayMS(response.headers)
86
91
  }
87
92
  }
88
93
 
@@ -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,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>>