oxarchive 0.1.1__tar.gz → 0.2.1__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.1
3
+ Version: 0.2.1
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
@@ -140,20 +140,6 @@ trades = client.trades.list(
140
140
  )
141
141
  ```
142
142
 
143
- ### Candles (OHLCV)
144
-
145
- ```python
146
- # Get hourly candles
147
- candles = client.candles.list(
148
- "BTC",
149
- interval="1h",
150
- start="2024-01-01",
151
- end="2024-01-02"
152
- )
153
-
154
- # Available intervals: '1m', '5m', '15m', '1h', '4h', '1d'
155
- ```
156
-
157
143
  ### Instruments
158
144
 
159
145
  ```python
@@ -272,8 +258,8 @@ async def main():
272
258
  print(f"{ts}: {data['mid_price']}")
273
259
  )
274
260
 
275
- ws.on_replay_start(lambda ch, coin, total, speed:
276
- print(f"Starting replay of {total} records at {speed}x")
261
+ ws.on_replay_start(lambda ch, coin, start, end, speed:
262
+ print(f"Starting replay from {start} to {end} at {speed}x")
277
263
  )
278
264
 
279
265
  ws.on_replay_complete(lambda ch, coin, sent:
@@ -316,8 +302,8 @@ async def main():
316
302
  all_data.extend([r.data for r in records])
317
303
  )
318
304
 
319
- ws.on_stream_progress(lambda sent, total, pct:
320
- print(f"Progress: {pct:.1f}%")
305
+ ws.on_stream_progress(lambda snapshots_sent:
306
+ print(f"Sent: {snapshots_sent} snapshots")
321
307
  )
322
308
 
323
309
  ws.on_stream_complete(lambda ch, coin, sent:
@@ -104,20 +104,6 @@ trades = client.trades.list(
104
104
  )
105
105
  ```
106
106
 
107
- ### Candles (OHLCV)
108
-
109
- ```python
110
- # Get hourly candles
111
- candles = client.candles.list(
112
- "BTC",
113
- interval="1h",
114
- start="2024-01-01",
115
- end="2024-01-02"
116
- )
117
-
118
- # Available intervals: '1m', '5m', '15m', '1h', '4h', '1d'
119
- ```
120
-
121
107
  ### Instruments
122
108
 
123
109
  ```python
@@ -236,8 +222,8 @@ async def main():
236
222
  print(f"{ts}: {data['mid_price']}")
237
223
  )
238
224
 
239
- ws.on_replay_start(lambda ch, coin, total, speed:
240
- print(f"Starting replay of {total} records at {speed}x")
225
+ ws.on_replay_start(lambda ch, coin, start, end, speed:
226
+ print(f"Starting replay from {start} to {end} at {speed}x")
241
227
  )
242
228
 
243
229
  ws.on_replay_complete(lambda ch, coin, sent:
@@ -280,8 +266,8 @@ async def main():
280
266
  all_data.extend([r.data for r in records])
281
267
  )
282
268
 
283
- ws.on_stream_progress(lambda sent, total, pct:
284
- print(f"Progress: {pct:.1f}%")
269
+ ws.on_stream_progress(lambda snapshots_sent:
270
+ print(f"Sent: {snapshots_sent} snapshots")
285
271
  )
286
272
 
287
273
  ws.on_stream_complete(lambda ch, coin, sent:
@@ -20,8 +20,6 @@ from .client import Client
20
20
  from .types import (
21
21
  OrderBook,
22
22
  Trade,
23
- Candle,
24
- CandleInterval,
25
23
  Instrument,
26
24
  FundingRate,
27
25
  OpenInterest,
@@ -70,8 +68,6 @@ __all__ = [
70
68
  # Types
71
69
  "OrderBook",
72
70
  "Trade",
73
- "Candle",
74
- "CandleInterval",
75
71
  "Instrument",
76
72
  "FundingRate",
77
73
  "OpenInterest",
@@ -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",
@@ -0,0 +1,408 @@
1
+ """Type definitions for the 0xarchive SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from typing import Any, Generic, Literal, Optional, TypeVar, Union
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ # =============================================================================
12
+ # Base Types
13
+ # =============================================================================
14
+
15
+ T = TypeVar("T")
16
+
17
+
18
+ class ApiMeta(BaseModel):
19
+ """Response metadata."""
20
+
21
+ count: int
22
+ next_cursor: Optional[str] = None
23
+ request_id: str
24
+
25
+
26
+ class ApiResponse(BaseModel, Generic[T]):
27
+ """Standard API response wrapper."""
28
+
29
+ success: bool
30
+ data: T
31
+ meta: ApiMeta
32
+
33
+
34
+ # =============================================================================
35
+ # Order Book Types
36
+ # =============================================================================
37
+
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
+
52
+ class OrderBook(BaseModel):
53
+ """L2 order book snapshot."""
54
+
55
+ coin: 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."""
75
+
76
+
77
+ # =============================================================================
78
+ # Trade/Fill Types
79
+ # =============================================================================
80
+
81
+
82
+ class Trade(BaseModel):
83
+ """Trade/fill record with full execution details."""
84
+
85
+ coin: str
86
+ """Trading pair symbol."""
87
+
88
+ side: Literal["A", "B"]
89
+ """Trade side: 'B' (buy) or 'A' (sell/ask)."""
90
+
91
+ price: str
92
+ """Execution price."""
93
+
94
+ size: str
95
+ """Trade size."""
96
+
97
+ timestamp: datetime
98
+ """Execution timestamp (UTC)."""
99
+
100
+ tx_hash: Optional[str] = None
101
+ """Blockchain transaction hash."""
102
+
103
+ trade_id: Optional[int] = None
104
+ """Unique trade ID."""
105
+
106
+ order_id: Optional[int] = None
107
+ """Associated order ID."""
108
+
109
+ crossed: Optional[bool] = None
110
+ """True if taker (crossed the spread), false if maker."""
111
+
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."""
132
+
133
+
134
+ # =============================================================================
135
+ # Instrument Types
136
+ # =============================================================================
137
+
138
+
139
+ class Instrument(BaseModel):
140
+ """Trading instrument specification."""
141
+
142
+ model_config = {"populate_by_name": True}
143
+
144
+ name: str
145
+ """Instrument symbol (e.g., BTC)."""
146
+
147
+ sz_decimals: int = Field(alias="szDecimals")
148
+ """Size decimal precision."""
149
+
150
+ max_leverage: Optional[int] = Field(default=None, alias="maxLeverage")
151
+ """Maximum leverage allowed."""
152
+
153
+ only_isolated: Optional[bool] = Field(default=None, alias="onlyIsolated")
154
+ """If true, only isolated margin mode is allowed."""
155
+
156
+ instrument_type: Optional[Literal["perp", "spot"]] = Field(default=None, alias="instrumentType")
157
+ """Type of instrument."""
158
+
159
+ is_active: bool = Field(default=True, alias="isActive")
160
+ """Whether the instrument is currently tradeable."""
161
+
162
+
163
+ # =============================================================================
164
+ # Funding Types
165
+ # =============================================================================
166
+
167
+
168
+ class FundingRate(BaseModel):
169
+ """Funding rate record."""
170
+
171
+ coin: str
172
+ """Trading pair symbol."""
173
+
174
+ timestamp: datetime
175
+ """Funding timestamp (UTC)."""
176
+
177
+ funding_rate: str
178
+ """Funding rate as decimal (e.g., 0.0001 = 0.01%)."""
179
+
180
+ premium: Optional[str] = None
181
+ """Premium component of funding rate."""
182
+
183
+
184
+ # =============================================================================
185
+ # Open Interest Types
186
+ # =============================================================================
187
+
188
+
189
+ class OpenInterest(BaseModel):
190
+ """Open interest snapshot with market context."""
191
+
192
+ coin: str
193
+ """Trading pair symbol."""
194
+
195
+ timestamp: datetime
196
+ """Snapshot timestamp (UTC)."""
197
+
198
+ open_interest: str
199
+ """Total open interest in contracts."""
200
+
201
+ mark_price: Optional[str] = None
202
+ """Mark price used for liquidations."""
203
+
204
+ oracle_price: Optional[str] = None
205
+ """Oracle price from external feed."""
206
+
207
+ day_ntl_volume: Optional[str] = None
208
+ """24-hour notional volume."""
209
+
210
+ prev_day_price: Optional[str] = None
211
+ """Price 24 hours ago."""
212
+
213
+ mid_price: Optional[str] = None
214
+ """Current mid price."""
215
+
216
+ impact_bid_price: Optional[str] = None
217
+ """Impact bid price for liquidations."""
218
+
219
+ impact_ask_price: Optional[str] = None
220
+ """Impact ask price for liquidations."""
221
+
222
+
223
+ # =============================================================================
224
+ # WebSocket Types
225
+ # =============================================================================
226
+
227
+ WsChannel = Literal["orderbook", "trades", "ticker", "all_tickers"]
228
+ """Available WebSocket channels. Note: ticker/all_tickers are real-time only."""
229
+
230
+ WsConnectionState = Literal["connecting", "connected", "disconnected", "reconnecting"]
231
+ """WebSocket connection state."""
232
+
233
+
234
+ class WsSubscribed(BaseModel):
235
+ """Subscription confirmed from server."""
236
+
237
+ type: Literal["subscribed"]
238
+ channel: WsChannel
239
+ coin: Optional[str] = None
240
+
241
+
242
+ class WsUnsubscribed(BaseModel):
243
+ """Unsubscription confirmed from server."""
244
+
245
+ type: Literal["unsubscribed"]
246
+ channel: WsChannel
247
+ coin: Optional[str] = None
248
+
249
+
250
+ class WsPong(BaseModel):
251
+ """Pong response from server."""
252
+
253
+ type: Literal["pong"]
254
+
255
+
256
+ class WsError(BaseModel):
257
+ """Error from server."""
258
+
259
+ type: Literal["error"]
260
+ message: str
261
+
262
+
263
+ class WsData(BaseModel):
264
+ """Real-time data message from server."""
265
+
266
+ type: Literal["data"]
267
+ channel: WsChannel
268
+ coin: str
269
+ data: dict[str, Any]
270
+
271
+
272
+ # =============================================================================
273
+ # WebSocket Replay Types (Historical Replay Mode)
274
+ # =============================================================================
275
+
276
+
277
+ class WsReplayStarted(BaseModel):
278
+ """Replay started response."""
279
+
280
+ type: Literal["replay_started"]
281
+ channel: WsChannel
282
+ coin: str
283
+ start: int
284
+ """Start timestamp in milliseconds."""
285
+ end: int
286
+ """End timestamp in milliseconds."""
287
+ speed: float
288
+ """Playback speed multiplier."""
289
+
290
+
291
+ class WsReplayPaused(BaseModel):
292
+ """Replay paused response."""
293
+
294
+ type: Literal["replay_paused"]
295
+ current_timestamp: int
296
+
297
+
298
+ class WsReplayResumed(BaseModel):
299
+ """Replay resumed response."""
300
+
301
+ type: Literal["replay_resumed"]
302
+ current_timestamp: int
303
+
304
+
305
+ class WsReplayCompleted(BaseModel):
306
+ """Replay completed response."""
307
+
308
+ type: Literal["replay_completed"]
309
+ channel: WsChannel
310
+ coin: str
311
+ snapshots_sent: int
312
+
313
+
314
+ class WsReplayStopped(BaseModel):
315
+ """Replay stopped response."""
316
+
317
+ type: Literal["replay_stopped"]
318
+
319
+
320
+ class WsHistoricalData(BaseModel):
321
+ """Historical data point (replay mode)."""
322
+
323
+ type: Literal["historical_data"]
324
+ channel: WsChannel
325
+ coin: str
326
+ timestamp: int
327
+ data: dict[str, Any]
328
+
329
+
330
+ # =============================================================================
331
+ # WebSocket Bulk Stream Types (Bulk Download Mode)
332
+ # =============================================================================
333
+
334
+
335
+ class WsStreamStarted(BaseModel):
336
+ """Stream started response."""
337
+
338
+ type: Literal["stream_started"]
339
+ channel: WsChannel
340
+ coin: str
341
+ start: int
342
+ """Start timestamp in milliseconds."""
343
+ end: int
344
+ """End timestamp in milliseconds."""
345
+
346
+
347
+ class WsStreamProgress(BaseModel):
348
+ """Stream progress response (sent every ~2 seconds)."""
349
+
350
+ type: Literal["stream_progress"]
351
+ snapshots_sent: int
352
+
353
+
354
+ class TimestampedRecord(BaseModel):
355
+ """A record with timestamp for batched data."""
356
+
357
+ timestamp: int
358
+ data: dict[str, Any]
359
+
360
+
361
+ class WsHistoricalBatch(BaseModel):
362
+ """Batch of historical data (bulk streaming)."""
363
+
364
+ type: Literal["historical_batch"]
365
+ channel: WsChannel
366
+ coin: str
367
+ data: list[TimestampedRecord]
368
+
369
+
370
+ class WsStreamCompleted(BaseModel):
371
+ """Stream completed response."""
372
+
373
+ type: Literal["stream_completed"]
374
+ channel: WsChannel
375
+ coin: str
376
+ snapshots_sent: int
377
+
378
+
379
+ class WsStreamStopped(BaseModel):
380
+ """Stream stopped response."""
381
+
382
+ type: Literal["stream_stopped"]
383
+ snapshots_sent: int
384
+
385
+
386
+ # =============================================================================
387
+ # Error Types
388
+ # =============================================================================
389
+
390
+
391
+ class OxArchiveError(Exception):
392
+ """SDK error class."""
393
+
394
+ def __init__(self, message: str, code: int, request_id: Optional[str] = None):
395
+ super().__init__(message)
396
+ self.message = message
397
+ self.code = code
398
+ self.request_id = request_id
399
+
400
+ def __str__(self) -> str:
401
+ if self.request_id:
402
+ return f"[{self.code}] {self.message} (request_id: {self.request_id})"
403
+ return f"[{self.code}] {self.message}"
404
+
405
+
406
+ # Type alias for timestamp parameters
407
+ Timestamp = Union[int, str, datetime]
408
+ """Timestamp can be Unix ms (int), ISO string, or datetime object."""
@@ -33,6 +33,7 @@ from typing import Any, Callable, Optional, Set, Union
33
33
  try:
34
34
  import websockets
35
35
  from websockets.client import WebSocketClientProtocol
36
+ from websockets.protocol import State as WsState
36
37
  except ImportError:
37
38
  raise ImportError(
38
39
  "WebSocket support requires the 'websockets' package. "
@@ -108,14 +109,14 @@ ErrorHandler = Callable[[Exception], None]
108
109
 
109
110
  # Replay handlers
110
111
  HistoricalDataHandler = Callable[[str, int, dict], None]
111
- ReplayStartHandler = Callable[[WsChannel, str, int, float], None]
112
- ReplayCompleteHandler = Callable[[WsChannel, str, int], None]
112
+ ReplayStartHandler = Callable[[WsChannel, str, int, int, float], None] # channel, coin, start, end, speed
113
+ ReplayCompleteHandler = Callable[[WsChannel, str, int], None] # channel, coin, snapshots_sent
113
114
 
114
115
  # Stream handlers
115
116
  BatchHandler = Callable[[str, list[TimestampedRecord]], None]
116
- StreamStartHandler = Callable[[WsChannel, str, int], None]
117
- StreamProgressHandler = Callable[[int, int, float], None]
118
- StreamCompleteHandler = Callable[[WsChannel, str, int], None]
117
+ StreamStartHandler = Callable[[WsChannel, str, int, int], None] # channel, coin, start, end
118
+ StreamProgressHandler = Callable[[int], None] # snapshots_sent
119
+ StreamCompleteHandler = Callable[[WsChannel, str, int], None] # channel, coin, snapshots_sent
119
120
 
120
121
 
121
122
  class OxArchiveWs:
@@ -164,7 +165,7 @@ class OxArchiveWs:
164
165
  @property
165
166
  def is_connected(self) -> bool:
166
167
  """Check if connected."""
167
- return self._ws is not None and self._ws.open
168
+ return self._ws is not None and self._ws.state == WsState.OPEN
168
169
 
169
170
  async def connect(self) -> None:
170
171
  """Connect to the WebSocket server."""
@@ -421,14 +422,14 @@ class OxArchiveWs:
421
422
  def on_replay_start(self, handler: ReplayStartHandler) -> None:
422
423
  """Set handler for replay started event.
423
424
 
424
- Handler receives: (channel, coin, total_records, speed)
425
+ Handler receives: (channel, coin, start, end, speed)
425
426
  """
426
427
  self._on_replay_start = handler
427
428
 
428
429
  def on_replay_complete(self, handler: ReplayCompleteHandler) -> None:
429
430
  """Set handler for replay completed event.
430
431
 
431
- Handler receives: (channel, coin, records_sent)
432
+ Handler receives: (channel, coin, snapshots_sent)
432
433
  """
433
434
  self._on_replay_complete = handler
434
435
 
@@ -444,21 +445,21 @@ class OxArchiveWs:
444
445
  def on_stream_start(self, handler: StreamStartHandler) -> None:
445
446
  """Set handler for stream started event.
446
447
 
447
- Handler receives: (channel, coin, total_records)
448
+ Handler receives: (channel, coin, start, end)
448
449
  """
449
450
  self._on_stream_start = handler
450
451
 
451
452
  def on_stream_progress(self, handler: StreamProgressHandler) -> None:
452
453
  """Set handler for stream progress event.
453
454
 
454
- Handler receives: (records_sent, total_records, progress_pct)
455
+ Handler receives: (snapshots_sent)
455
456
  """
456
457
  self._on_stream_progress = handler
457
458
 
458
459
  def on_stream_complete(self, handler: StreamCompleteHandler) -> None:
459
460
  """Set handler for stream completed event.
460
461
 
461
- Handler receives: (channel, coin, records_sent)
462
+ Handler receives: (channel, coin, snapshots_sent)
462
463
  """
463
464
  self._on_stream_complete = handler
464
465
 
@@ -585,30 +586,28 @@ class OxArchiveWs:
585
586
  # Replay messages (Option B)
586
587
  elif msg_type == "replay_started" and self._on_replay_start:
587
588
  self._on_replay_start(
588
- data["channel"], data["coin"], data["total_records"], data["speed"]
589
+ data["channel"], data["coin"], data["start"], data["end"], data["speed"]
589
590
  )
590
591
 
591
592
  elif msg_type == "historical_data" and self._on_historical_data:
592
593
  self._on_historical_data(data["coin"], data["timestamp"], data["data"])
593
594
 
594
595
  elif msg_type == "replay_completed" and self._on_replay_complete:
595
- self._on_replay_complete(data["channel"], data["coin"], data["records_sent"])
596
+ self._on_replay_complete(data["channel"], data["coin"], data["snapshots_sent"])
596
597
 
597
598
  # Stream messages (Option D)
598
599
  elif msg_type == "stream_started" and self._on_stream_start:
599
- self._on_stream_start(data["channel"], data["coin"], data["total_records"])
600
+ self._on_stream_start(data["channel"], data["coin"], data["start"], data["end"])
600
601
 
601
602
  elif msg_type == "stream_progress" and self._on_stream_progress:
602
- self._on_stream_progress(
603
- data["records_sent"], data["total_records"], data["progress_pct"]
604
- )
603
+ self._on_stream_progress(data["snapshots_sent"])
605
604
 
606
605
  elif msg_type == "historical_batch" and self._on_batch:
607
- records = [TimestampedRecord(**r) for r in data["records"]]
606
+ records = [TimestampedRecord(**r) for r in data["data"]]
608
607
  self._on_batch(data["coin"], records)
609
608
 
610
609
  elif msg_type == "stream_completed" and self._on_stream_complete:
611
- self._on_stream_complete(data["channel"], data["coin"], data["records_sent"])
610
+ self._on_stream_complete(data["channel"], data["coin"], data["snapshots_sent"])
612
611
 
613
612
  except Exception as e:
614
613
  logger.error(f"Error handling message: {e}")
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oxarchive"
7
- version = "0.1.1"
7
+ version = "0.2.1"
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"]]
@@ -1,304 +0,0 @@
1
- """Type definitions for the 0xarchive SDK."""
2
-
3
- from __future__ import annotations
4
-
5
- from datetime import datetime
6
- from typing import Literal, Optional, Union
7
-
8
- from pydantic import BaseModel, Field
9
-
10
-
11
- # =============================================================================
12
- # Base Types
13
- # =============================================================================
14
-
15
-
16
- class ApiResponse(BaseModel):
17
- """Standard API response wrapper."""
18
-
19
- success: bool
20
- count: int
21
- request_id: str
22
-
23
-
24
- # =============================================================================
25
- # Order Book Types
26
- # =============================================================================
27
-
28
-
29
- class OrderBook(BaseModel):
30
- """Order book snapshot."""
31
-
32
- 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
39
-
40
-
41
- # =============================================================================
42
- # Trade Types
43
- # =============================================================================
44
-
45
-
46
- class Trade(BaseModel):
47
- """Trade/fill record."""
48
-
49
- id: str
50
- coin: str
51
- side: Literal["buy", "sell"]
52
- price: str
53
- size: str
54
- value: str
55
- timestamp: int
56
- trade_type: str
57
-
58
-
59
- # =============================================================================
60
- # Candle Types
61
- # =============================================================================
62
-
63
- CandleInterval = Literal["1m", "5m", "15m", "1h", "4h", "1d"]
64
-
65
-
66
- class Candle(BaseModel):
67
- """OHLCV candle."""
68
-
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
78
-
79
-
80
- # =============================================================================
81
- # Instrument Types
82
- # =============================================================================
83
-
84
-
85
- class Instrument(BaseModel):
86
- """Trading instrument metadata."""
87
-
88
- coin: str
89
- name: str
90
- sz_decimals: int
91
- max_leverage: int
92
- only_isolated: bool
93
- is_active: bool
94
-
95
-
96
- # =============================================================================
97
- # Funding Types
98
- # =============================================================================
99
-
100
-
101
- class FundingRate(BaseModel):
102
- """Funding rate record."""
103
-
104
- coin: str
105
- funding_rate: str
106
- premium: str
107
- timestamp: int
108
-
109
-
110
- # =============================================================================
111
- # Open Interest Types
112
- # =============================================================================
113
-
114
-
115
- class OpenInterest(BaseModel):
116
- """Open interest record."""
117
-
118
- coin: str
119
- open_interest: str
120
- timestamp: int
121
-
122
-
123
- # =============================================================================
124
- # WebSocket Types
125
- # =============================================================================
126
-
127
- WsChannel = Literal["orderbook", "trades", "ticker", "all_tickers", "candles", "funding", "openinterest"]
128
- WsConnectionState = Literal["connecting", "connected", "disconnected", "reconnecting"]
129
-
130
-
131
- class WsSubscribed(BaseModel):
132
- """Subscription confirmed from server."""
133
-
134
- type: Literal["subscribed"]
135
- channel: WsChannel
136
- coin: Optional[str] = None
137
-
138
-
139
- class WsUnsubscribed(BaseModel):
140
- """Unsubscription confirmed from server."""
141
-
142
- type: Literal["unsubscribed"]
143
- channel: WsChannel
144
- coin: Optional[str] = None
145
-
146
-
147
- class WsPong(BaseModel):
148
- """Pong response from server."""
149
-
150
- type: Literal["pong"]
151
-
152
-
153
- class WsError(BaseModel):
154
- """Error from server."""
155
-
156
- type: Literal["error"]
157
- message: str
158
-
159
-
160
- class WsData(BaseModel):
161
- """Data message from server."""
162
-
163
- type: Literal["data"]
164
- channel: WsChannel
165
- coin: str
166
- data: dict
167
-
168
-
169
- # =============================================================================
170
- # WebSocket Replay Types (Option B - Like Tardis.dev)
171
- # =============================================================================
172
-
173
-
174
- class WsReplayStarted(BaseModel):
175
- """Replay started response."""
176
-
177
- type: Literal["replay_started"]
178
- channel: WsChannel
179
- coin: str
180
- start: int
181
- end: int
182
- speed: float
183
- total_records: int
184
-
185
-
186
- class WsReplayPaused(BaseModel):
187
- """Replay paused response."""
188
-
189
- type: Literal["replay_paused"]
190
- current_timestamp: int
191
-
192
-
193
- class WsReplayResumed(BaseModel):
194
- """Replay resumed response."""
195
-
196
- type: Literal["replay_resumed"]
197
- current_timestamp: int
198
-
199
-
200
- class WsReplayCompleted(BaseModel):
201
- """Replay completed response."""
202
-
203
- type: Literal["replay_completed"]
204
- channel: WsChannel
205
- coin: str
206
- records_sent: int
207
-
208
-
209
- class WsReplayStopped(BaseModel):
210
- """Replay stopped response."""
211
-
212
- type: Literal["replay_stopped"]
213
-
214
-
215
- class WsHistoricalData(BaseModel):
216
- """Historical data point (replay mode)."""
217
-
218
- type: Literal["historical_data"]
219
- channel: WsChannel
220
- coin: str
221
- timestamp: int
222
- data: dict
223
-
224
-
225
- # =============================================================================
226
- # WebSocket Bulk Stream Types (Option D - Like Databento)
227
- # =============================================================================
228
-
229
-
230
- class WsStreamStarted(BaseModel):
231
- """Stream started response."""
232
-
233
- type: Literal["stream_started"]
234
- channel: WsChannel
235
- coin: str
236
- start: int
237
- end: int
238
- batch_size: int
239
- total_records: int
240
-
241
-
242
- class WsStreamProgress(BaseModel):
243
- """Stream progress response."""
244
-
245
- type: Literal["stream_progress"]
246
- records_sent: int
247
- total_records: int
248
- progress_pct: float
249
-
250
-
251
- class TimestampedRecord(BaseModel):
252
- """A record with timestamp."""
253
-
254
- timestamp: int
255
- data: dict
256
-
257
-
258
- class WsHistoricalBatch(BaseModel):
259
- """Stream batch (bulk data)."""
260
-
261
- type: Literal["historical_batch"]
262
- channel: WsChannel
263
- coin: str
264
- batch_index: int
265
- records: list[TimestampedRecord]
266
-
267
-
268
- class WsStreamCompleted(BaseModel):
269
- """Stream completed response."""
270
-
271
- type: Literal["stream_completed"]
272
- channel: WsChannel
273
- coin: str
274
- records_sent: int
275
-
276
-
277
- class WsStreamStopped(BaseModel):
278
- """Stream stopped response."""
279
-
280
- type: Literal["stream_stopped"]
281
-
282
-
283
- # =============================================================================
284
- # Error Types
285
- # =============================================================================
286
-
287
-
288
- class OxArchiveError(Exception):
289
- """SDK error class."""
290
-
291
- def __init__(self, message: str, code: int, request_id: Optional[str] = None):
292
- super().__init__(message)
293
- self.message = message
294
- self.code = code
295
- self.request_id = request_id
296
-
297
- def __str__(self) -> str:
298
- if self.request_id:
299
- return f"[{self.code}] {self.message} (request_id: {self.request_id})"
300
- return f"[{self.code}] {self.message}"
301
-
302
-
303
- # Type alias for timestamp parameters
304
- Timestamp = Union[int, str, datetime]
File without changes