oxarchive 0.3.6__py3-none-any.whl → 0.4.3__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 +63 -4
- oxarchive/websocket.py +6 -7
- {oxarchive-0.3.6.dist-info → oxarchive-0.4.3.dist-info}/METADATA +140 -39
- oxarchive-0.4.3.dist-info/RECORD +15 -0
- oxarchive-0.3.6.dist-info/RECORD +0 -14
- {oxarchive-0.3.6.dist-info → oxarchive-0.4.3.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
|
# =============================================================================
|
|
@@ -414,6 +458,21 @@ class OxArchiveError(Exception):
|
|
|
414
458
|
return f"[{self.code}] {self.message}"
|
|
415
459
|
|
|
416
460
|
|
|
461
|
+
# =============================================================================
|
|
462
|
+
# Pagination Types
|
|
463
|
+
# =============================================================================
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
class CursorResponse(BaseModel, Generic[T]):
|
|
467
|
+
"""Response with cursor for pagination."""
|
|
468
|
+
|
|
469
|
+
data: T
|
|
470
|
+
"""The paginated data."""
|
|
471
|
+
|
|
472
|
+
next_cursor: Optional[str] = None
|
|
473
|
+
"""Cursor for the next page (use as cursor parameter)."""
|
|
474
|
+
|
|
475
|
+
|
|
417
476
|
# Type alias for timestamp parameters
|
|
418
477
|
Timestamp = Union[int, str, datetime]
|
|
419
478
|
"""Timestamp can be Unix ms (int), ISO string, or datetime object."""
|
oxarchive/websocket.py
CHANGED
|
@@ -31,12 +31,12 @@ 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
|
|
|
@@ -147,7 +147,6 @@ def _transform_trade(coin: str, raw: dict) -> Trade:
|
|
|
147
147
|
"closed_pnl": raw.get("closed_pnl") or raw.get("closedPnl"),
|
|
148
148
|
"direction": raw.get("direction"),
|
|
149
149
|
"start_position": raw.get("start_position") or raw.get("startPosition"),
|
|
150
|
-
"source": raw.get("source"),
|
|
151
150
|
"user_address": raw.get("user_address") or raw.get("userAddress"),
|
|
152
151
|
"maker_address": raw.get("maker_address"),
|
|
153
152
|
"taker_address": raw.get("taker_address"),
|
|
@@ -258,7 +257,7 @@ class OxArchiveWs:
|
|
|
258
257
|
options: WebSocket connection options
|
|
259
258
|
"""
|
|
260
259
|
self.options = options
|
|
261
|
-
self._ws: Optional[
|
|
260
|
+
self._ws: Optional[ClientConnection] = None
|
|
262
261
|
self._state: WsConnectionState = "disconnected"
|
|
263
262
|
self._subscriptions: Set[str] = set()
|
|
264
263
|
self._reconnect_attempts = 0
|
|
@@ -308,7 +307,7 @@ class OxArchiveWs:
|
|
|
308
307
|
url = f"{self.options.ws_url}?apiKey={self.options.api_key}"
|
|
309
308
|
|
|
310
309
|
try:
|
|
311
|
-
self._ws = await
|
|
310
|
+
self._ws = await ws_connect(url)
|
|
312
311
|
self._reconnect_attempts = 0
|
|
313
312
|
self._set_state("connected")
|
|
314
313
|
|
|
@@ -652,7 +651,7 @@ class OxArchiveWs:
|
|
|
652
651
|
try:
|
|
653
652
|
message = await self._ws.recv()
|
|
654
653
|
self._handle_message(message)
|
|
655
|
-
except
|
|
654
|
+
except ConnectionClosed as e:
|
|
656
655
|
logger.info(f"Connection closed: {e.code} {e.reason}")
|
|
657
656
|
if self._on_close:
|
|
658
657
|
self._on_close(e.code, e.reason)
|