oxarchive 0.1.0__tar.gz → 0.2.0__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.1.0
3
+ Version: 0.2.0
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
@@ -345,7 +345,7 @@ asyncio.run(main())
345
345
  ```python
346
346
  ws = OxArchiveWs(WsOptions(
347
347
  api_key="ox_your_api_key",
348
- ws_url="wss://ws.0xarchive.io", # Optional
348
+ ws_url="wss://api.0xarchive.io/ws", # Optional
349
349
  auto_reconnect=True, # Auto-reconnect on disconnect
350
350
  reconnect_delay=1.0, # Initial reconnect delay (seconds)
351
351
  max_reconnect_attempts=10, # Max reconnect attempts
@@ -309,7 +309,7 @@ asyncio.run(main())
309
309
  ```python
310
310
  ws = OxArchiveWs(WsOptions(
311
311
  api_key="ox_your_api_key",
312
- ws_url="wss://ws.0xarchive.io", # Optional
312
+ ws_url="wss://api.0xarchive.io/ws", # Optional
313
313
  auto_reconnect=True, # Auto-reconnect on disconnect
314
314
  reconnect_delay=1.0, # Initial reconnect delay (seconds)
315
315
  max_reconnect_attempts=10, # Max reconnect attempts
@@ -8,7 +8,6 @@ from .http import HttpClient
8
8
  from .resources import (
9
9
  OrderBookResource,
10
10
  TradesResource,
11
- CandlesResource,
12
11
  InstrumentsResource,
13
12
  FundingResource,
14
13
  OpenInterestResource,
@@ -81,9 +80,6 @@ class Client:
81
80
  self.trades = TradesResource(self._http)
82
81
  """Trade/fill history"""
83
82
 
84
- self.candles = CandlesResource(self._http)
85
- """OHLCV candles"""
86
-
87
83
  self.instruments = InstrumentsResource(self._http)
88
84
  """Trading instruments metadata"""
89
85
 
@@ -2,7 +2,6 @@
2
2
 
3
3
  from .orderbook import OrderBookResource
4
4
  from .trades import TradesResource
5
- from .candles import CandlesResource
6
5
  from .instruments import InstrumentsResource
7
6
  from .funding import FundingResource
8
7
  from .openinterest import OpenInterestResource
@@ -10,7 +9,6 @@ from .openinterest import OpenInterestResource
10
9
  __all__ = [
11
10
  "OrderBookResource",
12
11
  "TradesResource",
13
- "CandlesResource",
14
12
  "InstrumentsResource",
15
13
  "FundingResource",
16
14
  "OpenInterestResource",
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from datetime import datetime
6
- from typing import Literal, Optional, Union
6
+ from typing import Any, Generic, Literal, Optional, TypeVar, Union
7
7
 
8
8
  from pydantic import BaseModel, Field
9
9
 
@@ -12,69 +12,123 @@ from pydantic import BaseModel, Field
12
12
  # Base Types
13
13
  # =============================================================================
14
14
 
15
+ T = TypeVar("T")
15
16
 
16
- class ApiResponse(BaseModel):
17
- """Standard API response wrapper."""
18
17
 
19
- success: bool
18
+ class ApiMeta(BaseModel):
19
+ """Response metadata."""
20
+
20
21
  count: int
22
+ next_cursor: Optional[str] = None
21
23
  request_id: str
22
24
 
23
25
 
26
+ class ApiResponse(BaseModel, Generic[T]):
27
+ """Standard API response wrapper."""
28
+
29
+ success: bool
30
+ data: T
31
+ meta: ApiMeta
32
+
33
+
24
34
  # =============================================================================
25
35
  # Order Book Types
26
36
  # =============================================================================
27
37
 
28
38
 
39
+ class PriceLevel(BaseModel):
40
+ """Single price level in the order book."""
41
+
42
+ px: str
43
+ """Price at this level."""
44
+
45
+ sz: str
46
+ """Total size at this price level."""
47
+
48
+ n: int
49
+ """Number of orders at this level."""
50
+
51
+
29
52
  class OrderBook(BaseModel):
30
- """Order book snapshot."""
53
+ """L2 order book snapshot."""
31
54
 
32
55
  coin: str
33
- timestamp: int
34
- bids: list[tuple[str, str]]
35
- asks: list[tuple[str, str]]
36
- mid_price: str
37
- spread: str
38
- spread_bps: str
56
+ """Trading pair symbol (e.g., BTC, ETH)."""
57
+
58
+ timestamp: datetime
59
+ """Snapshot timestamp (UTC)."""
60
+
61
+ bids: list[PriceLevel]
62
+ """Bid price levels (best bid first)."""
63
+
64
+ asks: list[PriceLevel]
65
+ """Ask price levels (best ask first)."""
66
+
67
+ mid_price: Optional[str] = None
68
+ """Mid price (best bid + best ask) / 2."""
69
+
70
+ spread: Optional[str] = None
71
+ """Spread in absolute terms (best ask - best bid)."""
72
+
73
+ spread_bps: Optional[str] = None
74
+ """Spread in basis points."""
39
75
 
40
76
 
41
77
  # =============================================================================
42
- # Trade Types
78
+ # Trade/Fill Types
43
79
  # =============================================================================
44
80
 
45
81
 
46
82
  class Trade(BaseModel):
47
- """Trade/fill record."""
83
+ """Trade/fill record with full execution details."""
48
84
 
49
- id: str
50
85
  coin: str
51
- side: Literal["buy", "sell"]
86
+ """Trading pair symbol."""
87
+
88
+ side: Literal["A", "B"]
89
+ """Trade side: 'B' (buy) or 'A' (sell/ask)."""
90
+
52
91
  price: str
92
+ """Execution price."""
93
+
53
94
  size: str
54
- value: str
55
- timestamp: int
56
- trade_type: str
95
+ """Trade size."""
57
96
 
97
+ timestamp: datetime
98
+ """Execution timestamp (UTC)."""
58
99
 
59
- # =============================================================================
60
- # Candle Types
61
- # =============================================================================
100
+ tx_hash: Optional[str] = None
101
+ """Blockchain transaction hash."""
62
102
 
63
- CandleInterval = Literal["1m", "5m", "15m", "1h", "4h", "1d"]
103
+ trade_id: Optional[int] = None
104
+ """Unique trade ID."""
64
105
 
106
+ order_id: Optional[int] = None
107
+ """Associated order ID."""
65
108
 
66
- class Candle(BaseModel):
67
- """OHLCV candle."""
109
+ crossed: Optional[bool] = None
110
+ """True if taker (crossed the spread), false if maker."""
68
111
 
69
- coin: str
70
- interval: str
71
- timestamp: int
72
- open: str
73
- high: str
74
- low: str
75
- close: str
76
- volume: str
77
- trades: int
112
+ fee: Optional[str] = None
113
+ """Trading fee amount."""
114
+
115
+ fee_token: Optional[str] = None
116
+ """Fee denomination (e.g., USDC)."""
117
+
118
+ closed_pnl: Optional[str] = None
119
+ """Realized PnL if closing a position."""
120
+
121
+ direction: Optional[Literal["Open Long", "Open Short", "Close Long", "Close Short"]] = None
122
+ """Position direction."""
123
+
124
+ start_position: Optional[str] = None
125
+ """Position size before this trade."""
126
+
127
+ source: Optional[Literal["s3", "ws", "api"]] = None
128
+ """Data source."""
129
+
130
+ user_address: Optional[str] = None
131
+ """User's wallet address."""
78
132
 
79
133
 
80
134
  # =============================================================================
@@ -83,14 +137,25 @@ class Candle(BaseModel):
83
137
 
84
138
 
85
139
  class Instrument(BaseModel):
86
- """Trading instrument metadata."""
140
+ """Trading instrument specification."""
87
141
 
88
- coin: str
89
142
  name: str
143
+ """Instrument symbol (e.g., BTC)."""
144
+
90
145
  sz_decimals: int
91
- max_leverage: int
92
- only_isolated: bool
93
- is_active: bool
146
+ """Size decimal precision."""
147
+
148
+ max_leverage: Optional[int] = None
149
+ """Maximum leverage allowed."""
150
+
151
+ only_isolated: Optional[bool] = None
152
+ """If true, only isolated margin mode is allowed."""
153
+
154
+ instrument_type: Optional[Literal["perp", "spot"]] = None
155
+ """Type of instrument."""
156
+
157
+ is_active: bool = True
158
+ """Whether the instrument is currently tradeable."""
94
159
 
95
160
 
96
161
  # =============================================================================
@@ -102,9 +167,16 @@ class FundingRate(BaseModel):
102
167
  """Funding rate record."""
103
168
 
104
169
  coin: str
170
+ """Trading pair symbol."""
171
+
172
+ timestamp: datetime
173
+ """Funding timestamp (UTC)."""
174
+
105
175
  funding_rate: str
106
- premium: str
107
- timestamp: int
176
+ """Funding rate as decimal (e.g., 0.0001 = 0.01%)."""
177
+
178
+ premium: Optional[str] = None
179
+ """Premium component of funding rate."""
108
180
 
109
181
 
110
182
  # =============================================================================
@@ -113,19 +185,48 @@ class FundingRate(BaseModel):
113
185
 
114
186
 
115
187
  class OpenInterest(BaseModel):
116
- """Open interest record."""
188
+ """Open interest snapshot with market context."""
117
189
 
118
190
  coin: str
191
+ """Trading pair symbol."""
192
+
193
+ timestamp: datetime
194
+ """Snapshot timestamp (UTC)."""
195
+
119
196
  open_interest: str
120
- timestamp: int
197
+ """Total open interest in contracts."""
198
+
199
+ mark_price: Optional[str] = None
200
+ """Mark price used for liquidations."""
201
+
202
+ oracle_price: Optional[str] = None
203
+ """Oracle price from external feed."""
204
+
205
+ day_ntl_volume: Optional[str] = None
206
+ """24-hour notional volume."""
207
+
208
+ prev_day_price: Optional[str] = None
209
+ """Price 24 hours ago."""
210
+
211
+ mid_price: Optional[str] = None
212
+ """Current mid price."""
213
+
214
+ impact_bid_price: Optional[str] = None
215
+ """Impact bid price for liquidations."""
216
+
217
+ impact_ask_price: Optional[str] = None
218
+ """Impact ask price for liquidations."""
121
219
 
122
220
 
123
221
  # =============================================================================
124
222
  # WebSocket Types
125
223
  # =============================================================================
126
224
 
127
- WsChannel = Literal["orderbook", "trades", "ticker", "all_tickers", "candles", "funding", "openinterest"]
225
+ WsChannel = Literal["orderbook", "trades", "ticker", "all_tickers"]
226
+ """Available WebSocket channels. Note: ticker/all_tickers are real-time only."""
227
+
128
228
  WsConnectionState = Literal["connecting", "connected", "disconnected", "reconnecting"]
229
+ """WebSocket connection state."""
129
230
 
130
231
 
131
232
  class WsSubscribed(BaseModel):
@@ -158,16 +259,16 @@ class WsError(BaseModel):
158
259
 
159
260
 
160
261
  class WsData(BaseModel):
161
- """Data message from server."""
262
+ """Real-time data message from server."""
162
263
 
163
264
  type: Literal["data"]
164
265
  channel: WsChannel
165
266
  coin: str
166
- data: dict
267
+ data: dict[str, Any]
167
268
 
168
269
 
169
270
  # =============================================================================
170
- # WebSocket Replay Types (Option B - Like Tardis.dev)
271
+ # WebSocket Replay Types (Historical Replay Mode)
171
272
  # =============================================================================
172
273
 
173
274
 
@@ -178,9 +279,11 @@ class WsReplayStarted(BaseModel):
178
279
  channel: WsChannel
179
280
  coin: str
180
281
  start: int
282
+ """Start timestamp in milliseconds."""
181
283
  end: int
284
+ """End timestamp in milliseconds."""
182
285
  speed: float
183
- total_records: int
286
+ """Playback speed multiplier."""
184
287
 
185
288
 
186
289
  class WsReplayPaused(BaseModel):
@@ -203,7 +306,7 @@ class WsReplayCompleted(BaseModel):
203
306
  type: Literal["replay_completed"]
204
307
  channel: WsChannel
205
308
  coin: str
206
- records_sent: int
309
+ snapshots_sent: int
207
310
 
208
311
 
209
312
  class WsReplayStopped(BaseModel):
@@ -219,11 +322,11 @@ class WsHistoricalData(BaseModel):
219
322
  channel: WsChannel
220
323
  coin: str
221
324
  timestamp: int
222
- data: dict
325
+ data: dict[str, Any]
223
326
 
224
327
 
225
328
  # =============================================================================
226
- # WebSocket Bulk Stream Types (Option D - Like Databento)
329
+ # WebSocket Bulk Stream Types (Bulk Download Mode)
227
330
  # =============================================================================
228
331
 
229
332
 
@@ -234,35 +337,32 @@ class WsStreamStarted(BaseModel):
234
337
  channel: WsChannel
235
338
  coin: str
236
339
  start: int
340
+ """Start timestamp in milliseconds."""
237
341
  end: int
238
- batch_size: int
239
- total_records: int
342
+ """End timestamp in milliseconds."""
240
343
 
241
344
 
242
345
  class WsStreamProgress(BaseModel):
243
- """Stream progress response."""
346
+ """Stream progress response (sent every ~2 seconds)."""
244
347
 
245
348
  type: Literal["stream_progress"]
246
- records_sent: int
247
- total_records: int
248
- progress_pct: float
349
+ snapshots_sent: int
249
350
 
250
351
 
251
352
  class TimestampedRecord(BaseModel):
252
- """A record with timestamp."""
353
+ """A record with timestamp for batched data."""
253
354
 
254
355
  timestamp: int
255
- data: dict
356
+ data: dict[str, Any]
256
357
 
257
358
 
258
359
  class WsHistoricalBatch(BaseModel):
259
- """Stream batch (bulk data)."""
360
+ """Batch of historical data (bulk streaming)."""
260
361
 
261
362
  type: Literal["historical_batch"]
262
363
  channel: WsChannel
263
364
  coin: str
264
- batch_index: int
265
- records: list[TimestampedRecord]
365
+ data: list[TimestampedRecord]
266
366
 
267
367
 
268
368
  class WsStreamCompleted(BaseModel):
@@ -271,13 +371,14 @@ class WsStreamCompleted(BaseModel):
271
371
  type: Literal["stream_completed"]
272
372
  channel: WsChannel
273
373
  coin: str
274
- records_sent: int
374
+ snapshots_sent: int
275
375
 
276
376
 
277
377
  class WsStreamStopped(BaseModel):
278
378
  """Stream stopped response."""
279
379
 
280
380
  type: Literal["stream_stopped"]
381
+ snapshots_sent: int
281
382
 
282
383
 
283
384
  # =============================================================================
@@ -302,3 +403,4 @@ class OxArchiveError(Exception):
302
403
 
303
404
  # Type alias for timestamp parameters
304
405
  Timestamp = Union[int, str, datetime]
406
+ """Timestamp can be Unix ms (int), ISO string, or datetime object."""
@@ -65,15 +65,32 @@ from .types import (
65
65
 
66
66
  logger = logging.getLogger("oxarchive.websocket")
67
67
 
68
- DEFAULT_WS_URL = "wss://ws.0xarchive.io"
68
+ DEFAULT_WS_URL = "wss://api.0xarchive.io/ws"
69
69
  DEFAULT_PING_INTERVAL = 30
70
70
  DEFAULT_RECONNECT_DELAY = 1.0
71
71
  DEFAULT_MAX_RECONNECT_ATTEMPTS = 10
72
72
 
73
+ # Server idle timeout is 60 seconds. The SDK sends pings every 30 seconds
74
+ # to keep the connection alive. The websockets library also automatically
75
+ # responds to WebSocket protocol-level ping frames from the server.
76
+
73
77
 
74
78
  @dataclass
75
79
  class WsOptions:
76
- """WebSocket connection options."""
80
+ """WebSocket connection options.
81
+
82
+ The server sends WebSocket ping frames every 30 seconds and will disconnect
83
+ idle connections after 60 seconds. This SDK automatically handles keep-alive
84
+ by sending application-level pings at the configured interval.
85
+
86
+ Attributes:
87
+ api_key: Your 0xarchive API key
88
+ ws_url: WebSocket server URL (default: wss://api.0xarchive.io/ws)
89
+ auto_reconnect: Automatically reconnect on disconnect (default: True)
90
+ reconnect_delay: Initial reconnect delay in seconds (default: 1.0)
91
+ max_reconnect_attempts: Maximum reconnection attempts (default: 10)
92
+ ping_interval: Interval between keep-alive pings in seconds (default: 30)
93
+ """
77
94
 
78
95
  api_key: str
79
96
  ws_url: str = DEFAULT_WS_URL
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oxarchive"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Official Python SDK for 0xarchive - Hyperliquid Historical Data API"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,100 +0,0 @@
1
- """Candles 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, Timestamp
10
-
11
-
12
- class CandlesResource:
13
- """
14
- Candles (OHLCV) API resource.
15
-
16
- Example:
17
- >>> # Get hourly candles
18
- >>> candles = client.candles.list("BTC", interval="1h")
19
- >>>
20
- >>> # Get daily candles for a date range
21
- >>> daily = client.candles.list("ETH", interval="1d", start="2024-01-01", end="2024-01-31")
22
- """
23
-
24
- def __init__(self, http: HttpClient):
25
- self._http = http
26
-
27
- def _convert_timestamp(self, ts: Optional[Timestamp]) -> Optional[int]:
28
- """Convert timestamp to Unix milliseconds."""
29
- if ts is None:
30
- return None
31
- if isinstance(ts, int):
32
- return ts
33
- if isinstance(ts, datetime):
34
- return int(ts.timestamp() * 1000)
35
- if isinstance(ts, str):
36
- try:
37
- dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
38
- return int(dt.timestamp() * 1000)
39
- except ValueError:
40
- return int(ts)
41
- return None
42
-
43
- def list(
44
- self,
45
- coin: str,
46
- *,
47
- interval: Optional[CandleInterval] = None,
48
- start: Optional[Timestamp] = None,
49
- end: Optional[Timestamp] = None,
50
- limit: Optional[int] = None,
51
- offset: Optional[int] = None,
52
- ) -> list[Candle]:
53
- """
54
- Get OHLCV candles for a coin.
55
-
56
- Args:
57
- coin: The coin symbol (e.g., 'BTC', 'ETH')
58
- interval: Candle interval ('1m', '5m', '15m', '1h', '4h', '1d')
59
- start: Start timestamp
60
- end: End timestamp
61
- limit: Maximum number of results
62
- offset: Number of results to skip
63
-
64
- Returns:
65
- List of candles
66
- """
67
- data = self._http.get(
68
- f"/v1/candles/{coin.upper()}",
69
- params={
70
- "interval": interval,
71
- "start": self._convert_timestamp(start),
72
- "end": self._convert_timestamp(end),
73
- "limit": limit,
74
- "offset": offset,
75
- },
76
- )
77
- return [Candle.model_validate(item) for item in data["data"]]
78
-
79
- async def alist(
80
- self,
81
- coin: str,
82
- *,
83
- interval: Optional[CandleInterval] = None,
84
- start: Optional[Timestamp] = None,
85
- end: Optional[Timestamp] = None,
86
- limit: Optional[int] = None,
87
- offset: Optional[int] = None,
88
- ) -> list[Candle]:
89
- """Async version of list()."""
90
- data = await self._http.aget(
91
- f"/v1/candles/{coin.upper()}",
92
- params={
93
- "interval": interval,
94
- "start": self._convert_timestamp(start),
95
- "end": self._convert_timestamp(end),
96
- "limit": limit,
97
- "offset": offset,
98
- },
99
- )
100
- return [Candle.model_validate(item) for item in data["data"]]
File without changes