tardis-dev 15.3.2 → 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.
Files changed (43) hide show
  1. package/dist/consts.d.ts +2 -1
  2. package/dist/consts.d.ts.map +1 -1
  3. package/dist/consts.js +5 -2
  4. package/dist/consts.js.map +1 -1
  5. package/dist/mappers/bybit.d.ts +2 -2
  6. package/dist/mappers/bybitspot.d.ts +1 -1
  7. package/dist/mappers/cryptocom.d.ts +1 -1
  8. package/dist/mappers/huobi.d.ts +3 -3
  9. package/dist/mappers/index.d.ts +6 -0
  10. package/dist/mappers/index.d.ts.map +1 -1
  11. package/dist/mappers/index.js +9 -3
  12. package/dist/mappers/index.js.map +1 -1
  13. package/dist/mappers/lighter.d.ts +159 -0
  14. package/dist/mappers/lighter.d.ts.map +1 -0
  15. package/dist/mappers/lighter.js +173 -0
  16. package/dist/mappers/lighter.js.map +1 -0
  17. package/dist/options.d.ts +2 -0
  18. package/dist/options.d.ts.map +1 -1
  19. package/dist/options.js +1 -0
  20. package/dist/options.js.map +1 -1
  21. package/dist/realtimefeeds/index.d.ts.map +1 -1
  22. package/dist/realtimefeeds/index.js +3 -1
  23. package/dist/realtimefeeds/index.js.map +1 -1
  24. package/dist/realtimefeeds/lighter.d.ts +8 -0
  25. package/dist/realtimefeeds/lighter.d.ts.map +1 -0
  26. package/dist/realtimefeeds/lighter.js +25 -0
  27. package/dist/realtimefeeds/lighter.js.map +1 -0
  28. package/dist/replay.d.ts.map +1 -1
  29. package/dist/replay.js +1 -0
  30. package/dist/replay.js.map +1 -1
  31. package/dist/worker.d.ts +2 -0
  32. package/dist/worker.d.ts.map +1 -1
  33. package/dist/worker.js +4 -5
  34. package/dist/worker.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/consts.ts +6 -2
  37. package/src/mappers/index.ts +15 -3
  38. package/src/mappers/lighter.ts +348 -0
  39. package/src/options.ts +4 -0
  40. package/src/realtimefeeds/index.ts +3 -1
  41. package/src/realtimefeeds/lighter.ts +31 -0
  42. package/src/replay.ts +1 -0
  43. package/src/worker.ts +6 -5
@@ -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
package/src/options.ts CHANGED
@@ -10,6 +10,7 @@ const defaultOptions: Options = {
10
10
  datasetsEndpoint: 'https://datasets.tardis.dev/v1',
11
11
  cacheDir: path.join(os.tmpdir(), '.tardis-cache'),
12
12
  apiKey: '',
13
+ dataFeedCompression: 'zstd',
13
14
  _userAgent: `tardis-dev/${packageJson.version} (+https://github.com/tardis-dev/tardis-node)`
14
15
  }
15
16
 
@@ -28,5 +29,8 @@ type Options = {
28
29
  datasetsEndpoint: string
29
30
  cacheDir: string
30
31
  apiKey: string
32
+ dataFeedCompression: DataFeedCompression
31
33
  _userAgent: string
32
34
  }
35
+
36
+ export type DataFeedCompression = 'gzip' | 'zstd'
@@ -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/replay.ts CHANGED
@@ -53,6 +53,7 @@ export async function* replay<T extends Exchange, U extends boolean = false, Z e
53
53
  cacheDir: options.cacheDir,
54
54
  endpoint: options.endpoint,
55
55
  apiKey: apiKey || options.apiKey,
56
+ dataFeedCompression: options.dataFeedCompression,
56
57
  userAgent: options._userAgent,
57
58
  fromDate,
58
59
  toDate,
package/src/worker.ts CHANGED
@@ -3,12 +3,12 @@ import { existsSync } from 'node:fs'
3
3
  import pMap from 'p-map'
4
4
  import { isMainThread, parentPort, workerData } from 'worker_threads'
5
5
  import { addMinutes, download, formatDateToPath, optimizeFilters, sequence, sha256, wait, cleanTempFiles } from './handy.ts'
6
+ import type { DataFeedCompression } from './options.ts'
6
7
  import { Exchange, Filter } from './types.ts'
7
8
  const debug = dbg('tardis-dev')
8
- const ACCEPT_ENCODING = 'zstd, gzip'
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) {
@@ -92,7 +92,7 @@ async function getDataFeedSlices(payload: WorkerJobPayload) {
92
92
  }
93
93
 
94
94
  async function getDataFeedSlice(
95
- { exchange, fromDate, endpoint, apiKey, userAgent }: WorkerJobPayload,
95
+ { exchange, fromDate, endpoint, apiKey, dataFeedCompression, userAgent }: WorkerJobPayload,
96
96
  offset: number,
97
97
  filters: object[],
98
98
  cacheDir: string
@@ -104,7 +104,7 @@ async function getDataFeedSlice(
104
104
  const gzipSlicePath = `${sliceBasePath}.gz`
105
105
  const cachedSlicePath = existsSync(zstdSlicePath) ? zstdSlicePath : existsSync(gzipSlicePath) ? gzipSlicePath : undefined
106
106
 
107
- let url = `${endpoint}/data-feeds/${exchange}?from=${fromDate.toISOString()}&offset=${offset}`
107
+ let url = `${endpoint}/data-feeds/${exchange}?from=${fromDate.toISOString()}&offset=${offset}&compression=${dataFeedCompression}`
108
108
 
109
109
  if (filters.length > 0) {
110
110
  url += `&filters=${encodeURIComponent(JSON.stringify(filters))}`
@@ -119,7 +119,7 @@ async function getDataFeedSlice(
119
119
  url,
120
120
  userAgent,
121
121
  appendContentEncodingExtension: true,
122
- acceptEncoding: ACCEPT_ENCODING
122
+ acceptEncoding: dataFeedCompression === 'gzip' ? 'gzip' : 'zstd, gzip'
123
123
  })
124
124
  ).downloadPath
125
125
 
@@ -146,6 +146,7 @@ export type WorkerJobPayload = {
146
146
  cacheDir: string
147
147
  endpoint: string
148
148
  apiKey: string
149
+ dataFeedCompression: DataFeedCompression
149
150
  userAgent: string
150
151
  fromDate: Date
151
152
  toDate: Date