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.
- package/dist/ws/subscriptionsmappers.d.ts.map +1 -1
- package/dist/ws/subscriptionsmappers.js +3 -1
- package/dist/ws/subscriptionsmappers.js.map +1 -1
- package/node_modules/tardis-dev/dist/consts.d.ts +3 -1
- package/node_modules/tardis-dev/dist/consts.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/consts.js +17 -0
- package/node_modules/tardis-dev/dist/consts.js.map +1 -1
- package/node_modules/tardis-dev/dist/downloaddatasets.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/downloaddatasets.js +3 -1
- package/node_modules/tardis-dev/dist/downloaddatasets.js.map +1 -1
- package/node_modules/tardis-dev/dist/handy.d.ts +15 -1
- package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/handy.js +10 -5
- package/node_modules/tardis-dev/dist/handy.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/ascendex.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/aster.d.ts +16 -0
- package/node_modules/tardis-dev/dist/mappers/aster.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/mappers/aster.js +294 -0
- package/node_modules/tardis-dev/dist/mappers/aster.js.map +1 -0
- package/node_modules/tardis-dev/dist/mappers/binance.d.ts +18 -18
- package/node_modules/tardis-dev/dist/mappers/binance.js +22 -22
- package/node_modules/tardis-dev/dist/mappers/binance.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/binanceeuropeanoptions.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitfinex.d.ts +8 -8
- package/node_modules/tardis-dev/dist/mappers/bitflyer.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitget.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitmex.d.ts +2 -2
- package/node_modules/tardis-dev/dist/mappers/bitnomial.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/bitstamp.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/blockchaincom.d.ts +2 -2
- package/node_modules/tardis-dev/dist/mappers/bullish.d.ts +5 -5
- package/node_modules/tardis-dev/dist/mappers/bybit.d.ts +7 -7
- package/node_modules/tardis-dev/dist/mappers/bybitspot.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/coinbase.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/coinbaseinternational.d.ts +2 -2
- package/node_modules/tardis-dev/dist/mappers/coinflex.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/cryptocom.d.ts +5 -5
- package/node_modules/tardis-dev/dist/mappers/delta.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/deribit.d.ts +2 -2
- package/node_modules/tardis-dev/dist/mappers/dydx.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/dydxv4.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/ftx.d.ts +8 -8
- package/node_modules/tardis-dev/dist/mappers/gateio.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/gateiofutures.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/gemini.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/huobi.d.ts +26 -26
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.js +15 -4
- package/node_modules/tardis-dev/dist/mappers/hyperliquid.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/index.d.ts +180 -162
- package/node_modules/tardis-dev/dist/mappers/index.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/index.js +2 -1
- package/node_modules/tardis-dev/dist/mappers/index.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/kucoin.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/kucoinfutures.d.ts +4 -4
- package/node_modules/tardis-dev/dist/mappers/lighter.d.ts +5 -5
- package/node_modules/tardis-dev/dist/mappers/mexc.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/mexcfutures.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/okex.d.ts +15 -15
- package/node_modules/tardis-dev/dist/mappers/phemex.d.ts +1 -1
- package/node_modules/tardis-dev/dist/mappers/polymarket.d.ts +3 -3
- package/node_modules/tardis-dev/dist/mappers/registry.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/registry.js.map +1 -1
- package/node_modules/tardis-dev/dist/mappers/serum.d.ts +9 -9
- package/node_modules/tardis-dev/dist/mappers/upbit.d.ts +2 -2
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts +53 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.js +271 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/aster.js.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts +1 -4
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.js +36 -95
- package/node_modules/tardis-dev/dist/realtimefeeds/binance.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/index.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/index.js +3 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/index.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js +12 -3
- package/node_modules/tardis-dev/dist/realtimefeeds/realtimefeed.js.map +1 -1
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts +14 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js +72 -0
- package/node_modules/tardis-dev/dist/realtimefeeds/requestweight.js.map +1 -0
- package/node_modules/tardis-dev/dist/replay.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/replay.js +6 -4
- package/node_modules/tardis-dev/dist/replay.js.map +1 -1
- package/node_modules/tardis-dev/dist/worker.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/worker.js +5 -2
- package/node_modules/tardis-dev/dist/worker.js.map +1 -1
- package/node_modules/tardis-dev/dist/zstd.d.ts +3 -0
- package/node_modules/tardis-dev/dist/zstd.d.ts.map +1 -0
- package/node_modules/tardis-dev/dist/zstd.js +122 -0
- package/node_modules/tardis-dev/dist/zstd.js.map +1 -0
- package/node_modules/tardis-dev/package.json +2 -2
- package/node_modules/tardis-dev/src/consts.ts +18 -0
- package/node_modules/tardis-dev/src/downloaddatasets.ts +3 -1
- package/node_modules/tardis-dev/src/handy.ts +13 -5
- package/node_modules/tardis-dev/src/mappers/aster.ts +438 -0
- package/node_modules/tardis-dev/src/mappers/binance.ts +23 -21
- package/node_modules/tardis-dev/src/mappers/bitfinex.ts +1 -2
- package/node_modules/tardis-dev/src/mappers/hyperliquid.ts +19 -5
- package/node_modules/tardis-dev/src/mappers/index.ts +2 -0
- package/node_modules/tardis-dev/src/mappers/registry.ts +1 -2
- package/node_modules/tardis-dev/src/realtimefeeds/aster.ts +388 -0
- package/node_modules/tardis-dev/src/realtimefeeds/binance.ts +59 -145
- package/node_modules/tardis-dev/src/realtimefeeds/index.ts +3 -0
- package/node_modules/tardis-dev/src/realtimefeeds/realtimefeed.ts +12 -3
- package/node_modules/tardis-dev/src/realtimefeeds/requestweight.ts +91 -0
- package/node_modules/tardis-dev/src/replay.ts +6 -4
- package/node_modules/tardis-dev/src/worker.ts +5 -2
- package/node_modules/tardis-dev/src/zstd.ts +139 -0
- package/package.json +4 -4
- 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
|
-
|
|
173
|
-
|
|
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:
|
|
201
|
-
asks:
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
|
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>>
|