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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oxarchive
3
- Version: 0.4.4
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 versions
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
- | `ticker` | Price and 24h volume | Yes |
492
- | `all_tickers` | All market tickers | No |
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
- trades: list[Trade] = client.trades.recent("BTC")
543
- result: CursorResponse = client.trades.list("BTC", start=..., end=...)
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 versions
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
- | `ticker` | Price and 24h volume | Yes |
455
- | `all_tickers` | All market tickers | No |
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
- trades: list[Trade] = client.trades.recent("BTC")
506
- result: CursorResponse = client.trades.list("BTC", start=..., end=...)
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.4.4"
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
- self._ws = await ws_connect(url)
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
- await self._send({
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oxarchive"
7
- version = "0.4.4"
7
+ version = "0.5.3"
8
8
  description = "Official Python SDK for 0xarchive - Hyperliquid Historical Data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
File without changes
File without changes
File without changes