tardis-dev 16.0.0 → 16.1.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.
@@ -0,0 +1,348 @@
1
+ import { asNumberIfValid } from '../handy.ts'
2
+ import { BookChange, BookTicker, DerivativeTicker, Liquidation, Trade } from '../types.ts'
3
+ import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
4
+
5
+ function parseChannelMarketId(channel: string): string | undefined {
6
+ const colonIndex = channel.indexOf(':')
7
+ if (colonIndex < 0) {
8
+ return undefined
9
+ }
10
+ const suffix = channel.slice(colonIndex + 1)
11
+ if (suffix === 'all') {
12
+ return undefined
13
+ }
14
+ return suffix
15
+ }
16
+
17
+ export class LighterTradesMapper implements Mapper<'lighter', Trade> {
18
+ canHandle(message: LighterTradeMessage) {
19
+ return message.type === 'update/trade'
20
+ }
21
+
22
+ getFilters(symbols?: string[]) {
23
+ return [
24
+ {
25
+ channel: 'trade' as const,
26
+ symbols
27
+ }
28
+ ]
29
+ }
30
+
31
+ *map(message: LighterTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
32
+ for (const trade of message.trades) {
33
+ yield {
34
+ type: 'trade',
35
+ symbol: trade.market_id.toString(),
36
+ exchange: 'lighter',
37
+ id: trade.trade_id_str,
38
+ price: Number(trade.price),
39
+ amount: Number(trade.size),
40
+ side: trade.is_maker_ask ? 'buy' : 'sell',
41
+ timestamp: new Date(trade.timestamp),
42
+ localTimestamp
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ export class LighterLiquidationMapper implements Mapper<'lighter', Liquidation> {
49
+ canHandle(message: LighterTradeMessage) {
50
+ return message.type === 'update/trade'
51
+ }
52
+
53
+ getFilters(symbols?: string[]) {
54
+ return [
55
+ {
56
+ channel: 'trade' as const,
57
+ symbols
58
+ }
59
+ ]
60
+ }
61
+
62
+ *map(message: LighterTradeMessage, localTimestamp: Date): IterableIterator<Liquidation> {
63
+ for (const trade of message.liquidation_trades) {
64
+ if (trade.type !== 'liquidation') {
65
+ continue
66
+ }
67
+
68
+ yield {
69
+ type: 'liquidation',
70
+ symbol: trade.market_id.toString(),
71
+ exchange: 'lighter',
72
+ id: trade.trade_id_str,
73
+ price: Number(trade.price),
74
+ amount: Number(trade.size),
75
+ side: trade.is_maker_ask ? 'buy' : 'sell',
76
+ timestamp: new Date(trade.timestamp),
77
+ localTimestamp
78
+ }
79
+ }
80
+ }
81
+ }
82
+
83
+ export class LighterBookChangeMapper implements Mapper<'lighter', BookChange> {
84
+ canHandle(message: LighterOrderBookMessage) {
85
+ return message.type === 'subscribed/order_book' || message.type === 'update/order_book'
86
+ }
87
+
88
+ getFilters(symbols?: string[]) {
89
+ return [
90
+ {
91
+ channel: 'order_book' as const,
92
+ symbols
93
+ }
94
+ ]
95
+ }
96
+
97
+ *map(message: LighterOrderBookMessage, localTimestamp: Date): IterableIterator<BookChange> {
98
+ const symbol = parseChannelMarketId(message.channel)
99
+ if (symbol === undefined) return
100
+
101
+ yield {
102
+ type: 'book_change',
103
+ symbol,
104
+ exchange: 'lighter',
105
+ isSnapshot: message.type === 'subscribed/order_book',
106
+ bids: message.order_book.bids.map(this.mapLevel),
107
+ asks: message.order_book.asks.map(this.mapLevel),
108
+ timestamp: new Date(message.timestamp),
109
+ localTimestamp
110
+ }
111
+ }
112
+
113
+ private mapLevel(level: LighterLevel) {
114
+ return {
115
+ price: Number(level.price),
116
+ amount: Number(level.size)
117
+ }
118
+ }
119
+ }
120
+
121
+ export class LighterBookTickerMapper implements Mapper<'lighter', BookTicker> {
122
+ canHandle(message: LighterTickerMessage) {
123
+ return message.type === 'update/ticker'
124
+ }
125
+
126
+ getFilters(symbols?: string[]) {
127
+ return [
128
+ {
129
+ channel: 'ticker' as const,
130
+ symbols
131
+ }
132
+ ]
133
+ }
134
+
135
+ *map(message: LighterTickerMessage, localTimestamp: Date): IterableIterator<BookTicker> {
136
+ const symbol = parseChannelMarketId(message.channel)
137
+ if (symbol === undefined) return
138
+
139
+ yield {
140
+ type: 'book_ticker',
141
+ symbol,
142
+ exchange: 'lighter',
143
+ askAmount: asNumberIfValid(message.ticker?.a?.size),
144
+ askPrice: asNumberIfValid(message.ticker?.a?.price),
145
+ bidPrice: asNumberIfValid(message.ticker?.b?.price),
146
+ bidAmount: asNumberIfValid(message.ticker?.b?.size),
147
+ timestamp: new Date(message.timestamp),
148
+ localTimestamp
149
+ }
150
+ }
151
+ }
152
+
153
+ export class LighterDerivativeTickerMapper implements Mapper<'lighter', DerivativeTicker> {
154
+ private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
155
+
156
+ canHandle(message: LighterMarketStatsMessage) {
157
+ return message.type === 'update/market_stats'
158
+ }
159
+
160
+ getFilters(_symbols?: string[]) {
161
+ return [
162
+ {
163
+ channel: 'market_stats' as const,
164
+ symbols: []
165
+ }
166
+ ]
167
+ }
168
+
169
+ *map(message: LighterMarketStatsMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
170
+ for (const entry of this.iterateMarketStats(message)) {
171
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(entry.market_id.toString(), 'lighter')
172
+
173
+ pendingTickerInfo.updateMarkPrice(Number(entry.mark_price))
174
+ pendingTickerInfo.updateIndexPrice(Number(entry.index_price))
175
+ pendingTickerInfo.updateFundingRate(Number(entry.current_funding_rate))
176
+ pendingTickerInfo.updateLastPrice(Number(entry.last_trade_price))
177
+ pendingTickerInfo.updateOpenInterest(Number(entry.open_interest))
178
+
179
+ if (pendingTickerInfo.hasChanged()) {
180
+ pendingTickerInfo.updateTimestamp(new Date(message.timestamp))
181
+ yield pendingTickerInfo.getSnapshot(localTimestamp)
182
+ }
183
+ }
184
+ }
185
+
186
+ private *iterateMarketStats(message: LighterMarketStatsMessage): IterableIterator<LighterMarketStats> {
187
+ if (message.channel === 'market_stats:all') {
188
+ for (const key of Object.keys(message.market_stats)) {
189
+ yield message.market_stats[key]
190
+ }
191
+ return
192
+ }
193
+
194
+ yield message.market_stats
195
+ }
196
+ }
197
+
198
+ type LighterLevel = {
199
+ price: string
200
+ size: string
201
+ }
202
+
203
+ type LighterOrderBook = {
204
+ asks: LighterLevel[]
205
+ bids: LighterLevel[]
206
+ code: number
207
+ nonce: number
208
+ begin_nonce: number
209
+ offset: number
210
+ last_updated_at: number
211
+ }
212
+
213
+ type LighterOrderBookMessage = {
214
+ type: 'subscribed/order_book' | 'update/order_book'
215
+ channel: `order_book:${number}`
216
+ last_updated_at: number
217
+ offset: number
218
+ timestamp: number
219
+ order_book: LighterOrderBook
220
+ }
221
+
222
+ type LighterTicker = {
223
+ s: string
224
+ a?: Partial<LighterLevel>
225
+ b?: Partial<LighterLevel>
226
+ last_updated_at: number
227
+ }
228
+
229
+ type LighterTickerMessage = {
230
+ type: 'subscribed/ticker' | 'update/ticker'
231
+ channel: `ticker:${number}`
232
+ last_updated_at: number
233
+ nonce: number
234
+ ticker: LighterTicker
235
+ timestamp: number
236
+ }
237
+
238
+ type LighterTrade = {
239
+ trade_id: number
240
+ trade_id_str: string
241
+ tx_hash: string
242
+ type: 'trade' | 'liquidation' | 'deleverage' | 'market-settlement'
243
+ market_id: number
244
+ size: string
245
+ price: string
246
+ usd_amount: string
247
+ ask_id: number
248
+ ask_id_str: string
249
+ bid_id: number
250
+ bid_id_str: string
251
+ ask_client_id: number
252
+ ask_client_id_str: string
253
+ bid_client_id: number
254
+ bid_client_id_str: string
255
+ ask_account_id: number
256
+ bid_account_id: number
257
+ is_maker_ask: boolean
258
+ block_height: number
259
+ timestamp: number
260
+ taker_fee?: number
261
+ taker_position_size_before?: string
262
+ taker_entry_quote_before?: string
263
+ taker_initial_margin_fraction_before?: number
264
+ taker_position_sign_changed?: boolean
265
+ taker_allocated_margin_usdc_before?: number
266
+ maker_fee?: number
267
+ maker_position_size_before?: string
268
+ maker_entry_quote_before?: string
269
+ maker_initial_margin_fraction_before?: number
270
+ maker_position_sign_changed?: boolean
271
+ transaction_time: number
272
+ ask_account_pnl?: string
273
+ bid_account_pnl?: string
274
+ }
275
+
276
+ type LighterTradeMessage = {
277
+ type: 'subscribed/trade' | 'update/trade'
278
+ channel: `trade:${number}`
279
+ nonce: number
280
+ trades: LighterTrade[]
281
+ liquidation_trades: LighterTrade[]
282
+ }
283
+
284
+ type LighterMarketStats = {
285
+ symbol: string
286
+ market_id: number
287
+ index_price: string
288
+ mark_price: string
289
+ mid_price: string
290
+ open_interest: string
291
+ open_interest_limit: string
292
+ funding_clamp_small: string
293
+ funding_clamp_big: string
294
+ last_trade_price: string
295
+ current_funding_rate: string
296
+ funding_rate: string
297
+ funding_timestamp: number
298
+ daily_base_token_volume: number
299
+ daily_quote_token_volume: number
300
+ daily_price_low: number
301
+ daily_price_high: number
302
+ daily_price_change: number
303
+ }
304
+
305
+ type LighterMarketStatsAllMessage = {
306
+ type: 'subscribed/market_stats' | 'update/market_stats'
307
+ channel: 'market_stats:all'
308
+ timestamp: number
309
+ market_stats: Record<string, LighterMarketStats>
310
+ }
311
+
312
+ type LighterMarketStatsMarketIdMessage = {
313
+ type: 'subscribed/market_stats' | 'update/market_stats'
314
+ channel: `market_stats:${number}`
315
+ timestamp: number
316
+ market_stats: LighterMarketStats
317
+ }
318
+
319
+ type LighterMarketStatsMessage = LighterMarketStatsAllMessage | LighterMarketStatsMarketIdMessage
320
+
321
+ type LighterSpotMarketStats = {
322
+ symbol: string
323
+ market_id: number
324
+ index_price: string
325
+ mid_price: string
326
+ last_trade_price: string
327
+ daily_base_token_volume: number
328
+ daily_quote_token_volume: number
329
+ daily_price_low: number
330
+ daily_price_high: number
331
+ daily_price_change: number
332
+ }
333
+
334
+ type LighterSpotMarketStatsAllMessage = {
335
+ type: 'subscribed/spot_market_stats' | 'update/spot_market_stats'
336
+ channel: 'spot_market_stats:all'
337
+ timestamp: number
338
+ spot_market_stats: Record<string, LighterSpotMarketStats>
339
+ }
340
+
341
+ type LighterSpotMarketStatsMarketIdMessage = {
342
+ type: 'subscribed/spot_market_stats' | 'update/spot_market_stats'
343
+ channel: `spot_market_stats:${number}`
344
+ timestamp: number
345
+ spot_market_stats: LighterSpotMarketStats
346
+ }
347
+
348
+ type LighterSpotMarketStatsMessage = LighterSpotMarketStatsAllMessage | LighterSpotMarketStatsMarketIdMessage
@@ -52,6 +52,7 @@ import { DydxV4RealTimeFeed } from './dydx_v4.ts'
52
52
  import { BitgetFuturesRealTimeFeed, BitgetRealTimeFeed } from './bitget.ts'
53
53
  import { CoinbaseInternationalRealTimeFeed } from './coinbaseinternational.ts'
54
54
  import { HyperliquidRealTimeFeed } from './hyperliquid.ts'
55
+ import { LighterRealTimeFeed } from './lighter.ts'
55
56
 
56
57
  export * from './realtimefeed.ts'
57
58
 
@@ -114,7 +115,8 @@ const realTimeFeedsMap: {
114
115
  bitget: BitgetRealTimeFeed,
115
116
  'bitget-futures': BitgetFuturesRealTimeFeed,
116
117
  'coinbase-international': CoinbaseInternationalRealTimeFeed,
117
- hyperliquid: HyperliquidRealTimeFeed
118
+ hyperliquid: HyperliquidRealTimeFeed,
119
+ lighter: LighterRealTimeFeed
118
120
  }
119
121
 
120
122
  export function getRealTimeFeedFactory(exchange: Exchange): RealTimeFeed {
@@ -0,0 +1,31 @@
1
+ import { Filter } from '../types.ts'
2
+ import { RealTimeFeedBase } from './realtimefeed.ts'
3
+
4
+ export class LighterRealTimeFeed extends RealTimeFeedBase {
5
+ protected wssURL = 'wss://mainnet.zklighter.elliot.ai/stream'
6
+
7
+ protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
8
+ return filters.flatMap((filter) => {
9
+ if (filter.channel === 'market_stats') {
10
+ return [{ type: 'subscribe', channel: 'market_stats/all' }]
11
+ }
12
+
13
+ if (filter.channel === 'spot_market_stats') {
14
+ return [{ type: 'subscribe', channel: 'spot_market_stats/all' }]
15
+ }
16
+
17
+ if (!filter.symbols || filter.symbols.length === 0) {
18
+ throw new Error('LighterRealTimeFeed requires explicitly specified symbols when subscribing to live feed')
19
+ }
20
+
21
+ return filter.symbols.map((marketId) => ({
22
+ type: 'subscribe',
23
+ channel: `${filter.channel}/${marketId}`
24
+ }))
25
+ })
26
+ }
27
+
28
+ protected messageIsError(message: any): boolean {
29
+ return message.error !== undefined
30
+ }
31
+ }
package/src/worker.ts CHANGED
@@ -8,7 +8,7 @@ import { Exchange, Filter } from './types.ts'
8
8
  const debug = dbg('tardis-dev')
9
9
 
10
10
  if (isMainThread) {
11
- debug('existing, worker is not meant to run in main thread')
11
+ debug('current worker is not meant to run in main thread')
12
12
  } else {
13
13
  parentPort!.on('message', (signal: WorkerSignal) => {
14
14
  if (signal === WorkerSignal.BEFORE_TERMINATE) {