oxarchive 0.3.6__py3-none-any.whl → 0.4.5__py3-none-any.whl
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/__init__.py +19 -6
- oxarchive/client.py +39 -17
- oxarchive/exchanges.py +79 -0
- oxarchive/resources/__init__.py +2 -1
- oxarchive/resources/funding.py +34 -18
- oxarchive/resources/instruments.py +61 -6
- oxarchive/resources/openinterest.py +34 -18
- oxarchive/resources/orderbook.py +59 -23
- oxarchive/resources/trades.py +9 -151
- oxarchive/types.py +98 -4
- oxarchive/websocket.py +36 -9
- {oxarchive-0.3.6.dist-info → oxarchive-0.4.5.dist-info}/METADATA +174 -39
- oxarchive-0.4.5.dist-info/RECORD +15 -0
- oxarchive-0.3.6.dist-info/RECORD +0 -14
- {oxarchive-0.3.6.dist-info → oxarchive-0.4.5.dist-info}/WHEEL +0 -0
oxarchive/resources/orderbook.py
CHANGED
|
@@ -6,7 +6,12 @@ from datetime import datetime
|
|
|
6
6
|
from typing import Optional, Union
|
|
7
7
|
|
|
8
8
|
from ..http import HttpClient
|
|
9
|
-
from
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from ..types import CursorResponse, OrderBook, Timestamp
|
|
12
|
+
|
|
13
|
+
# Lighter orderbook granularity levels (Lighter.xyz only)
|
|
14
|
+
LighterGranularity = Literal["checkpoint", "30s", "10s", "1s", "tick"]
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
class OrderBookResource:
|
|
@@ -14,18 +19,22 @@ class OrderBookResource:
|
|
|
14
19
|
Order book API resource.
|
|
15
20
|
|
|
16
21
|
Example:
|
|
17
|
-
>>> # Get current order book
|
|
18
|
-
>>> orderbook = client.orderbook.get("BTC")
|
|
22
|
+
>>> # Get current order book (Hyperliquid)
|
|
23
|
+
>>> orderbook = client.hyperliquid.orderbook.get("BTC")
|
|
19
24
|
>>>
|
|
20
25
|
>>> # Get order book at specific timestamp
|
|
21
|
-
>>> historical = client.orderbook.get("ETH", timestamp=1704067200000)
|
|
26
|
+
>>> historical = client.hyperliquid.orderbook.get("ETH", timestamp=1704067200000)
|
|
22
27
|
>>>
|
|
23
28
|
>>> # Get order book history
|
|
24
|
-
>>> history = client.orderbook.history("BTC", start="2024-01-01", end="2024-01-02")
|
|
29
|
+
>>> history = client.hyperliquid.orderbook.history("BTC", start="2024-01-01", end="2024-01-02")
|
|
30
|
+
>>>
|
|
31
|
+
>>> # Lighter.xyz order book
|
|
32
|
+
>>> lighter_ob = client.lighter.orderbook.get("BTC")
|
|
25
33
|
"""
|
|
26
34
|
|
|
27
|
-
def __init__(self, http: HttpClient):
|
|
35
|
+
def __init__(self, http: HttpClient, base_path: str = "/v1"):
|
|
28
36
|
self._http = http
|
|
37
|
+
self._base_path = base_path
|
|
29
38
|
|
|
30
39
|
def _convert_timestamp(self, ts: Optional[Timestamp]) -> Optional[int]:
|
|
31
40
|
"""Convert timestamp to Unix milliseconds."""
|
|
@@ -63,7 +72,7 @@ class OrderBookResource:
|
|
|
63
72
|
Order book snapshot
|
|
64
73
|
"""
|
|
65
74
|
data = self._http.get(
|
|
66
|
-
f"/
|
|
75
|
+
f"{self._base_path}/orderbook/{coin.upper()}",
|
|
67
76
|
params={
|
|
68
77
|
"timestamp": self._convert_timestamp(timestamp),
|
|
69
78
|
"depth": depth,
|
|
@@ -80,7 +89,7 @@ class OrderBookResource:
|
|
|
80
89
|
) -> OrderBook:
|
|
81
90
|
"""Async version of get()."""
|
|
82
91
|
data = await self._http.aget(
|
|
83
|
-
f"/
|
|
92
|
+
f"{self._base_path}/orderbook/{coin.upper()}",
|
|
84
93
|
params={
|
|
85
94
|
"timestamp": self._convert_timestamp(timestamp),
|
|
86
95
|
"depth": depth,
|
|
@@ -94,35 +103,57 @@ class OrderBookResource:
|
|
|
94
103
|
*,
|
|
95
104
|
start: Timestamp,
|
|
96
105
|
end: Timestamp,
|
|
106
|
+
cursor: Optional[Timestamp] = None,
|
|
97
107
|
limit: Optional[int] = None,
|
|
98
|
-
offset: Optional[int] = None,
|
|
99
108
|
depth: Optional[int] = None,
|
|
100
|
-
|
|
109
|
+
granularity: Optional[LighterGranularity] = None,
|
|
110
|
+
) -> CursorResponse[list[OrderBook]]:
|
|
101
111
|
"""
|
|
102
|
-
Get historical order book snapshots.
|
|
112
|
+
Get historical order book snapshots with cursor-based pagination.
|
|
103
113
|
|
|
104
114
|
Args:
|
|
105
115
|
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
106
116
|
start: Start timestamp (required)
|
|
107
117
|
end: End timestamp (required)
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
cursor: Cursor from previous response's next_cursor (timestamp)
|
|
119
|
+
limit: Maximum number of results (default: 100, max: 1000)
|
|
110
120
|
depth: Number of price levels per side
|
|
121
|
+
granularity: Data resolution for Lighter orderbook (Lighter.xyz only, ignored for Hyperliquid).
|
|
122
|
+
Options: 'checkpoint' (1min, default), '30s', '10s', '1s', 'tick'.
|
|
123
|
+
Tier restrictions apply. Credit multipliers: checkpoint=1x, 30s=2x, 10s=3x, 1s=10x, tick=20x.
|
|
111
124
|
|
|
112
125
|
Returns:
|
|
113
|
-
|
|
126
|
+
CursorResponse with order book snapshots and next_cursor for pagination
|
|
127
|
+
|
|
128
|
+
Example:
|
|
129
|
+
>>> result = client.orderbook.history("BTC", start=start, end=end, limit=1000)
|
|
130
|
+
>>> snapshots = result.data
|
|
131
|
+
>>> while result.next_cursor:
|
|
132
|
+
... result = client.orderbook.history(
|
|
133
|
+
... "BTC", start=start, end=end, cursor=result.next_cursor, limit=1000
|
|
134
|
+
... )
|
|
135
|
+
... snapshots.extend(result.data)
|
|
136
|
+
>>>
|
|
137
|
+
>>> # Lighter.xyz with 10s granularity (Build+ tier)
|
|
138
|
+
>>> result = client.lighter.orderbook.history(
|
|
139
|
+
... "BTC", start=start, end=end, granularity="10s"
|
|
140
|
+
... )
|
|
114
141
|
"""
|
|
115
142
|
data = self._http.get(
|
|
116
|
-
f"/
|
|
143
|
+
f"{self._base_path}/orderbook/{coin.upper()}/history",
|
|
117
144
|
params={
|
|
118
145
|
"start": self._convert_timestamp(start),
|
|
119
146
|
"end": self._convert_timestamp(end),
|
|
147
|
+
"cursor": self._convert_timestamp(cursor),
|
|
120
148
|
"limit": limit,
|
|
121
|
-
"offset": offset,
|
|
122
149
|
"depth": depth,
|
|
150
|
+
"granularity": granularity,
|
|
123
151
|
},
|
|
124
152
|
)
|
|
125
|
-
return
|
|
153
|
+
return CursorResponse(
|
|
154
|
+
data=[OrderBook.model_validate(item) for item in data["data"]],
|
|
155
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
156
|
+
)
|
|
126
157
|
|
|
127
158
|
async def ahistory(
|
|
128
159
|
self,
|
|
@@ -130,19 +161,24 @@ class OrderBookResource:
|
|
|
130
161
|
*,
|
|
131
162
|
start: Timestamp,
|
|
132
163
|
end: Timestamp,
|
|
164
|
+
cursor: Optional[Timestamp] = None,
|
|
133
165
|
limit: Optional[int] = None,
|
|
134
|
-
offset: Optional[int] = None,
|
|
135
166
|
depth: Optional[int] = None,
|
|
136
|
-
|
|
137
|
-
|
|
167
|
+
granularity: Optional[LighterGranularity] = None,
|
|
168
|
+
) -> CursorResponse[list[OrderBook]]:
|
|
169
|
+
"""Async version of history(). start and end are required. See history() for granularity details."""
|
|
138
170
|
data = await self._http.aget(
|
|
139
|
-
f"/
|
|
171
|
+
f"{self._base_path}/orderbook/{coin.upper()}/history",
|
|
140
172
|
params={
|
|
141
173
|
"start": self._convert_timestamp(start),
|
|
142
174
|
"end": self._convert_timestamp(end),
|
|
175
|
+
"cursor": self._convert_timestamp(cursor),
|
|
143
176
|
"limit": limit,
|
|
144
|
-
"offset": offset,
|
|
145
177
|
"depth": depth,
|
|
178
|
+
"granularity": granularity,
|
|
146
179
|
},
|
|
147
180
|
)
|
|
148
|
-
return
|
|
181
|
+
return CursorResponse(
|
|
182
|
+
data=[OrderBook.model_validate(item) for item in data["data"]],
|
|
183
|
+
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
184
|
+
)
|
oxarchive/resources/trades.py
CHANGED
|
@@ -2,20 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import warnings
|
|
6
|
-
from dataclasses import dataclass
|
|
7
5
|
from datetime import datetime
|
|
8
6
|
from typing import Literal, Optional
|
|
9
7
|
|
|
10
8
|
from ..http import HttpClient
|
|
11
|
-
from ..types import Trade, Timestamp
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@dataclass
|
|
15
|
-
class CursorResponse:
|
|
16
|
-
"""Response with cursor for pagination."""
|
|
17
|
-
data: list[Trade]
|
|
18
|
-
next_cursor: Optional[str] = None
|
|
9
|
+
from ..types import CursorResponse, Trade, Timestamp
|
|
19
10
|
|
|
20
11
|
|
|
21
12
|
class TradesResource:
|
|
@@ -36,8 +27,9 @@ class TradesResource:
|
|
|
36
27
|
... trades.extend(result.data)
|
|
37
28
|
"""
|
|
38
29
|
|
|
39
|
-
def __init__(self, http: HttpClient):
|
|
30
|
+
def __init__(self, http: HttpClient, base_path: str = "/v1"):
|
|
40
31
|
self._http = http
|
|
32
|
+
self._base_path = base_path
|
|
41
33
|
|
|
42
34
|
def _convert_timestamp(self, ts: Optional[Timestamp]) -> Optional[int]:
|
|
43
35
|
"""Convert timestamp to Unix milliseconds."""
|
|
@@ -64,7 +56,7 @@ class TradesResource:
|
|
|
64
56
|
cursor: Optional[Timestamp] = None,
|
|
65
57
|
limit: Optional[int] = None,
|
|
66
58
|
side: Optional[Literal["buy", "sell"]] = None,
|
|
67
|
-
) -> CursorResponse:
|
|
59
|
+
) -> CursorResponse[list[Trade]]:
|
|
68
60
|
"""
|
|
69
61
|
Get trade history for a coin using cursor-based pagination.
|
|
70
62
|
|
|
@@ -95,7 +87,7 @@ class TradesResource:
|
|
|
95
87
|
... trades.extend(result.data)
|
|
96
88
|
"""
|
|
97
89
|
data = self._http.get(
|
|
98
|
-
f"/
|
|
90
|
+
f"{self._base_path}/trades/{coin.upper()}",
|
|
99
91
|
params={
|
|
100
92
|
"start": self._convert_timestamp(start),
|
|
101
93
|
"end": self._convert_timestamp(end),
|
|
@@ -118,14 +110,14 @@ class TradesResource:
|
|
|
118
110
|
cursor: Optional[Timestamp] = None,
|
|
119
111
|
limit: Optional[int] = None,
|
|
120
112
|
side: Optional[Literal["buy", "sell"]] = None,
|
|
121
|
-
) -> CursorResponse:
|
|
113
|
+
) -> CursorResponse[list[Trade]]:
|
|
122
114
|
"""
|
|
123
115
|
Async version of list().
|
|
124
116
|
|
|
125
117
|
Uses cursor-based pagination by default.
|
|
126
118
|
"""
|
|
127
119
|
data = await self._http.aget(
|
|
128
|
-
f"/
|
|
120
|
+
f"{self._base_path}/trades/{coin.upper()}",
|
|
129
121
|
params={
|
|
130
122
|
"start": self._convert_timestamp(start),
|
|
131
123
|
"end": self._convert_timestamp(end),
|
|
@@ -139,83 +131,6 @@ class TradesResource:
|
|
|
139
131
|
next_cursor=data.get("meta", {}).get("next_cursor"),
|
|
140
132
|
)
|
|
141
133
|
|
|
142
|
-
def list_with_offset(
|
|
143
|
-
self,
|
|
144
|
-
coin: str,
|
|
145
|
-
*,
|
|
146
|
-
start: Timestamp,
|
|
147
|
-
end: Timestamp,
|
|
148
|
-
limit: Optional[int] = None,
|
|
149
|
-
offset: Optional[int] = None,
|
|
150
|
-
side: Optional[Literal["buy", "sell"]] = None,
|
|
151
|
-
) -> list[Trade]:
|
|
152
|
-
"""
|
|
153
|
-
Get trade history using offset-based pagination.
|
|
154
|
-
|
|
155
|
-
.. deprecated::
|
|
156
|
-
Use list() with cursor-based pagination instead for better performance.
|
|
157
|
-
|
|
158
|
-
Args:
|
|
159
|
-
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
160
|
-
start: Start timestamp (required)
|
|
161
|
-
end: End timestamp (required)
|
|
162
|
-
limit: Maximum number of results
|
|
163
|
-
offset: Number of results to skip
|
|
164
|
-
side: Filter by trade side
|
|
165
|
-
|
|
166
|
-
Returns:
|
|
167
|
-
List of trades
|
|
168
|
-
"""
|
|
169
|
-
warnings.warn(
|
|
170
|
-
"list_with_offset() is deprecated. Use list() with cursor-based pagination instead.",
|
|
171
|
-
DeprecationWarning,
|
|
172
|
-
stacklevel=2,
|
|
173
|
-
)
|
|
174
|
-
data = self._http.get(
|
|
175
|
-
f"/v1/trades/{coin.upper()}",
|
|
176
|
-
params={
|
|
177
|
-
"start": self._convert_timestamp(start),
|
|
178
|
-
"end": self._convert_timestamp(end),
|
|
179
|
-
"limit": limit,
|
|
180
|
-
"offset": offset,
|
|
181
|
-
"side": side,
|
|
182
|
-
},
|
|
183
|
-
)
|
|
184
|
-
return [Trade.model_validate(item) for item in data["data"]]
|
|
185
|
-
|
|
186
|
-
async def alist_with_offset(
|
|
187
|
-
self,
|
|
188
|
-
coin: str,
|
|
189
|
-
*,
|
|
190
|
-
start: Timestamp,
|
|
191
|
-
end: Timestamp,
|
|
192
|
-
limit: Optional[int] = None,
|
|
193
|
-
offset: Optional[int] = None,
|
|
194
|
-
side: Optional[Literal["buy", "sell"]] = None,
|
|
195
|
-
) -> list[Trade]:
|
|
196
|
-
"""
|
|
197
|
-
Async version of list_with_offset().
|
|
198
|
-
|
|
199
|
-
.. deprecated::
|
|
200
|
-
Use alist() with cursor-based pagination instead.
|
|
201
|
-
"""
|
|
202
|
-
warnings.warn(
|
|
203
|
-
"alist_with_offset() is deprecated. Use alist() with cursor-based pagination instead.",
|
|
204
|
-
DeprecationWarning,
|
|
205
|
-
stacklevel=2,
|
|
206
|
-
)
|
|
207
|
-
data = await self._http.aget(
|
|
208
|
-
f"/v1/trades/{coin.upper()}",
|
|
209
|
-
params={
|
|
210
|
-
"start": self._convert_timestamp(start),
|
|
211
|
-
"end": self._convert_timestamp(end),
|
|
212
|
-
"limit": limit,
|
|
213
|
-
"offset": offset,
|
|
214
|
-
"side": side,
|
|
215
|
-
},
|
|
216
|
-
)
|
|
217
|
-
return [Trade.model_validate(item) for item in data["data"]]
|
|
218
|
-
|
|
219
134
|
def recent(self, coin: str, limit: Optional[int] = None) -> list[Trade]:
|
|
220
135
|
"""
|
|
221
136
|
Get most recent trades for a coin.
|
|
@@ -228,7 +143,7 @@ class TradesResource:
|
|
|
228
143
|
List of recent trades
|
|
229
144
|
"""
|
|
230
145
|
data = self._http.get(
|
|
231
|
-
f"/
|
|
146
|
+
f"{self._base_path}/trades/{coin.upper()}/recent",
|
|
232
147
|
params={"limit": limit},
|
|
233
148
|
)
|
|
234
149
|
return [Trade.model_validate(item) for item in data["data"]]
|
|
@@ -236,64 +151,7 @@ class TradesResource:
|
|
|
236
151
|
async def arecent(self, coin: str, limit: Optional[int] = None) -> list[Trade]:
|
|
237
152
|
"""Async version of recent()."""
|
|
238
153
|
data = await self._http.aget(
|
|
239
|
-
f"/
|
|
154
|
+
f"{self._base_path}/trades/{coin.upper()}/recent",
|
|
240
155
|
params={"limit": limit},
|
|
241
156
|
)
|
|
242
157
|
return [Trade.model_validate(item) for item in data["data"]]
|
|
243
|
-
|
|
244
|
-
def list_cursor(
|
|
245
|
-
self,
|
|
246
|
-
coin: str,
|
|
247
|
-
*,
|
|
248
|
-
start: Timestamp,
|
|
249
|
-
end: Timestamp,
|
|
250
|
-
cursor: Optional[Timestamp] = None,
|
|
251
|
-
limit: Optional[int] = None,
|
|
252
|
-
side: Optional[Literal["buy", "sell"]] = None,
|
|
253
|
-
) -> CursorResponse:
|
|
254
|
-
"""
|
|
255
|
-
Get trade history using cursor-based pagination.
|
|
256
|
-
|
|
257
|
-
.. deprecated::
|
|
258
|
-
Use list() instead - it now uses cursor-based pagination by default.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
262
|
-
start: Start timestamp (required)
|
|
263
|
-
end: End timestamp (required)
|
|
264
|
-
cursor: Cursor from previous response's next_cursor (timestamp)
|
|
265
|
-
limit: Maximum number of results
|
|
266
|
-
side: Filter by trade side
|
|
267
|
-
|
|
268
|
-
Returns:
|
|
269
|
-
CursorResponse with trades and next_cursor for pagination
|
|
270
|
-
"""
|
|
271
|
-
warnings.warn(
|
|
272
|
-
"list_cursor() is deprecated. Use list() instead - it now uses cursor-based pagination by default.",
|
|
273
|
-
DeprecationWarning,
|
|
274
|
-
stacklevel=2,
|
|
275
|
-
)
|
|
276
|
-
return self.list(coin, start=start, end=end, cursor=cursor, limit=limit, side=side)
|
|
277
|
-
|
|
278
|
-
async def alist_cursor(
|
|
279
|
-
self,
|
|
280
|
-
coin: str,
|
|
281
|
-
*,
|
|
282
|
-
start: Timestamp,
|
|
283
|
-
end: Timestamp,
|
|
284
|
-
cursor: Optional[Timestamp] = None,
|
|
285
|
-
limit: Optional[int] = None,
|
|
286
|
-
side: Optional[Literal["buy", "sell"]] = None,
|
|
287
|
-
) -> CursorResponse:
|
|
288
|
-
"""
|
|
289
|
-
Async version of list_cursor().
|
|
290
|
-
|
|
291
|
-
.. deprecated::
|
|
292
|
-
Use alist() instead - it now uses cursor-based pagination by default.
|
|
293
|
-
"""
|
|
294
|
-
warnings.warn(
|
|
295
|
-
"alist_cursor() is deprecated. Use alist() instead - it now uses cursor-based pagination by default.",
|
|
296
|
-
DeprecationWarning,
|
|
297
|
-
stacklevel=2,
|
|
298
|
-
)
|
|
299
|
-
return await self.alist(coin, start=start, end=end, cursor=cursor, limit=limit, side=side)
|
oxarchive/types.py
CHANGED
|
@@ -124,9 +124,6 @@ class Trade(BaseModel):
|
|
|
124
124
|
start_position: Optional[str] = None
|
|
125
125
|
"""Position size before this trade."""
|
|
126
126
|
|
|
127
|
-
source: Optional[Literal["s3", "ws", "api", "live"]] = None
|
|
128
|
-
"""Data source: 's3' (historical), 'api' (REST backfill), 'ws' (websocket), 'live' (real-time ingestion)."""
|
|
129
|
-
|
|
130
127
|
user_address: Optional[str] = None
|
|
131
128
|
"""User's wallet address (for fill-level data)."""
|
|
132
129
|
|
|
@@ -143,7 +140,7 @@ class Trade(BaseModel):
|
|
|
143
140
|
|
|
144
141
|
|
|
145
142
|
class Instrument(BaseModel):
|
|
146
|
-
"""Trading instrument specification."""
|
|
143
|
+
"""Trading instrument specification (Hyperliquid)."""
|
|
147
144
|
|
|
148
145
|
model_config = {"populate_by_name": True}
|
|
149
146
|
|
|
@@ -166,6 +163,53 @@ class Instrument(BaseModel):
|
|
|
166
163
|
"""Whether the instrument is currently tradeable."""
|
|
167
164
|
|
|
168
165
|
|
|
166
|
+
class LighterInstrument(BaseModel):
|
|
167
|
+
"""Trading instrument specification (Lighter.xyz).
|
|
168
|
+
|
|
169
|
+
Lighter instruments have a different schema than Hyperliquid with more
|
|
170
|
+
detailed market configuration including fees and minimum amounts.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
symbol: str
|
|
174
|
+
"""Instrument symbol (e.g., BTC, ETH)."""
|
|
175
|
+
|
|
176
|
+
market_id: int
|
|
177
|
+
"""Unique market identifier."""
|
|
178
|
+
|
|
179
|
+
market_type: str
|
|
180
|
+
"""Market type (e.g., 'perp')."""
|
|
181
|
+
|
|
182
|
+
status: str
|
|
183
|
+
"""Market status (e.g., 'active')."""
|
|
184
|
+
|
|
185
|
+
taker_fee: float
|
|
186
|
+
"""Taker fee rate (e.g., 0.0005 = 0.05%)."""
|
|
187
|
+
|
|
188
|
+
maker_fee: float
|
|
189
|
+
"""Maker fee rate (e.g., 0.0002 = 0.02%)."""
|
|
190
|
+
|
|
191
|
+
liquidation_fee: float
|
|
192
|
+
"""Liquidation fee rate."""
|
|
193
|
+
|
|
194
|
+
min_base_amount: float
|
|
195
|
+
"""Minimum order size in base currency."""
|
|
196
|
+
|
|
197
|
+
min_quote_amount: float
|
|
198
|
+
"""Minimum order size in quote currency."""
|
|
199
|
+
|
|
200
|
+
size_decimals: int
|
|
201
|
+
"""Size decimal precision."""
|
|
202
|
+
|
|
203
|
+
price_decimals: int
|
|
204
|
+
"""Price decimal precision."""
|
|
205
|
+
|
|
206
|
+
quote_decimals: int
|
|
207
|
+
"""Quote currency decimal precision."""
|
|
208
|
+
|
|
209
|
+
is_active: bool
|
|
210
|
+
"""Whether the instrument is currently tradeable."""
|
|
211
|
+
|
|
212
|
+
|
|
169
213
|
# =============================================================================
|
|
170
214
|
# Funding Types
|
|
171
215
|
# =============================================================================
|
|
@@ -338,6 +382,41 @@ class WsHistoricalData(BaseModel):
|
|
|
338
382
|
data: dict[str, Any]
|
|
339
383
|
|
|
340
384
|
|
|
385
|
+
class OrderbookDelta(BaseModel):
|
|
386
|
+
"""Orderbook delta for tick-level data."""
|
|
387
|
+
|
|
388
|
+
timestamp: int
|
|
389
|
+
"""Timestamp in milliseconds."""
|
|
390
|
+
|
|
391
|
+
side: Literal["bid", "ask"]
|
|
392
|
+
"""Side: 'bid' or 'ask'."""
|
|
393
|
+
|
|
394
|
+
price: float
|
|
395
|
+
"""Price level."""
|
|
396
|
+
|
|
397
|
+
size: float
|
|
398
|
+
"""New size (0 = level removed)."""
|
|
399
|
+
|
|
400
|
+
sequence: int
|
|
401
|
+
"""Sequence number for ordering."""
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
class WsHistoricalTickData(BaseModel):
|
|
405
|
+
"""Historical tick data (granularity='tick' mode) - checkpoint + deltas.
|
|
406
|
+
|
|
407
|
+
This message type is sent when using granularity='tick' for Lighter.xyz
|
|
408
|
+
orderbook data. It provides a full checkpoint followed by incremental deltas.
|
|
409
|
+
"""
|
|
410
|
+
|
|
411
|
+
type: Literal["historical_tick_data"]
|
|
412
|
+
channel: WsChannel
|
|
413
|
+
coin: str
|
|
414
|
+
checkpoint: dict[str, Any]
|
|
415
|
+
"""Initial checkpoint (full orderbook snapshot)."""
|
|
416
|
+
deltas: list[OrderbookDelta]
|
|
417
|
+
"""Incremental deltas to apply after checkpoint."""
|
|
418
|
+
|
|
419
|
+
|
|
341
420
|
# =============================================================================
|
|
342
421
|
# WebSocket Bulk Stream Types (Bulk Download Mode)
|
|
343
422
|
# =============================================================================
|
|
@@ -414,6 +493,21 @@ class OxArchiveError(Exception):
|
|
|
414
493
|
return f"[{self.code}] {self.message}"
|
|
415
494
|
|
|
416
495
|
|
|
496
|
+
# =============================================================================
|
|
497
|
+
# Pagination Types
|
|
498
|
+
# =============================================================================
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class CursorResponse(BaseModel, Generic[T]):
|
|
502
|
+
"""Response with cursor for pagination."""
|
|
503
|
+
|
|
504
|
+
data: T
|
|
505
|
+
"""The paginated data."""
|
|
506
|
+
|
|
507
|
+
next_cursor: Optional[str] = None
|
|
508
|
+
"""Cursor for the next page (use as cursor parameter)."""
|
|
509
|
+
|
|
510
|
+
|
|
417
511
|
# Type alias for timestamp parameters
|
|
418
512
|
Timestamp = Union[int, str, datetime]
|
|
419
513
|
"""Timestamp can be Unix ms (int), ISO string, or datetime object."""
|
oxarchive/websocket.py
CHANGED
|
@@ -31,17 +31,18 @@ from dataclasses import dataclass, field
|
|
|
31
31
|
from typing import Any, Callable, Optional, Set, Union
|
|
32
32
|
|
|
33
33
|
try:
|
|
34
|
-
import
|
|
35
|
-
from websockets.
|
|
34
|
+
from websockets.asyncio.client import connect as ws_connect, ClientConnection
|
|
35
|
+
from websockets.exceptions import ConnectionClosed
|
|
36
36
|
from websockets.protocol import State as WsState
|
|
37
37
|
except ImportError:
|
|
38
38
|
raise ImportError(
|
|
39
|
-
"WebSocket support requires the 'websockets' package. "
|
|
39
|
+
"WebSocket support requires the 'websockets' package (>=14.0). "
|
|
40
40
|
"Install with: pip install oxarchive[websocket]"
|
|
41
41
|
)
|
|
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
|
|
|
@@ -147,7 +150,6 @@ def _transform_trade(coin: str, raw: dict) -> Trade:
|
|
|
147
150
|
"closed_pnl": raw.get("closed_pnl") or raw.get("closedPnl"),
|
|
148
151
|
"direction": raw.get("direction"),
|
|
149
152
|
"start_position": raw.get("start_position") or raw.get("startPosition"),
|
|
150
|
-
"source": raw.get("source"),
|
|
151
153
|
"user_address": raw.get("user_address") or raw.get("userAddress"),
|
|
152
154
|
"maker_address": raw.get("maker_address"),
|
|
153
155
|
"taker_address": raw.get("taker_address"),
|
|
@@ -258,7 +260,7 @@ class OxArchiveWs:
|
|
|
258
260
|
options: WebSocket connection options
|
|
259
261
|
"""
|
|
260
262
|
self.options = options
|
|
261
|
-
self._ws: Optional[
|
|
263
|
+
self._ws: Optional[ClientConnection] = None
|
|
262
264
|
self._state: WsConnectionState = "disconnected"
|
|
263
265
|
self._subscriptions: Set[str] = set()
|
|
264
266
|
self._reconnect_attempts = 0
|
|
@@ -277,6 +279,7 @@ class OxArchiveWs:
|
|
|
277
279
|
|
|
278
280
|
# Replay handlers (Option B)
|
|
279
281
|
self._on_historical_data: Optional[HistoricalDataHandler] = None
|
|
282
|
+
self._on_historical_tick_data: Optional[HistoricalTickDataHandler] = None
|
|
280
283
|
self._on_replay_start: Optional[ReplayStartHandler] = None
|
|
281
284
|
self._on_replay_complete: Optional[ReplayCompleteHandler] = None
|
|
282
285
|
|
|
@@ -308,7 +311,8 @@ class OxArchiveWs:
|
|
|
308
311
|
url = f"{self.options.ws_url}?apiKey={self.options.api_key}"
|
|
309
312
|
|
|
310
313
|
try:
|
|
311
|
-
|
|
314
|
+
# Increase max_size from default 1MB to 10MB for large Lighter orderbook data
|
|
315
|
+
self._ws = await ws_connect(url, max_size=10 * 1024 * 1024)
|
|
312
316
|
self._reconnect_attempts = 0
|
|
313
317
|
self._set_state("connected")
|
|
314
318
|
|
|
@@ -428,6 +432,7 @@ class OxArchiveWs:
|
|
|
428
432
|
start: int,
|
|
429
433
|
end: Optional[int] = None,
|
|
430
434
|
speed: float = 1.0,
|
|
435
|
+
granularity: Optional[str] = None,
|
|
431
436
|
) -> None:
|
|
432
437
|
"""Start historical replay with timing preserved.
|
|
433
438
|
|
|
@@ -437,6 +442,7 @@ class OxArchiveWs:
|
|
|
437
442
|
start: Start timestamp (Unix ms)
|
|
438
443
|
end: End timestamp (Unix ms, defaults to now)
|
|
439
444
|
speed: Playback speed multiplier (1 = real-time, 10 = 10x faster)
|
|
445
|
+
granularity: Data resolution for Lighter orderbook ('checkpoint', '30s', '10s', '1s', 'tick')
|
|
440
446
|
|
|
441
447
|
Example:
|
|
442
448
|
>>> await ws.replay("orderbook", "BTC", start=time.time()*1000 - 86400000, speed=10)
|
|
@@ -450,6 +456,8 @@ class OxArchiveWs:
|
|
|
450
456
|
}
|
|
451
457
|
if end is not None:
|
|
452
458
|
msg["end"] = end
|
|
459
|
+
if granularity is not None:
|
|
460
|
+
msg["granularity"] = granularity
|
|
453
461
|
await self._send(msg)
|
|
454
462
|
|
|
455
463
|
async def replay_pause(self) -> None:
|
|
@@ -483,6 +491,7 @@ class OxArchiveWs:
|
|
|
483
491
|
start: int,
|
|
484
492
|
end: int,
|
|
485
493
|
batch_size: int = 1000,
|
|
494
|
+
granularity: Optional[str] = None,
|
|
486
495
|
) -> None:
|
|
487
496
|
"""Start bulk streaming for fast data download.
|
|
488
497
|
|
|
@@ -492,18 +501,22 @@ class OxArchiveWs:
|
|
|
492
501
|
start: Start timestamp (Unix ms)
|
|
493
502
|
end: End timestamp (Unix ms)
|
|
494
503
|
batch_size: Records per batch message
|
|
504
|
+
granularity: Data resolution for Lighter orderbook ('checkpoint', '30s', '10s', '1s', 'tick')
|
|
495
505
|
|
|
496
506
|
Example:
|
|
497
507
|
>>> await ws.stream("orderbook", "ETH", start=..., end=..., batch_size=1000)
|
|
498
508
|
"""
|
|
499
|
-
|
|
509
|
+
msg = {
|
|
500
510
|
"op": "stream",
|
|
501
511
|
"channel": channel,
|
|
502
512
|
"coin": coin,
|
|
503
513
|
"start": start,
|
|
504
514
|
"end": end,
|
|
505
515
|
"batch_size": batch_size,
|
|
506
|
-
}
|
|
516
|
+
}
|
|
517
|
+
if granularity is not None:
|
|
518
|
+
msg["granularity"] = granularity
|
|
519
|
+
await self._send(msg)
|
|
507
520
|
|
|
508
521
|
async def stream_stop(self) -> None:
|
|
509
522
|
"""Stop the current bulk stream."""
|
|
@@ -548,6 +561,16 @@ class OxArchiveWs:
|
|
|
548
561
|
"""
|
|
549
562
|
self._on_historical_data = handler
|
|
550
563
|
|
|
564
|
+
def on_historical_tick_data(self, handler: HistoricalTickDataHandler) -> None:
|
|
565
|
+
"""Set handler for historical tick data (granularity='tick' mode).
|
|
566
|
+
|
|
567
|
+
This is for tick-level granularity on Lighter.xyz orderbook data.
|
|
568
|
+
Receives a checkpoint (full orderbook) followed by incremental deltas.
|
|
569
|
+
|
|
570
|
+
Handler receives: (coin, checkpoint, deltas)
|
|
571
|
+
"""
|
|
572
|
+
self._on_historical_tick_data = handler
|
|
573
|
+
|
|
551
574
|
def on_replay_start(self, handler: ReplayStartHandler) -> None:
|
|
552
575
|
"""Set handler for replay started event.
|
|
553
576
|
|
|
@@ -652,7 +675,7 @@ class OxArchiveWs:
|
|
|
652
675
|
try:
|
|
653
676
|
message = await self._ws.recv()
|
|
654
677
|
self._handle_message(message)
|
|
655
|
-
except
|
|
678
|
+
except ConnectionClosed as e:
|
|
656
679
|
logger.info(f"Connection closed: {e.code} {e.reason}")
|
|
657
680
|
if self._on_close:
|
|
658
681
|
self._on_close(e.code, e.reason)
|
|
@@ -723,6 +746,10 @@ class OxArchiveWs:
|
|
|
723
746
|
elif msg_type == "historical_data" and self._on_historical_data:
|
|
724
747
|
self._on_historical_data(data["coin"], data["timestamp"], data["data"])
|
|
725
748
|
|
|
749
|
+
elif msg_type == "historical_tick_data" and self._on_historical_tick_data:
|
|
750
|
+
msg = WsHistoricalTickData(**data)
|
|
751
|
+
self._on_historical_tick_data(msg.coin, msg.checkpoint, msg.deltas)
|
|
752
|
+
|
|
726
753
|
elif msg_type == "replay_completed" and self._on_replay_complete:
|
|
727
754
|
self._on_replay_complete(data["channel"], data["coin"], data["snapshots_sent"])
|
|
728
755
|
|