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.
- 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/handy.d.ts +13 -0
- package/node_modules/tardis-dev/dist/handy.d.ts.map +1 -1
- package/node_modules/tardis-dev/dist/handy.js +5 -1
- 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 +3 -3
- package/node_modules/tardis-dev/dist/mappers/index.d.ts +179 -161
- 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/package.json +2 -2
- package/node_modules/tardis-dev/src/consts.ts +18 -0
- package/node_modules/tardis-dev/src/handy.ts +6 -1
- 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/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/package.json +3 -3
- 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
|
-
|
|
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,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>>
|