oxarchive 0.4.4__tar.gz → 0.5.3__tar.gz
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.
- {oxarchive-0.4.4 → oxarchive-0.5.3}/PKG-INFO +116 -15
- {oxarchive-0.4.4 → oxarchive-0.5.3}/README.md +115 -14
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/__init__.py +5 -1
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/exchanges.py +11 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/resources/__init__.py +4 -0
- oxarchive-0.5.3/oxarchive/resources/candles.py +121 -0
- oxarchive-0.5.3/oxarchive/resources/liquidations.py +198 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/resources/trades.py +9 -5
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/types.py +119 -2
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/websocket.py +42 -3
- {oxarchive-0.4.4 → oxarchive-0.5.3}/pyproject.toml +1 -1
- {oxarchive-0.4.4 → oxarchive-0.5.3}/.gitignore +0 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/client.py +0 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/http.py +0 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/resources/funding.py +0 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/resources/instruments.py +0 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/resources/openinterest.py +0 -0
- {oxarchive-0.4.4 → oxarchive-0.5.3}/oxarchive/resources/orderbook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oxarchive
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Official Python SDK for 0xarchive - Hyperliquid Historical Data API
|
|
5
5
|
Project-URL: Homepage, https://0xarchive.io
|
|
6
6
|
Project-URL: Documentation, https://0xarchive.io/docs/sdks
|
|
@@ -211,9 +211,6 @@ history = client.lighter.orderbook.history(
|
|
|
211
211
|
The trades API uses cursor-based pagination for efficient retrieval of large datasets.
|
|
212
212
|
|
|
213
213
|
```python
|
|
214
|
-
# Get recent trades
|
|
215
|
-
recent = client.hyperliquid.trades.recent("BTC", limit=100)
|
|
216
|
-
|
|
217
214
|
# Get trade history with cursor-based pagination
|
|
218
215
|
result = client.hyperliquid.trades.list("ETH", start="2024-01-01", end="2024-01-02", limit=1000)
|
|
219
216
|
trades = result.data
|
|
@@ -232,8 +229,7 @@ while result.next_cursor:
|
|
|
232
229
|
# Filter by side
|
|
233
230
|
buys = client.hyperliquid.trades.list("BTC", start=..., end=..., side="buy")
|
|
234
231
|
|
|
235
|
-
# Async
|
|
236
|
-
recent = await client.hyperliquid.trades.arecent("BTC")
|
|
232
|
+
# Async version
|
|
237
233
|
result = await client.hyperliquid.trades.alist("ETH", start=..., end=...)
|
|
238
234
|
```
|
|
239
235
|
|
|
@@ -317,6 +313,57 @@ current = await client.hyperliquid.open_interest.acurrent("BTC")
|
|
|
317
313
|
history = await client.hyperliquid.open_interest.ahistory("ETH", start=..., end=...)
|
|
318
314
|
```
|
|
319
315
|
|
|
316
|
+
### Candles (OHLCV)
|
|
317
|
+
|
|
318
|
+
Get historical OHLCV candle data aggregated from trades.
|
|
319
|
+
|
|
320
|
+
```python
|
|
321
|
+
# Get candle history (start is required)
|
|
322
|
+
candles = client.hyperliquid.candles.history(
|
|
323
|
+
"BTC",
|
|
324
|
+
start="2024-01-01",
|
|
325
|
+
end="2024-01-02",
|
|
326
|
+
interval="1h", # 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
|
|
327
|
+
limit=100
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Iterate through candles
|
|
331
|
+
for candle in candles.data:
|
|
332
|
+
print(f"{candle.timestamp}: O={candle.open} H={candle.high} L={candle.low} C={candle.close} V={candle.volume}")
|
|
333
|
+
|
|
334
|
+
# Cursor-based pagination for large datasets
|
|
335
|
+
result = client.hyperliquid.candles.history("BTC", start=..., end=..., interval="1m", limit=1000)
|
|
336
|
+
while result.next_cursor:
|
|
337
|
+
result = client.hyperliquid.candles.history(
|
|
338
|
+
"BTC", start=..., end=..., interval="1m",
|
|
339
|
+
cursor=result.next_cursor, limit=1000
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Lighter.xyz candles
|
|
343
|
+
lighter_candles = client.lighter.candles.history(
|
|
344
|
+
"BTC",
|
|
345
|
+
start="2024-01-01",
|
|
346
|
+
end="2024-01-02",
|
|
347
|
+
interval="15m"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# Async versions
|
|
351
|
+
candles = await client.hyperliquid.candles.ahistory("BTC", start=..., end=..., interval="1h")
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
#### Available Intervals
|
|
355
|
+
|
|
356
|
+
| Interval | Description |
|
|
357
|
+
|----------|-------------|
|
|
358
|
+
| `1m` | 1 minute |
|
|
359
|
+
| `5m` | 5 minutes |
|
|
360
|
+
| `15m` | 15 minutes |
|
|
361
|
+
| `30m` | 30 minutes |
|
|
362
|
+
| `1h` | 1 hour (default) |
|
|
363
|
+
| `4h` | 4 hours |
|
|
364
|
+
| `1d` | 1 day |
|
|
365
|
+
| `1w` | 1 week |
|
|
366
|
+
|
|
320
367
|
### Legacy API (Deprecated)
|
|
321
368
|
|
|
322
369
|
The following legacy methods are deprecated and will be removed in v2.0. They default to Hyperliquid data:
|
|
@@ -418,6 +465,19 @@ async def main():
|
|
|
418
465
|
speed=10 # Optional, defaults to 1x
|
|
419
466
|
)
|
|
420
467
|
|
|
468
|
+
# Lighter.xyz replay with granularity (tier restrictions apply)
|
|
469
|
+
await ws.replay(
|
|
470
|
+
"orderbook", "BTC",
|
|
471
|
+
start=int(time.time() * 1000) - 86400000,
|
|
472
|
+
speed=10,
|
|
473
|
+
granularity="10s" # Options: 'checkpoint', '30s', '10s', '1s', 'tick'
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Handle tick-level data (granularity='tick', Enterprise tier)
|
|
477
|
+
ws.on_historical_tick_data(lambda coin, checkpoint, deltas:
|
|
478
|
+
print(f"Checkpoint: {len(checkpoint['bids'])} bids, Deltas: {len(deltas)}")
|
|
479
|
+
)
|
|
480
|
+
|
|
421
481
|
# Control playback
|
|
422
482
|
await ws.replay_pause()
|
|
423
483
|
await ws.replay_resume()
|
|
@@ -463,6 +523,14 @@ async def main():
|
|
|
463
523
|
batch_size=1000 # Optional, defaults to 1000
|
|
464
524
|
)
|
|
465
525
|
|
|
526
|
+
# Lighter.xyz stream with granularity (tier restrictions apply)
|
|
527
|
+
await ws.stream(
|
|
528
|
+
"orderbook", "BTC",
|
|
529
|
+
start=int(time.time() * 1000) - 3600000,
|
|
530
|
+
end=int(time.time() * 1000),
|
|
531
|
+
granularity="10s" # Options: 'checkpoint', '30s', '10s', '1s', 'tick'
|
|
532
|
+
)
|
|
533
|
+
|
|
466
534
|
# Stop if needed
|
|
467
535
|
await ws.stream_stop()
|
|
468
536
|
|
|
@@ -484,12 +552,43 @@ ws = OxArchiveWs(WsOptions(
|
|
|
484
552
|
|
|
485
553
|
### Available Channels
|
|
486
554
|
|
|
487
|
-
| Channel | Description | Requires Coin |
|
|
488
|
-
|
|
489
|
-
| `orderbook` | L2 order book updates | Yes |
|
|
490
|
-
| `trades` | Trade/fill updates | Yes |
|
|
491
|
-
| `
|
|
492
|
-
| `
|
|
555
|
+
| Channel | Description | Requires Coin | Historical Support |
|
|
556
|
+
|---------|-------------|---------------|-------------------|
|
|
557
|
+
| `orderbook` | L2 order book updates | Yes | Yes |
|
|
558
|
+
| `trades` | Trade/fill updates | Yes | Yes |
|
|
559
|
+
| `candles` | OHLCV candle data | Yes | Yes (replay/stream only) |
|
|
560
|
+
| `ticker` | Price and 24h volume | Yes | Real-time only |
|
|
561
|
+
| `all_tickers` | All market tickers | No | Real-time only |
|
|
562
|
+
|
|
563
|
+
#### Candle Replay/Stream
|
|
564
|
+
|
|
565
|
+
```python
|
|
566
|
+
# Replay candles at 10x speed
|
|
567
|
+
await ws.replay(
|
|
568
|
+
"candles", "BTC",
|
|
569
|
+
start=int(time.time() * 1000) - 86400000,
|
|
570
|
+
end=int(time.time() * 1000),
|
|
571
|
+
speed=10,
|
|
572
|
+
interval="15m" # 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
# Bulk stream candles
|
|
576
|
+
await ws.stream(
|
|
577
|
+
"candles", "ETH",
|
|
578
|
+
start=int(time.time() * 1000) - 3600000,
|
|
579
|
+
end=int(time.time() * 1000),
|
|
580
|
+
batch_size=1000,
|
|
581
|
+
interval="1h"
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
# Lighter.xyz candles
|
|
585
|
+
await ws.replay(
|
|
586
|
+
"lighter_candles", "BTC",
|
|
587
|
+
start=...,
|
|
588
|
+
speed=10,
|
|
589
|
+
interval="5m"
|
|
590
|
+
)
|
|
591
|
+
```
|
|
493
592
|
|
|
494
593
|
## Timestamp Formats
|
|
495
594
|
|
|
@@ -538,9 +637,11 @@ from oxarchive.resources.trades import CursorResponse
|
|
|
538
637
|
|
|
539
638
|
client = Client(api_key="ox_your_api_key")
|
|
540
639
|
|
|
541
|
-
orderbook: OrderBook = client.orderbook.get("BTC")
|
|
542
|
-
|
|
543
|
-
|
|
640
|
+
orderbook: OrderBook = client.hyperliquid.orderbook.get("BTC")
|
|
641
|
+
result: CursorResponse = client.hyperliquid.trades.list("BTC", start=..., end=...)
|
|
642
|
+
|
|
643
|
+
# Lighter has real-time data, so recent() is available
|
|
644
|
+
recent: list[Trade] = client.lighter.trades.recent("BTC")
|
|
544
645
|
|
|
545
646
|
# Lighter granularity type hint
|
|
546
647
|
granularity: LighterGranularity = "10s"
|
|
@@ -174,9 +174,6 @@ history = client.lighter.orderbook.history(
|
|
|
174
174
|
The trades API uses cursor-based pagination for efficient retrieval of large datasets.
|
|
175
175
|
|
|
176
176
|
```python
|
|
177
|
-
# Get recent trades
|
|
178
|
-
recent = client.hyperliquid.trades.recent("BTC", limit=100)
|
|
179
|
-
|
|
180
177
|
# Get trade history with cursor-based pagination
|
|
181
178
|
result = client.hyperliquid.trades.list("ETH", start="2024-01-01", end="2024-01-02", limit=1000)
|
|
182
179
|
trades = result.data
|
|
@@ -195,8 +192,7 @@ while result.next_cursor:
|
|
|
195
192
|
# Filter by side
|
|
196
193
|
buys = client.hyperliquid.trades.list("BTC", start=..., end=..., side="buy")
|
|
197
194
|
|
|
198
|
-
# Async
|
|
199
|
-
recent = await client.hyperliquid.trades.arecent("BTC")
|
|
195
|
+
# Async version
|
|
200
196
|
result = await client.hyperliquid.trades.alist("ETH", start=..., end=...)
|
|
201
197
|
```
|
|
202
198
|
|
|
@@ -280,6 +276,57 @@ current = await client.hyperliquid.open_interest.acurrent("BTC")
|
|
|
280
276
|
history = await client.hyperliquid.open_interest.ahistory("ETH", start=..., end=...)
|
|
281
277
|
```
|
|
282
278
|
|
|
279
|
+
### Candles (OHLCV)
|
|
280
|
+
|
|
281
|
+
Get historical OHLCV candle data aggregated from trades.
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
# Get candle history (start is required)
|
|
285
|
+
candles = client.hyperliquid.candles.history(
|
|
286
|
+
"BTC",
|
|
287
|
+
start="2024-01-01",
|
|
288
|
+
end="2024-01-02",
|
|
289
|
+
interval="1h", # 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
|
|
290
|
+
limit=100
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Iterate through candles
|
|
294
|
+
for candle in candles.data:
|
|
295
|
+
print(f"{candle.timestamp}: O={candle.open} H={candle.high} L={candle.low} C={candle.close} V={candle.volume}")
|
|
296
|
+
|
|
297
|
+
# Cursor-based pagination for large datasets
|
|
298
|
+
result = client.hyperliquid.candles.history("BTC", start=..., end=..., interval="1m", limit=1000)
|
|
299
|
+
while result.next_cursor:
|
|
300
|
+
result = client.hyperliquid.candles.history(
|
|
301
|
+
"BTC", start=..., end=..., interval="1m",
|
|
302
|
+
cursor=result.next_cursor, limit=1000
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
# Lighter.xyz candles
|
|
306
|
+
lighter_candles = client.lighter.candles.history(
|
|
307
|
+
"BTC",
|
|
308
|
+
start="2024-01-01",
|
|
309
|
+
end="2024-01-02",
|
|
310
|
+
interval="15m"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Async versions
|
|
314
|
+
candles = await client.hyperliquid.candles.ahistory("BTC", start=..., end=..., interval="1h")
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
#### Available Intervals
|
|
318
|
+
|
|
319
|
+
| Interval | Description |
|
|
320
|
+
|----------|-------------|
|
|
321
|
+
| `1m` | 1 minute |
|
|
322
|
+
| `5m` | 5 minutes |
|
|
323
|
+
| `15m` | 15 minutes |
|
|
324
|
+
| `30m` | 30 minutes |
|
|
325
|
+
| `1h` | 1 hour (default) |
|
|
326
|
+
| `4h` | 4 hours |
|
|
327
|
+
| `1d` | 1 day |
|
|
328
|
+
| `1w` | 1 week |
|
|
329
|
+
|
|
283
330
|
### Legacy API (Deprecated)
|
|
284
331
|
|
|
285
332
|
The following legacy methods are deprecated and will be removed in v2.0. They default to Hyperliquid data:
|
|
@@ -381,6 +428,19 @@ async def main():
|
|
|
381
428
|
speed=10 # Optional, defaults to 1x
|
|
382
429
|
)
|
|
383
430
|
|
|
431
|
+
# Lighter.xyz replay with granularity (tier restrictions apply)
|
|
432
|
+
await ws.replay(
|
|
433
|
+
"orderbook", "BTC",
|
|
434
|
+
start=int(time.time() * 1000) - 86400000,
|
|
435
|
+
speed=10,
|
|
436
|
+
granularity="10s" # Options: 'checkpoint', '30s', '10s', '1s', 'tick'
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Handle tick-level data (granularity='tick', Enterprise tier)
|
|
440
|
+
ws.on_historical_tick_data(lambda coin, checkpoint, deltas:
|
|
441
|
+
print(f"Checkpoint: {len(checkpoint['bids'])} bids, Deltas: {len(deltas)}")
|
|
442
|
+
)
|
|
443
|
+
|
|
384
444
|
# Control playback
|
|
385
445
|
await ws.replay_pause()
|
|
386
446
|
await ws.replay_resume()
|
|
@@ -426,6 +486,14 @@ async def main():
|
|
|
426
486
|
batch_size=1000 # Optional, defaults to 1000
|
|
427
487
|
)
|
|
428
488
|
|
|
489
|
+
# Lighter.xyz stream with granularity (tier restrictions apply)
|
|
490
|
+
await ws.stream(
|
|
491
|
+
"orderbook", "BTC",
|
|
492
|
+
start=int(time.time() * 1000) - 3600000,
|
|
493
|
+
end=int(time.time() * 1000),
|
|
494
|
+
granularity="10s" # Options: 'checkpoint', '30s', '10s', '1s', 'tick'
|
|
495
|
+
)
|
|
496
|
+
|
|
429
497
|
# Stop if needed
|
|
430
498
|
await ws.stream_stop()
|
|
431
499
|
|
|
@@ -447,12 +515,43 @@ ws = OxArchiveWs(WsOptions(
|
|
|
447
515
|
|
|
448
516
|
### Available Channels
|
|
449
517
|
|
|
450
|
-
| Channel | Description | Requires Coin |
|
|
451
|
-
|
|
452
|
-
| `orderbook` | L2 order book updates | Yes |
|
|
453
|
-
| `trades` | Trade/fill updates | Yes |
|
|
454
|
-
| `
|
|
455
|
-
| `
|
|
518
|
+
| Channel | Description | Requires Coin | Historical Support |
|
|
519
|
+
|---------|-------------|---------------|-------------------|
|
|
520
|
+
| `orderbook` | L2 order book updates | Yes | Yes |
|
|
521
|
+
| `trades` | Trade/fill updates | Yes | Yes |
|
|
522
|
+
| `candles` | OHLCV candle data | Yes | Yes (replay/stream only) |
|
|
523
|
+
| `ticker` | Price and 24h volume | Yes | Real-time only |
|
|
524
|
+
| `all_tickers` | All market tickers | No | Real-time only |
|
|
525
|
+
|
|
526
|
+
#### Candle Replay/Stream
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
# Replay candles at 10x speed
|
|
530
|
+
await ws.replay(
|
|
531
|
+
"candles", "BTC",
|
|
532
|
+
start=int(time.time() * 1000) - 86400000,
|
|
533
|
+
end=int(time.time() * 1000),
|
|
534
|
+
speed=10,
|
|
535
|
+
interval="15m" # 1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
# Bulk stream candles
|
|
539
|
+
await ws.stream(
|
|
540
|
+
"candles", "ETH",
|
|
541
|
+
start=int(time.time() * 1000) - 3600000,
|
|
542
|
+
end=int(time.time() * 1000),
|
|
543
|
+
batch_size=1000,
|
|
544
|
+
interval="1h"
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
# Lighter.xyz candles
|
|
548
|
+
await ws.replay(
|
|
549
|
+
"lighter_candles", "BTC",
|
|
550
|
+
start=...,
|
|
551
|
+
speed=10,
|
|
552
|
+
interval="5m"
|
|
553
|
+
)
|
|
554
|
+
```
|
|
456
555
|
|
|
457
556
|
## Timestamp Formats
|
|
458
557
|
|
|
@@ -501,9 +600,11 @@ from oxarchive.resources.trades import CursorResponse
|
|
|
501
600
|
|
|
502
601
|
client = Client(api_key="ox_your_api_key")
|
|
503
602
|
|
|
504
|
-
orderbook: OrderBook = client.orderbook.get("BTC")
|
|
505
|
-
|
|
506
|
-
|
|
603
|
+
orderbook: OrderBook = client.hyperliquid.orderbook.get("BTC")
|
|
604
|
+
result: CursorResponse = client.hyperliquid.trades.list("BTC", start=..., end=...)
|
|
605
|
+
|
|
606
|
+
# Lighter has real-time data, so recent() is available
|
|
607
|
+
recent: list[Trade] = client.lighter.trades.recent("BTC")
|
|
507
608
|
|
|
508
609
|
# Lighter granularity type hint
|
|
509
610
|
granularity: LighterGranularity = "10s"
|
|
@@ -31,6 +31,8 @@ from .types import (
|
|
|
31
31
|
LighterInstrument,
|
|
32
32
|
FundingRate,
|
|
33
33
|
OpenInterest,
|
|
34
|
+
Candle,
|
|
35
|
+
CandleInterval,
|
|
34
36
|
OxArchiveError,
|
|
35
37
|
# WebSocket types
|
|
36
38
|
WsChannel,
|
|
@@ -65,7 +67,7 @@ except ImportError:
|
|
|
65
67
|
OxArchiveWs = None # type: ignore
|
|
66
68
|
WsOptions = None # type: ignore
|
|
67
69
|
|
|
68
|
-
__version__ = "0.
|
|
70
|
+
__version__ = "0.5.3"
|
|
69
71
|
|
|
70
72
|
__all__ = [
|
|
71
73
|
# Client
|
|
@@ -84,6 +86,8 @@ __all__ = [
|
|
|
84
86
|
"LighterGranularity",
|
|
85
87
|
"FundingRate",
|
|
86
88
|
"OpenInterest",
|
|
89
|
+
"Candle",
|
|
90
|
+
"CandleInterval",
|
|
87
91
|
"OxArchiveError",
|
|
88
92
|
# WebSocket Types
|
|
89
93
|
"WsChannel",
|
|
@@ -10,6 +10,8 @@ from .resources import (
|
|
|
10
10
|
LighterInstrumentsResource,
|
|
11
11
|
FundingResource,
|
|
12
12
|
OpenInterestResource,
|
|
13
|
+
CandlesResource,
|
|
14
|
+
LiquidationsResource,
|
|
13
15
|
)
|
|
14
16
|
|
|
15
17
|
|
|
@@ -44,6 +46,12 @@ class HyperliquidClient:
|
|
|
44
46
|
self.open_interest = OpenInterestResource(http, base_path)
|
|
45
47
|
"""Open interest"""
|
|
46
48
|
|
|
49
|
+
self.candles = CandlesResource(http, base_path)
|
|
50
|
+
"""OHLCV candle data"""
|
|
51
|
+
|
|
52
|
+
self.liquidations = LiquidationsResource(http, base_path)
|
|
53
|
+
"""Liquidation events (May 2025+)"""
|
|
54
|
+
|
|
47
55
|
|
|
48
56
|
class LighterClient:
|
|
49
57
|
"""
|
|
@@ -77,3 +85,6 @@ class LighterClient:
|
|
|
77
85
|
|
|
78
86
|
self.open_interest = OpenInterestResource(http, base_path)
|
|
79
87
|
"""Open interest"""
|
|
88
|
+
|
|
89
|
+
self.candles = CandlesResource(http, base_path)
|
|
90
|
+
"""OHLCV candle data"""
|
|
@@ -5,6 +5,8 @@ from .trades import TradesResource
|
|
|
5
5
|
from .instruments import InstrumentsResource, LighterInstrumentsResource
|
|
6
6
|
from .funding import FundingResource
|
|
7
7
|
from .openinterest import OpenInterestResource
|
|
8
|
+
from .candles import CandlesResource
|
|
9
|
+
from .liquidations import LiquidationsResource
|
|
8
10
|
|
|
9
11
|
__all__ = [
|
|
10
12
|
"OrderBookResource",
|
|
@@ -13,4 +15,6 @@ __all__ = [
|
|
|
13
15
|
"LighterInstrumentsResource",
|
|
14
16
|
"FundingResource",
|
|
15
17
|
"OpenInterestResource",
|
|
18
|
+
"CandlesResource",
|
|
19
|
+
"LiquidationsResource",
|
|
16
20
|
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Candles (OHLCV) API resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..http import HttpClient
|
|
9
|
+
from ..types import Candle, CandleInterval, CursorResponse, Timestamp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CandlesResource:
|
|
13
|
+
"""
|
|
14
|
+
Candles (OHLCV) API resource.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> # Get candle history
|
|
18
|
+
>>> result = client.candles.history("BTC", start=start, end=end, interval="1h")
|
|
19
|
+
>>> for candle in result.data:
|
|
20
|
+
... print(f"{candle.timestamp}: O={candle.open} H={candle.high} L={candle.low} C={candle.close}")
|
|
21
|
+
>>>
|
|
22
|
+
>>> # Paginate through large datasets
|
|
23
|
+
>>> all_candles = result.data
|
|
24
|
+
>>> while result.next_cursor:
|
|
25
|
+
... result = client.candles.history("BTC", start=start, end=end, cursor=result.next_cursor)
|
|
26
|
+
... all_candles.extend(result.data)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, http: HttpClient, base_path: str = "/v1"):
|
|
30
|
+
self._http = http
|
|
31
|
+
self._base_path = base_path
|
|
32
|
+
|
|
33
|
+
def _convert_timestamp(self, ts: Optional[Timestamp]) -> Optional[int]:
|
|
34
|
+
"""Convert timestamp to Unix milliseconds."""
|
|
35
|
+
if ts is None:
|
|
36
|
+
return None
|
|
37
|
+
if isinstance(ts, int):
|
|
38
|
+
return ts
|
|
39
|
+
if isinstance(ts, datetime):
|
|
40
|
+
return int(ts.timestamp() * 1000)
|
|
41
|
+
if isinstance(ts, str):
|
|
42
|
+
try:
|
|
43
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
44
|
+
return int(dt.timestamp() * 1000)
|
|
45
|
+
except ValueError:
|
|
46
|
+
return int(ts)
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def history(
|
|
50
|
+
self,
|
|
51
|
+
coin: str,
|
|
52
|
+
*,
|
|
53
|
+
start: Timestamp,
|
|
54
|
+
end: Timestamp,
|
|
55
|
+
interval: Optional[CandleInterval] = None,
|
|
56
|
+
cursor: Optional[Timestamp] = None,
|
|
57
|
+
limit: Optional[int] = None,
|
|
58
|
+
) -> CursorResponse[list[Candle]]:
|
|
59
|
+
"""
|
|
60
|
+
Get historical OHLCV candle data with cursor-based pagination.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
64
|
+
start: Start timestamp (required)
|
|
65
|
+
end: End timestamp (required)
|
|
66
|
+
interval: Candle interval (1m, 5m, 15m, 30m, 1h, 4h, 1d, 1w). Default: 1h
|
|
67
|
+
cursor: Cursor from previous response's next_cursor
|
|
68
|
+
limit: Maximum number of results (default: 100, max: 1000)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
CursorResponse with candle records and next_cursor for pagination
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
>>> result = client.candles.history("BTC", start=start, end=end, interval="1h", limit=1000)
|
|
75
|
+
>>> candles = result.data
|
|
76
|
+
>>> while result.next_cursor:
|
|
77
|
+
... result = client.candles.history(
|
|
78
|
+
... "BTC", start=start, end=end, interval="1h", cursor=result.next_cursor, limit=1000
|
|
79
|
+
... )
|
|
80
|
+
... candles.extend(result.data)
|
|
81
|
+
"""
|
|
82
|
+
data = self._http.get(
|
|
83
|
+
f"{self._base_path}/candles/{coin.upper()}",
|
|
84
|
+
params={
|
|
85
|
+
"start": self._convert_timestamp(start),
|
|
86
|
+
"end": self._convert_timestamp(end),
|
|
87
|
+
"interval": interval,
|
|
88
|
+
"cursor": self._convert_timestamp(cursor),
|
|
89
|
+
"limit": limit,
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
return CursorResponse(
|
|
93
|
+
data=[Candle.model_validate(item) for item in data["data"]],
|
|
94
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
async def ahistory(
|
|
98
|
+
self,
|
|
99
|
+
coin: str,
|
|
100
|
+
*,
|
|
101
|
+
start: Timestamp,
|
|
102
|
+
end: Timestamp,
|
|
103
|
+
interval: Optional[CandleInterval] = None,
|
|
104
|
+
cursor: Optional[Timestamp] = None,
|
|
105
|
+
limit: Optional[int] = None,
|
|
106
|
+
) -> CursorResponse[list[Candle]]:
|
|
107
|
+
"""Async version of history(). start and end are required."""
|
|
108
|
+
data = await self._http.aget(
|
|
109
|
+
f"{self._base_path}/candles/{coin.upper()}",
|
|
110
|
+
params={
|
|
111
|
+
"start": self._convert_timestamp(start),
|
|
112
|
+
"end": self._convert_timestamp(end),
|
|
113
|
+
"interval": interval,
|
|
114
|
+
"cursor": self._convert_timestamp(cursor),
|
|
115
|
+
"limit": limit,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
return CursorResponse(
|
|
119
|
+
data=[Candle.model_validate(item) for item in data["data"]],
|
|
120
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
121
|
+
)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Liquidations API resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from ..http import HttpClient
|
|
9
|
+
from ..types import CursorResponse, Liquidation, Timestamp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LiquidationsResource:
|
|
13
|
+
"""
|
|
14
|
+
Liquidations API resource.
|
|
15
|
+
|
|
16
|
+
Retrieve historical liquidation events from Hyperliquid.
|
|
17
|
+
|
|
18
|
+
Note: Liquidation data is available from May 25, 2025 onwards.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> # Get recent liquidations
|
|
22
|
+
>>> liquidations = client.hyperliquid.liquidations.history(
|
|
23
|
+
... "BTC",
|
|
24
|
+
... start="2025-06-01",
|
|
25
|
+
... end="2025-06-02"
|
|
26
|
+
... )
|
|
27
|
+
>>>
|
|
28
|
+
>>> # Get liquidations for a specific user
|
|
29
|
+
>>> user_liquidations = client.hyperliquid.liquidations.by_user(
|
|
30
|
+
... "0x1234...",
|
|
31
|
+
... start="2025-06-01",
|
|
32
|
+
... end="2025-06-02"
|
|
33
|
+
... )
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, http: HttpClient, base_path: str = "/v1"):
|
|
37
|
+
self._http = http
|
|
38
|
+
self._base_path = base_path
|
|
39
|
+
|
|
40
|
+
def _convert_timestamp(self, ts: Optional[Timestamp]) -> Optional[int]:
|
|
41
|
+
"""Convert timestamp to Unix milliseconds."""
|
|
42
|
+
if ts is None:
|
|
43
|
+
return None
|
|
44
|
+
if isinstance(ts, int):
|
|
45
|
+
return ts
|
|
46
|
+
if isinstance(ts, datetime):
|
|
47
|
+
return int(ts.timestamp() * 1000)
|
|
48
|
+
if isinstance(ts, str):
|
|
49
|
+
try:
|
|
50
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
51
|
+
return int(dt.timestamp() * 1000)
|
|
52
|
+
except ValueError:
|
|
53
|
+
return int(ts)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
def history(
|
|
57
|
+
self,
|
|
58
|
+
coin: str,
|
|
59
|
+
*,
|
|
60
|
+
start: Timestamp,
|
|
61
|
+
end: Timestamp,
|
|
62
|
+
cursor: Optional[str] = None,
|
|
63
|
+
limit: Optional[int] = None,
|
|
64
|
+
) -> CursorResponse[list[Liquidation]]:
|
|
65
|
+
"""
|
|
66
|
+
Get liquidation history for a coin with cursor-based pagination.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
70
|
+
start: Start timestamp (required)
|
|
71
|
+
end: End timestamp (required)
|
|
72
|
+
cursor: Cursor from previous response's next_cursor
|
|
73
|
+
limit: Maximum number of results (default: 100, max: 1000)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
CursorResponse with liquidation records and next_cursor for pagination
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> result = client.hyperliquid.liquidations.history("BTC", start=start, end=end, limit=1000)
|
|
80
|
+
>>> liquidations = result.data
|
|
81
|
+
>>> while result.next_cursor:
|
|
82
|
+
... result = client.hyperliquid.liquidations.history(
|
|
83
|
+
... "BTC", start=start, end=end, cursor=result.next_cursor, limit=1000
|
|
84
|
+
... )
|
|
85
|
+
... liquidations.extend(result.data)
|
|
86
|
+
"""
|
|
87
|
+
data = self._http.get(
|
|
88
|
+
f"{self._base_path}/liquidations/{coin.upper()}",
|
|
89
|
+
params={
|
|
90
|
+
"start": self._convert_timestamp(start),
|
|
91
|
+
"end": self._convert_timestamp(end),
|
|
92
|
+
"cursor": cursor,
|
|
93
|
+
"limit": limit,
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
return CursorResponse(
|
|
97
|
+
data=[Liquidation.model_validate(item) for item in data["data"]],
|
|
98
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def ahistory(
|
|
102
|
+
self,
|
|
103
|
+
coin: str,
|
|
104
|
+
*,
|
|
105
|
+
start: Timestamp,
|
|
106
|
+
end: Timestamp,
|
|
107
|
+
cursor: Optional[str] = None,
|
|
108
|
+
limit: Optional[int] = None,
|
|
109
|
+
) -> CursorResponse[list[Liquidation]]:
|
|
110
|
+
"""Async version of history(). start and end are required."""
|
|
111
|
+
data = await self._http.aget(
|
|
112
|
+
f"{self._base_path}/liquidations/{coin.upper()}",
|
|
113
|
+
params={
|
|
114
|
+
"start": self._convert_timestamp(start),
|
|
115
|
+
"end": self._convert_timestamp(end),
|
|
116
|
+
"cursor": cursor,
|
|
117
|
+
"limit": limit,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
return CursorResponse(
|
|
121
|
+
data=[Liquidation.model_validate(item) for item in data["data"]],
|
|
122
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def by_user(
|
|
126
|
+
self,
|
|
127
|
+
user_address: str,
|
|
128
|
+
*,
|
|
129
|
+
start: Timestamp,
|
|
130
|
+
end: Timestamp,
|
|
131
|
+
coin: Optional[str] = None,
|
|
132
|
+
cursor: Optional[str] = None,
|
|
133
|
+
limit: Optional[int] = None,
|
|
134
|
+
) -> CursorResponse[list[Liquidation]]:
|
|
135
|
+
"""
|
|
136
|
+
Get liquidation history for a specific user.
|
|
137
|
+
|
|
138
|
+
This returns liquidations where the user was either:
|
|
139
|
+
- The liquidated party (their position was liquidated)
|
|
140
|
+
- The liquidator (they executed the liquidation)
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
user_address: User's wallet address (e.g., '0x1234...')
|
|
144
|
+
start: Start timestamp (required)
|
|
145
|
+
end: End timestamp (required)
|
|
146
|
+
coin: Optional coin filter (e.g., 'BTC', 'ETH')
|
|
147
|
+
cursor: Cursor from previous response's next_cursor
|
|
148
|
+
limit: Maximum number of results (default: 100, max: 1000)
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
CursorResponse with liquidation records and next_cursor for pagination
|
|
152
|
+
"""
|
|
153
|
+
params = {
|
|
154
|
+
"start": self._convert_timestamp(start),
|
|
155
|
+
"end": self._convert_timestamp(end),
|
|
156
|
+
"cursor": cursor,
|
|
157
|
+
"limit": limit,
|
|
158
|
+
}
|
|
159
|
+
if coin:
|
|
160
|
+
params["coin"] = coin.upper()
|
|
161
|
+
|
|
162
|
+
data = self._http.get(
|
|
163
|
+
f"{self._base_path}/liquidations/user/{user_address}",
|
|
164
|
+
params=params,
|
|
165
|
+
)
|
|
166
|
+
return CursorResponse(
|
|
167
|
+
data=[Liquidation.model_validate(item) for item in data["data"]],
|
|
168
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
async def aby_user(
|
|
172
|
+
self,
|
|
173
|
+
user_address: str,
|
|
174
|
+
*,
|
|
175
|
+
start: Timestamp,
|
|
176
|
+
end: Timestamp,
|
|
177
|
+
coin: Optional[str] = None,
|
|
178
|
+
cursor: Optional[str] = None,
|
|
179
|
+
limit: Optional[int] = None,
|
|
180
|
+
) -> CursorResponse[list[Liquidation]]:
|
|
181
|
+
"""Async version of by_user()."""
|
|
182
|
+
params = {
|
|
183
|
+
"start": self._convert_timestamp(start),
|
|
184
|
+
"end": self._convert_timestamp(end),
|
|
185
|
+
"cursor": cursor,
|
|
186
|
+
"limit": limit,
|
|
187
|
+
}
|
|
188
|
+
if coin:
|
|
189
|
+
params["coin"] = coin.upper()
|
|
190
|
+
|
|
191
|
+
data = await self._http.aget(
|
|
192
|
+
f"{self._base_path}/liquidations/user/{user_address}",
|
|
193
|
+
params=params,
|
|
194
|
+
)
|
|
195
|
+
return CursorResponse(
|
|
196
|
+
data=[Liquidation.model_validate(item) for item in data["data"]],
|
|
197
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
198
|
+
)
|
|
@@ -14,17 +14,17 @@ class TradesResource:
|
|
|
14
14
|
Trades API resource.
|
|
15
15
|
|
|
16
16
|
Example:
|
|
17
|
-
>>> # Get recent trades
|
|
18
|
-
>>> trades = client.trades.recent("BTC")
|
|
19
|
-
>>>
|
|
20
17
|
>>> # Get trade history with cursor-based pagination (recommended)
|
|
21
|
-
>>> result = client.trades.list("BTC", start="2024-01-01", end="2024-01-02")
|
|
18
|
+
>>> result = client.hyperliquid.trades.list("BTC", start="2024-01-01", end="2024-01-02")
|
|
22
19
|
>>> trades = result.data
|
|
23
20
|
>>>
|
|
24
21
|
>>> # Get all pages
|
|
25
22
|
>>> while result.next_cursor:
|
|
26
|
-
... result = client.trades.list("BTC", start="2024-01-01", end="2024-01-02", cursor=result.next_cursor)
|
|
23
|
+
... result = client.hyperliquid.trades.list("BTC", start="2024-01-01", end="2024-01-02", cursor=result.next_cursor)
|
|
27
24
|
... trades.extend(result.data)
|
|
25
|
+
>>>
|
|
26
|
+
>>> # Get recent trades (Lighter only - has real-time data)
|
|
27
|
+
>>> recent = client.lighter.trades.recent("BTC")
|
|
28
28
|
"""
|
|
29
29
|
|
|
30
30
|
def __init__(self, http: HttpClient, base_path: str = "/v1"):
|
|
@@ -135,6 +135,10 @@ class TradesResource:
|
|
|
135
135
|
"""
|
|
136
136
|
Get most recent trades for a coin.
|
|
137
137
|
|
|
138
|
+
Note: This method is only available for Lighter (client.lighter.trades.recent())
|
|
139
|
+
which has real-time data ingestion. Hyperliquid uses hourly backfill so this
|
|
140
|
+
endpoint is not available for Hyperliquid.
|
|
141
|
+
|
|
138
142
|
Args:
|
|
139
143
|
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
140
144
|
limit: Number of trades to return (default: 100)
|
|
@@ -270,12 +270,94 @@ class OpenInterest(BaseModel):
|
|
|
270
270
|
"""Impact ask price for liquidations."""
|
|
271
271
|
|
|
272
272
|
|
|
273
|
+
# =============================================================================
|
|
274
|
+
# Liquidation Types
|
|
275
|
+
# =============================================================================
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
class Liquidation(BaseModel):
|
|
279
|
+
"""Liquidation event record."""
|
|
280
|
+
|
|
281
|
+
coin: str
|
|
282
|
+
"""Trading pair symbol."""
|
|
283
|
+
|
|
284
|
+
timestamp: datetime
|
|
285
|
+
"""Liquidation timestamp (UTC)."""
|
|
286
|
+
|
|
287
|
+
liquidated_user: str
|
|
288
|
+
"""Address of the liquidated user."""
|
|
289
|
+
|
|
290
|
+
liquidator_user: str
|
|
291
|
+
"""Address of the liquidator."""
|
|
292
|
+
|
|
293
|
+
price: str
|
|
294
|
+
"""Liquidation execution price."""
|
|
295
|
+
|
|
296
|
+
size: str
|
|
297
|
+
"""Liquidation size."""
|
|
298
|
+
|
|
299
|
+
side: Literal["B", "S"]
|
|
300
|
+
"""Side: 'B' (buy) or 'S' (sell)."""
|
|
301
|
+
|
|
302
|
+
mark_price: Optional[str] = None
|
|
303
|
+
"""Mark price at time of liquidation."""
|
|
304
|
+
|
|
305
|
+
closed_pnl: Optional[str] = None
|
|
306
|
+
"""Realized PnL from the liquidation."""
|
|
307
|
+
|
|
308
|
+
direction: Optional[str] = None
|
|
309
|
+
"""Position direction (e.g., 'Open Long', 'Close Short')."""
|
|
310
|
+
|
|
311
|
+
trade_id: Optional[int] = None
|
|
312
|
+
"""Unique trade ID."""
|
|
313
|
+
|
|
314
|
+
tx_hash: Optional[str] = None
|
|
315
|
+
"""Blockchain transaction hash."""
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# =============================================================================
|
|
319
|
+
# Candle Types
|
|
320
|
+
# =============================================================================
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
CandleInterval = Literal["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"]
|
|
324
|
+
"""Candle interval for OHLCV data."""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
class Candle(BaseModel):
|
|
328
|
+
"""OHLCV candle data."""
|
|
329
|
+
|
|
330
|
+
timestamp: datetime
|
|
331
|
+
"""Candle open timestamp (UTC)."""
|
|
332
|
+
|
|
333
|
+
open: float
|
|
334
|
+
"""Opening price."""
|
|
335
|
+
|
|
336
|
+
high: float
|
|
337
|
+
"""Highest price during the interval."""
|
|
338
|
+
|
|
339
|
+
low: float
|
|
340
|
+
"""Lowest price during the interval."""
|
|
341
|
+
|
|
342
|
+
close: float
|
|
343
|
+
"""Closing price."""
|
|
344
|
+
|
|
345
|
+
volume: float
|
|
346
|
+
"""Total volume traded during the interval."""
|
|
347
|
+
|
|
348
|
+
quote_volume: Optional[float] = None
|
|
349
|
+
"""Total quote volume (volume * price)."""
|
|
350
|
+
|
|
351
|
+
trade_count: Optional[int] = None
|
|
352
|
+
"""Number of trades during the interval."""
|
|
353
|
+
|
|
354
|
+
|
|
273
355
|
# =============================================================================
|
|
274
356
|
# WebSocket Types
|
|
275
357
|
# =============================================================================
|
|
276
358
|
|
|
277
|
-
WsChannel = Literal["orderbook", "trades", "ticker", "all_tickers"]
|
|
278
|
-
"""Available WebSocket channels. Note: ticker/all_tickers are real-time only."""
|
|
359
|
+
WsChannel = Literal["orderbook", "trades", "candles", "liquidations", "ticker", "all_tickers"]
|
|
360
|
+
"""Available WebSocket channels. Note: ticker/all_tickers are real-time only. Liquidations is historical only (May 2025+)."""
|
|
279
361
|
|
|
280
362
|
WsConnectionState = Literal["connecting", "connected", "disconnected", "reconnecting"]
|
|
281
363
|
"""WebSocket connection state."""
|
|
@@ -382,6 +464,41 @@ class WsHistoricalData(BaseModel):
|
|
|
382
464
|
data: dict[str, Any]
|
|
383
465
|
|
|
384
466
|
|
|
467
|
+
class OrderbookDelta(BaseModel):
|
|
468
|
+
"""Orderbook delta for tick-level data."""
|
|
469
|
+
|
|
470
|
+
timestamp: int
|
|
471
|
+
"""Timestamp in milliseconds."""
|
|
472
|
+
|
|
473
|
+
side: Literal["bid", "ask"]
|
|
474
|
+
"""Side: 'bid' or 'ask'."""
|
|
475
|
+
|
|
476
|
+
price: float
|
|
477
|
+
"""Price level."""
|
|
478
|
+
|
|
479
|
+
size: float
|
|
480
|
+
"""New size (0 = level removed)."""
|
|
481
|
+
|
|
482
|
+
sequence: int
|
|
483
|
+
"""Sequence number for ordering."""
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
class WsHistoricalTickData(BaseModel):
|
|
487
|
+
"""Historical tick data (granularity='tick' mode) - checkpoint + deltas.
|
|
488
|
+
|
|
489
|
+
This message type is sent when using granularity='tick' for Lighter.xyz
|
|
490
|
+
orderbook data. It provides a full checkpoint followed by incremental deltas.
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
type: Literal["historical_tick_data"]
|
|
494
|
+
channel: WsChannel
|
|
495
|
+
coin: str
|
|
496
|
+
checkpoint: dict[str, Any]
|
|
497
|
+
"""Initial checkpoint (full orderbook snapshot)."""
|
|
498
|
+
deltas: list[OrderbookDelta]
|
|
499
|
+
"""Incremental deltas to apply after checkpoint."""
|
|
500
|
+
|
|
501
|
+
|
|
385
502
|
# =============================================================================
|
|
386
503
|
# WebSocket Bulk Stream Types (Bulk Download Mode)
|
|
387
504
|
# =============================================================================
|
|
@@ -42,6 +42,7 @@ except ImportError:
|
|
|
42
42
|
|
|
43
43
|
from .types import (
|
|
44
44
|
OrderBook,
|
|
45
|
+
OrderbookDelta,
|
|
45
46
|
PriceLevel,
|
|
46
47
|
Trade,
|
|
47
48
|
WsChannel,
|
|
@@ -57,6 +58,7 @@ from .types import (
|
|
|
57
58
|
WsReplayCompleted,
|
|
58
59
|
WsReplayStopped,
|
|
59
60
|
WsHistoricalData,
|
|
61
|
+
WsHistoricalTickData,
|
|
60
62
|
WsStreamStarted,
|
|
61
63
|
WsStreamProgress,
|
|
62
64
|
WsHistoricalBatch,
|
|
@@ -110,6 +112,7 @@ ErrorHandler = Callable[[Exception], None]
|
|
|
110
112
|
|
|
111
113
|
# Replay handlers
|
|
112
114
|
HistoricalDataHandler = Callable[[str, int, dict], None]
|
|
115
|
+
HistoricalTickDataHandler = Callable[[str, dict, list[OrderbookDelta]], None] # coin, checkpoint, deltas
|
|
113
116
|
ReplayStartHandler = Callable[[WsChannel, str, int, int, float], None] # channel, coin, start, end, speed
|
|
114
117
|
ReplayCompleteHandler = Callable[[WsChannel, str, int], None] # channel, coin, snapshots_sent
|
|
115
118
|
|
|
@@ -276,6 +279,7 @@ class OxArchiveWs:
|
|
|
276
279
|
|
|
277
280
|
# Replay handlers (Option B)
|
|
278
281
|
self._on_historical_data: Optional[HistoricalDataHandler] = None
|
|
282
|
+
self._on_historical_tick_data: Optional[HistoricalTickDataHandler] = None
|
|
279
283
|
self._on_replay_start: Optional[ReplayStartHandler] = None
|
|
280
284
|
self._on_replay_complete: Optional[ReplayCompleteHandler] = None
|
|
281
285
|
|
|
@@ -307,7 +311,9 @@ class OxArchiveWs:
|
|
|
307
311
|
url = f"{self.options.ws_url}?apiKey={self.options.api_key}"
|
|
308
312
|
|
|
309
313
|
try:
|
|
310
|
-
|
|
314
|
+
# Increase max_size to 50MB for large Lighter orderbook data with high granularity
|
|
315
|
+
# Lighter tick data with full depth (~3700 levels) can exceed 14MB per message
|
|
316
|
+
self._ws = await ws_connect(url, max_size=50 * 1024 * 1024)
|
|
311
317
|
self._reconnect_attempts = 0
|
|
312
318
|
self._set_state("connected")
|
|
313
319
|
|
|
@@ -427,6 +433,8 @@ class OxArchiveWs:
|
|
|
427
433
|
start: int,
|
|
428
434
|
end: Optional[int] = None,
|
|
429
435
|
speed: float = 1.0,
|
|
436
|
+
granularity: Optional[str] = None,
|
|
437
|
+
interval: Optional[str] = None,
|
|
430
438
|
) -> None:
|
|
431
439
|
"""Start historical replay with timing preserved.
|
|
432
440
|
|
|
@@ -436,9 +444,12 @@ class OxArchiveWs:
|
|
|
436
444
|
start: Start timestamp (Unix ms)
|
|
437
445
|
end: End timestamp (Unix ms, defaults to now)
|
|
438
446
|
speed: Playback speed multiplier (1 = real-time, 10 = 10x faster)
|
|
447
|
+
granularity: Data resolution for Lighter orderbook ('checkpoint', '30s', '10s', '1s', 'tick')
|
|
448
|
+
interval: Candle interval for candles channel ('1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w')
|
|
439
449
|
|
|
440
450
|
Example:
|
|
441
451
|
>>> await ws.replay("orderbook", "BTC", start=time.time()*1000 - 86400000, speed=10)
|
|
452
|
+
>>> await ws.replay("candles", "BTC", start=..., speed=10, interval="15m")
|
|
442
453
|
"""
|
|
443
454
|
msg = {
|
|
444
455
|
"op": "replay",
|
|
@@ -449,6 +460,10 @@ class OxArchiveWs:
|
|
|
449
460
|
}
|
|
450
461
|
if end is not None:
|
|
451
462
|
msg["end"] = end
|
|
463
|
+
if granularity is not None:
|
|
464
|
+
msg["granularity"] = granularity
|
|
465
|
+
if interval is not None:
|
|
466
|
+
msg["interval"] = interval
|
|
452
467
|
await self._send(msg)
|
|
453
468
|
|
|
454
469
|
async def replay_pause(self) -> None:
|
|
@@ -482,6 +497,8 @@ class OxArchiveWs:
|
|
|
482
497
|
start: int,
|
|
483
498
|
end: int,
|
|
484
499
|
batch_size: int = 1000,
|
|
500
|
+
granularity: Optional[str] = None,
|
|
501
|
+
interval: Optional[str] = None,
|
|
485
502
|
) -> None:
|
|
486
503
|
"""Start bulk streaming for fast data download.
|
|
487
504
|
|
|
@@ -491,18 +508,26 @@ class OxArchiveWs:
|
|
|
491
508
|
start: Start timestamp (Unix ms)
|
|
492
509
|
end: End timestamp (Unix ms)
|
|
493
510
|
batch_size: Records per batch message
|
|
511
|
+
granularity: Data resolution for Lighter orderbook ('checkpoint', '30s', '10s', '1s', 'tick')
|
|
512
|
+
interval: Candle interval for candles channel ('1m', '5m', '15m', '30m', '1h', '4h', '1d', '1w')
|
|
494
513
|
|
|
495
514
|
Example:
|
|
496
515
|
>>> await ws.stream("orderbook", "ETH", start=..., end=..., batch_size=1000)
|
|
516
|
+
>>> await ws.stream("candles", "BTC", start=..., end=..., interval="1h")
|
|
497
517
|
"""
|
|
498
|
-
|
|
518
|
+
msg = {
|
|
499
519
|
"op": "stream",
|
|
500
520
|
"channel": channel,
|
|
501
521
|
"coin": coin,
|
|
502
522
|
"start": start,
|
|
503
523
|
"end": end,
|
|
504
524
|
"batch_size": batch_size,
|
|
505
|
-
}
|
|
525
|
+
}
|
|
526
|
+
if granularity is not None:
|
|
527
|
+
msg["granularity"] = granularity
|
|
528
|
+
if interval is not None:
|
|
529
|
+
msg["interval"] = interval
|
|
530
|
+
await self._send(msg)
|
|
506
531
|
|
|
507
532
|
async def stream_stop(self) -> None:
|
|
508
533
|
"""Stop the current bulk stream."""
|
|
@@ -547,6 +572,16 @@ class OxArchiveWs:
|
|
|
547
572
|
"""
|
|
548
573
|
self._on_historical_data = handler
|
|
549
574
|
|
|
575
|
+
def on_historical_tick_data(self, handler: HistoricalTickDataHandler) -> None:
|
|
576
|
+
"""Set handler for historical tick data (granularity='tick' mode).
|
|
577
|
+
|
|
578
|
+
This is for tick-level granularity on Lighter.xyz orderbook data.
|
|
579
|
+
Receives a checkpoint (full orderbook) followed by incremental deltas.
|
|
580
|
+
|
|
581
|
+
Handler receives: (coin, checkpoint, deltas)
|
|
582
|
+
"""
|
|
583
|
+
self._on_historical_tick_data = handler
|
|
584
|
+
|
|
550
585
|
def on_replay_start(self, handler: ReplayStartHandler) -> None:
|
|
551
586
|
"""Set handler for replay started event.
|
|
552
587
|
|
|
@@ -722,6 +757,10 @@ class OxArchiveWs:
|
|
|
722
757
|
elif msg_type == "historical_data" and self._on_historical_data:
|
|
723
758
|
self._on_historical_data(data["coin"], data["timestamp"], data["data"])
|
|
724
759
|
|
|
760
|
+
elif msg_type == "historical_tick_data" and self._on_historical_tick_data:
|
|
761
|
+
msg = WsHistoricalTickData(**data)
|
|
762
|
+
self._on_historical_tick_data(msg.coin, msg.checkpoint, msg.deltas)
|
|
763
|
+
|
|
725
764
|
elif msg_type == "replay_completed" and self._on_replay_complete:
|
|
726
765
|
self._on_replay_complete(data["channel"], data["coin"], data["snapshots_sent"])
|
|
727
766
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|