tardis-dev 16.0.0 → 16.1.1

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,352 @@
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
+ if (!Array.isArray(message.liquidation_trades)) {
64
+ return
65
+ }
66
+
67
+ for (const trade of message.liquidation_trades) {
68
+ if (trade.type !== 'liquidation') {
69
+ continue
70
+ }
71
+
72
+ yield {
73
+ type: 'liquidation',
74
+ symbol: trade.market_id.toString(),
75
+ exchange: 'lighter',
76
+ id: trade.trade_id_str,
77
+ price: Number(trade.price),
78
+ amount: Number(trade.size),
79
+ side: trade.is_maker_ask ? 'buy' : 'sell',
80
+ timestamp: new Date(trade.timestamp),
81
+ localTimestamp
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ export class LighterBookChangeMapper implements Mapper<'lighter', BookChange> {
88
+ canHandle(message: LighterOrderBookMessage) {
89
+ return message.type === 'subscribed/order_book' || message.type === 'update/order_book'
90
+ }
91
+
92
+ getFilters(symbols?: string[]) {
93
+ return [
94
+ {
95
+ channel: 'order_book' as const,
96
+ symbols
97
+ }
98
+ ]
99
+ }
100
+
101
+ *map(message: LighterOrderBookMessage, localTimestamp: Date): IterableIterator<BookChange> {
102
+ const symbol = parseChannelMarketId(message.channel)
103
+ if (symbol === undefined) return
104
+
105
+ yield {
106
+ type: 'book_change',
107
+ symbol,
108
+ exchange: 'lighter',
109
+ isSnapshot: message.type === 'subscribed/order_book',
110
+ bids: message.order_book.bids.map(this.mapLevel),
111
+ asks: message.order_book.asks.map(this.mapLevel),
112
+ timestamp: new Date(message.timestamp),
113
+ localTimestamp
114
+ }
115
+ }
116
+
117
+ private mapLevel(level: LighterLevel) {
118
+ return {
119
+ price: Number(level.price),
120
+ amount: Number(level.size)
121
+ }
122
+ }
123
+ }
124
+
125
+ export class LighterBookTickerMapper implements Mapper<'lighter', BookTicker> {
126
+ canHandle(message: LighterTickerMessage) {
127
+ return message.type === 'update/ticker'
128
+ }
129
+
130
+ getFilters(symbols?: string[]) {
131
+ return [
132
+ {
133
+ channel: 'ticker' as const,
134
+ symbols
135
+ }
136
+ ]
137
+ }
138
+
139
+ *map(message: LighterTickerMessage, localTimestamp: Date): IterableIterator<BookTicker> {
140
+ const symbol = parseChannelMarketId(message.channel)
141
+ if (symbol === undefined) return
142
+
143
+ yield {
144
+ type: 'book_ticker',
145
+ symbol,
146
+ exchange: 'lighter',
147
+ askAmount: asNumberIfValid(message.ticker?.a?.size),
148
+ askPrice: asNumberIfValid(message.ticker?.a?.price),
149
+ bidPrice: asNumberIfValid(message.ticker?.b?.price),
150
+ bidAmount: asNumberIfValid(message.ticker?.b?.size),
151
+ timestamp: new Date(message.timestamp),
152
+ localTimestamp
153
+ }
154
+ }
155
+ }
156
+
157
+ export class LighterDerivativeTickerMapper implements Mapper<'lighter', DerivativeTicker> {
158
+ private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
159
+
160
+ canHandle(message: LighterMarketStatsMessage) {
161
+ return message.type === 'update/market_stats'
162
+ }
163
+
164
+ getFilters(_symbols?: string[]) {
165
+ return [
166
+ {
167
+ channel: 'market_stats' as const,
168
+ symbols: []
169
+ }
170
+ ]
171
+ }
172
+
173
+ *map(message: LighterMarketStatsMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
174
+ for (const entry of this.iterateMarketStats(message)) {
175
+ const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(entry.market_id.toString(), 'lighter')
176
+
177
+ pendingTickerInfo.updateMarkPrice(Number(entry.mark_price))
178
+ pendingTickerInfo.updateIndexPrice(Number(entry.index_price))
179
+ pendingTickerInfo.updateFundingRate(Number(entry.current_funding_rate))
180
+ pendingTickerInfo.updateLastPrice(Number(entry.last_trade_price))
181
+ pendingTickerInfo.updateOpenInterest(Number(entry.open_interest))
182
+
183
+ if (pendingTickerInfo.hasChanged()) {
184
+ pendingTickerInfo.updateTimestamp(new Date(message.timestamp))
185
+ yield pendingTickerInfo.getSnapshot(localTimestamp)
186
+ }
187
+ }
188
+ }
189
+
190
+ private *iterateMarketStats(message: LighterMarketStatsMessage): IterableIterator<LighterMarketStats> {
191
+ if (message.channel === 'market_stats:all') {
192
+ for (const key of Object.keys(message.market_stats)) {
193
+ yield message.market_stats[key]
194
+ }
195
+ return
196
+ }
197
+
198
+ yield message.market_stats
199
+ }
200
+ }
201
+
202
+ type LighterLevel = {
203
+ price: string
204
+ size: string
205
+ }
206
+
207
+ type LighterOrderBook = {
208
+ asks: LighterLevel[]
209
+ bids: LighterLevel[]
210
+ code: number
211
+ nonce: number
212
+ begin_nonce: number
213
+ offset: number
214
+ last_updated_at: number
215
+ }
216
+
217
+ type LighterOrderBookMessage = {
218
+ type: 'subscribed/order_book' | 'update/order_book'
219
+ channel: `order_book:${number}`
220
+ last_updated_at: number
221
+ offset: number
222
+ timestamp: number
223
+ order_book: LighterOrderBook
224
+ }
225
+
226
+ type LighterTicker = {
227
+ s: string
228
+ a?: Partial<LighterLevel>
229
+ b?: Partial<LighterLevel>
230
+ last_updated_at: number
231
+ }
232
+
233
+ type LighterTickerMessage = {
234
+ type: 'subscribed/ticker' | 'update/ticker'
235
+ channel: `ticker:${number}`
236
+ last_updated_at: number
237
+ nonce: number
238
+ ticker: LighterTicker
239
+ timestamp: number
240
+ }
241
+
242
+ type LighterTrade = {
243
+ trade_id: number
244
+ trade_id_str: string
245
+ tx_hash: string
246
+ type: 'trade' | 'liquidation' | 'deleverage' | 'market-settlement'
247
+ market_id: number
248
+ size: string
249
+ price: string
250
+ usd_amount: string
251
+ ask_id: number
252
+ ask_id_str: string
253
+ bid_id: number
254
+ bid_id_str: string
255
+ ask_client_id: number
256
+ ask_client_id_str: string
257
+ bid_client_id: number
258
+ bid_client_id_str: string
259
+ ask_account_id: number
260
+ bid_account_id: number
261
+ is_maker_ask: boolean
262
+ block_height: number
263
+ timestamp: number
264
+ taker_fee?: number
265
+ taker_position_size_before?: string
266
+ taker_entry_quote_before?: string
267
+ taker_initial_margin_fraction_before?: number
268
+ taker_position_sign_changed?: boolean
269
+ taker_allocated_margin_usdc_before?: number
270
+ maker_fee?: number
271
+ maker_position_size_before?: string
272
+ maker_entry_quote_before?: string
273
+ maker_initial_margin_fraction_before?: number
274
+ maker_position_sign_changed?: boolean
275
+ transaction_time: number
276
+ ask_account_pnl?: string
277
+ bid_account_pnl?: string
278
+ }
279
+
280
+ type LighterTradeMessage = {
281
+ type: 'subscribed/trade' | 'update/trade'
282
+ channel: `trade:${number}`
283
+ nonce: number
284
+ trades: LighterTrade[]
285
+ liquidation_trades?: LighterTrade[]
286
+ }
287
+
288
+ type LighterMarketStats = {
289
+ symbol: string
290
+ market_id: number
291
+ index_price: string
292
+ mark_price: string
293
+ mid_price: string
294
+ open_interest: string
295
+ open_interest_limit: string
296
+ funding_clamp_small: string
297
+ funding_clamp_big: string
298
+ last_trade_price: string
299
+ current_funding_rate: string
300
+ funding_rate: string
301
+ funding_timestamp: number
302
+ daily_base_token_volume: number
303
+ daily_quote_token_volume: number
304
+ daily_price_low: number
305
+ daily_price_high: number
306
+ daily_price_change: number
307
+ }
308
+
309
+ type LighterMarketStatsAllMessage = {
310
+ type: 'subscribed/market_stats' | 'update/market_stats'
311
+ channel: 'market_stats:all'
312
+ timestamp: number
313
+ market_stats: Record<string, LighterMarketStats>
314
+ }
315
+
316
+ type LighterMarketStatsMarketIdMessage = {
317
+ type: 'subscribed/market_stats' | 'update/market_stats'
318
+ channel: `market_stats:${number}`
319
+ timestamp: number
320
+ market_stats: LighterMarketStats
321
+ }
322
+
323
+ type LighterMarketStatsMessage = LighterMarketStatsAllMessage | LighterMarketStatsMarketIdMessage
324
+
325
+ type LighterSpotMarketStats = {
326
+ symbol: string
327
+ market_id: number
328
+ index_price: string
329
+ mid_price: string
330
+ last_trade_price: string
331
+ daily_base_token_volume: number
332
+ daily_quote_token_volume: number
333
+ daily_price_low: number
334
+ daily_price_high: number
335
+ daily_price_change: number
336
+ }
337
+
338
+ type LighterSpotMarketStatsAllMessage = {
339
+ type: 'subscribed/spot_market_stats' | 'update/spot_market_stats'
340
+ channel: 'spot_market_stats:all'
341
+ timestamp: number
342
+ spot_market_stats: Record<string, LighterSpotMarketStats>
343
+ }
344
+
345
+ type LighterSpotMarketStatsMarketIdMessage = {
346
+ type: 'subscribed/spot_market_stats' | 'update/spot_market_stats'
347
+ channel: `spot_market_stats:${number}`
348
+ timestamp: number
349
+ spot_market_stats: LighterSpotMarketStats
350
+ }
351
+
352
+ 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) {