oxarchive 0.1.1__py3-none-any.whl → 0.3.0__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 CHANGED
@@ -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",
oxarchive/client.py CHANGED
@@ -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",
@@ -44,8 +44,8 @@ class FundingResource:
44
44
  self,
45
45
  coin: str,
46
46
  *,
47
- start: Optional[Timestamp] = None,
48
- end: Optional[Timestamp] = None,
47
+ start: Timestamp,
48
+ end: Timestamp,
49
49
  limit: Optional[int] = None,
50
50
  offset: Optional[int] = None,
51
51
  ) -> list[FundingRate]:
@@ -54,8 +54,8 @@ class FundingResource:
54
54
 
55
55
  Args:
56
56
  coin: The coin symbol (e.g., 'BTC', 'ETH')
57
- start: Start timestamp
58
- end: End timestamp
57
+ start: Start timestamp (required)
58
+ end: End timestamp (required)
59
59
  limit: Maximum number of results
60
60
  offset: Number of results to skip
61
61
 
@@ -77,12 +77,12 @@ class FundingResource:
77
77
  self,
78
78
  coin: str,
79
79
  *,
80
- start: Optional[Timestamp] = None,
81
- end: Optional[Timestamp] = None,
80
+ start: Timestamp,
81
+ end: Timestamp,
82
82
  limit: Optional[int] = None,
83
83
  offset: Optional[int] = None,
84
84
  ) -> list[FundingRate]:
85
- """Async version of history()."""
85
+ """Async version of history(). start and end are required."""
86
86
  data = await self._http.aget(
87
87
  f"/v1/funding/{coin.upper()}",
88
88
  params={
@@ -44,8 +44,8 @@ class OpenInterestResource:
44
44
  self,
45
45
  coin: str,
46
46
  *,
47
- start: Optional[Timestamp] = None,
48
- end: Optional[Timestamp] = None,
47
+ start: Timestamp,
48
+ end: Timestamp,
49
49
  limit: Optional[int] = None,
50
50
  offset: Optional[int] = None,
51
51
  ) -> list[OpenInterest]:
@@ -54,8 +54,8 @@ class OpenInterestResource:
54
54
 
55
55
  Args:
56
56
  coin: The coin symbol (e.g., 'BTC', 'ETH')
57
- start: Start timestamp
58
- end: End timestamp
57
+ start: Start timestamp (required)
58
+ end: End timestamp (required)
59
59
  limit: Maximum number of results
60
60
  offset: Number of results to skip
61
61
 
@@ -77,12 +77,12 @@ class OpenInterestResource:
77
77
  self,
78
78
  coin: str,
79
79
  *,
80
- start: Optional[Timestamp] = None,
81
- end: Optional[Timestamp] = None,
80
+ start: Timestamp,
81
+ end: Timestamp,
82
82
  limit: Optional[int] = None,
83
83
  offset: Optional[int] = None,
84
84
  ) -> list[OpenInterest]:
85
- """Async version of history()."""
85
+ """Async version of history(). start and end are required."""
86
86
  data = await self._http.aget(
87
87
  f"/v1/openinterest/{coin.upper()}",
88
88
  params={
@@ -92,8 +92,8 @@ class OrderBookResource:
92
92
  self,
93
93
  coin: str,
94
94
  *,
95
- start: Optional[Timestamp] = None,
96
- end: Optional[Timestamp] = None,
95
+ start: Timestamp,
96
+ end: Timestamp,
97
97
  limit: Optional[int] = None,
98
98
  offset: Optional[int] = None,
99
99
  depth: Optional[int] = None,
@@ -103,8 +103,8 @@ class OrderBookResource:
103
103
 
104
104
  Args:
105
105
  coin: The coin symbol (e.g., 'BTC', 'ETH')
106
- start: Start timestamp
107
- end: End timestamp
106
+ start: Start timestamp (required)
107
+ end: End timestamp (required)
108
108
  limit: Maximum number of results
109
109
  offset: Number of results to skip
110
110
  depth: Number of price levels per side
@@ -128,13 +128,13 @@ class OrderBookResource:
128
128
  self,
129
129
  coin: str,
130
130
  *,
131
- start: Optional[Timestamp] = None,
132
- end: Optional[Timestamp] = None,
131
+ start: Timestamp,
132
+ end: Timestamp,
133
133
  limit: Optional[int] = None,
134
134
  offset: Optional[int] = None,
135
135
  depth: Optional[int] = None,
136
136
  ) -> list[OrderBook]:
137
- """Async version of history()."""
137
+ """Async version of history(). start and end are required."""
138
138
  data = await self._http.aget(
139
139
  f"/v1/orderbook/{coin.upper()}/history",
140
140
  params={
@@ -2,6 +2,8 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import warnings
6
+ from dataclasses import dataclass
5
7
  from datetime import datetime
6
8
  from typing import Literal, Optional
7
9
 
@@ -9,6 +11,13 @@ from ..http import HttpClient
9
11
  from ..types import Trade, Timestamp
10
12
 
11
13
 
14
+ @dataclass
15
+ class CursorResponse:
16
+ """Response with cursor for pagination."""
17
+ data: list[Trade]
18
+ next_cursor: Optional[str] = None
19
+
20
+
12
21
  class TradesResource:
13
22
  """
14
23
  Trades API resource.
@@ -17,8 +26,14 @@ class TradesResource:
17
26
  >>> # Get recent trades
18
27
  >>> trades = client.trades.recent("BTC")
19
28
  >>>
20
- >>> # Get trade history with time range
21
- >>> history = client.trades.list("ETH", start="2024-01-01", end="2024-01-02")
29
+ >>> # Get trade history with cursor-based pagination (recommended)
30
+ >>> result = client.trades.list("BTC", start="2024-01-01", end="2024-01-02")
31
+ >>> trades = result.data
32
+ >>>
33
+ >>> # Get all pages
34
+ >>> while result.next_cursor:
35
+ ... result = client.trades.list("BTC", start="2024-01-01", end="2024-01-02", cursor=result.next_cursor)
36
+ ... trades.extend(result.data)
22
37
  """
23
38
 
24
39
  def __init__(self, http: HttpClient):
@@ -44,19 +59,106 @@ class TradesResource:
44
59
  self,
45
60
  coin: str,
46
61
  *,
47
- start: Optional[Timestamp] = None,
48
- end: Optional[Timestamp] = None,
62
+ start: Timestamp,
63
+ end: Timestamp,
64
+ cursor: Optional[Timestamp] = None,
65
+ limit: Optional[int] = None,
66
+ side: Optional[Literal["buy", "sell"]] = None,
67
+ ) -> CursorResponse:
68
+ """
69
+ Get trade history for a coin using cursor-based pagination.
70
+
71
+ Uses cursor-based pagination by default, which is more efficient for large datasets.
72
+ Use the next_cursor from the response as the cursor parameter to get the next page.
73
+
74
+ Args:
75
+ coin: The coin symbol (e.g., 'BTC', 'ETH')
76
+ start: Start timestamp (required)
77
+ end: End timestamp (required)
78
+ cursor: Cursor from previous response's next_cursor (timestamp)
79
+ limit: Maximum number of results (default: 100, max: 1000)
80
+ side: Filter by trade side
81
+
82
+ Returns:
83
+ CursorResponse with trades and next_cursor for pagination
84
+
85
+ Example:
86
+ >>> # First page
87
+ >>> result = client.trades.list("BTC", start=start, end=end, limit=1000)
88
+ >>> trades = result.data
89
+ >>>
90
+ >>> # Subsequent pages
91
+ >>> while result.next_cursor:
92
+ ... result = client.trades.list(
93
+ ... "BTC", start=start, end=end, cursor=result.next_cursor, limit=1000
94
+ ... )
95
+ ... trades.extend(result.data)
96
+ """
97
+ data = self._http.get(
98
+ f"/v1/trades/{coin.upper()}",
99
+ params={
100
+ "start": self._convert_timestamp(start),
101
+ "end": self._convert_timestamp(end),
102
+ "cursor": self._convert_timestamp(cursor),
103
+ "limit": limit,
104
+ "side": side,
105
+ },
106
+ )
107
+ return CursorResponse(
108
+ data=[Trade.model_validate(item) for item in data["data"]],
109
+ next_cursor=data.get("meta", {}).get("next_cursor"),
110
+ )
111
+
112
+ async def alist(
113
+ self,
114
+ coin: str,
115
+ *,
116
+ start: Timestamp,
117
+ end: Timestamp,
118
+ cursor: Optional[Timestamp] = None,
119
+ limit: Optional[int] = None,
120
+ side: Optional[Literal["buy", "sell"]] = None,
121
+ ) -> CursorResponse:
122
+ """
123
+ Async version of list().
124
+
125
+ Uses cursor-based pagination by default.
126
+ """
127
+ data = await self._http.aget(
128
+ f"/v1/trades/{coin.upper()}",
129
+ params={
130
+ "start": self._convert_timestamp(start),
131
+ "end": self._convert_timestamp(end),
132
+ "cursor": self._convert_timestamp(cursor),
133
+ "limit": limit,
134
+ "side": side,
135
+ },
136
+ )
137
+ return CursorResponse(
138
+ data=[Trade.model_validate(item) for item in data["data"]],
139
+ next_cursor=data.get("meta", {}).get("next_cursor"),
140
+ )
141
+
142
+ def list_with_offset(
143
+ self,
144
+ coin: str,
145
+ *,
146
+ start: Timestamp,
147
+ end: Timestamp,
49
148
  limit: Optional[int] = None,
50
149
  offset: Optional[int] = None,
51
150
  side: Optional[Literal["buy", "sell"]] = None,
52
151
  ) -> list[Trade]:
53
152
  """
54
- Get trade history for a coin.
153
+ Get trade history using offset-based pagination.
154
+
155
+ .. deprecated::
156
+ Use list() with cursor-based pagination instead for better performance.
55
157
 
56
158
  Args:
57
159
  coin: The coin symbol (e.g., 'BTC', 'ETH')
58
- start: Start timestamp
59
- end: End timestamp
160
+ start: Start timestamp (required)
161
+ end: End timestamp (required)
60
162
  limit: Maximum number of results
61
163
  offset: Number of results to skip
62
164
  side: Filter by trade side
@@ -64,6 +166,11 @@ class TradesResource:
64
166
  Returns:
65
167
  List of trades
66
168
  """
169
+ warnings.warn(
170
+ "list_with_offset() is deprecated. Use list() with cursor-based pagination instead.",
171
+ DeprecationWarning,
172
+ stacklevel=2,
173
+ )
67
174
  data = self._http.get(
68
175
  f"/v1/trades/{coin.upper()}",
69
176
  params={
@@ -76,17 +183,27 @@ class TradesResource:
76
183
  )
77
184
  return [Trade.model_validate(item) for item in data["data"]]
78
185
 
79
- async def alist(
186
+ async def alist_with_offset(
80
187
  self,
81
188
  coin: str,
82
189
  *,
83
- start: Optional[Timestamp] = None,
84
- end: Optional[Timestamp] = None,
190
+ start: Timestamp,
191
+ end: Timestamp,
85
192
  limit: Optional[int] = None,
86
193
  offset: Optional[int] = None,
87
194
  side: Optional[Literal["buy", "sell"]] = None,
88
195
  ) -> list[Trade]:
89
- """Async version of list()."""
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
+ )
90
207
  data = await self._http.aget(
91
208
  f"/v1/trades/{coin.upper()}",
92
209
  params={
@@ -123,3 +240,60 @@ class TradesResource:
123
240
  params={"limit": limit},
124
241
  )
125
242
  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
@@ -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,27 @@ class Candle(BaseModel):
83
137
 
84
138
 
85
139
  class Instrument(BaseModel):
86
- """Trading instrument metadata."""
140
+ """Trading instrument specification."""
141
+
142
+ model_config = {"populate_by_name": True}
87
143
 
88
- coin: str
89
144
  name: str
90
- sz_decimals: int
91
- max_leverage: int
92
- only_isolated: bool
93
- is_active: bool
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."""
94
161
 
95
162
 
96
163
  # =============================================================================
@@ -102,9 +169,16 @@ class FundingRate(BaseModel):
102
169
  """Funding rate record."""
103
170
 
104
171
  coin: str
172
+ """Trading pair symbol."""
173
+
174
+ timestamp: datetime
175
+ """Funding timestamp (UTC)."""
176
+
105
177
  funding_rate: str
106
- premium: str
107
- timestamp: int
178
+ """Funding rate as decimal (e.g., 0.0001 = 0.01%)."""
179
+
180
+ premium: Optional[str] = None
181
+ """Premium component of funding rate."""
108
182
 
109
183
 
110
184
  # =============================================================================
@@ -113,19 +187,48 @@ class FundingRate(BaseModel):
113
187
 
114
188
 
115
189
  class OpenInterest(BaseModel):
116
- """Open interest record."""
190
+ """Open interest snapshot with market context."""
117
191
 
118
192
  coin: str
193
+ """Trading pair symbol."""
194
+
195
+ timestamp: datetime
196
+ """Snapshot timestamp (UTC)."""
197
+
119
198
  open_interest: str
120
- timestamp: int
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."""
121
221
 
122
222
 
123
223
  # =============================================================================
124
224
  # WebSocket Types
125
225
  # =============================================================================
126
226
 
127
- WsChannel = Literal["orderbook", "trades", "ticker", "all_tickers", "candles", "funding", "openinterest"]
227
+ WsChannel = Literal["orderbook", "trades", "ticker", "all_tickers"]
228
+ """Available WebSocket channels. Note: ticker/all_tickers are real-time only."""
229
+
128
230
  WsConnectionState = Literal["connecting", "connected", "disconnected", "reconnecting"]
231
+ """WebSocket connection state."""
129
232
 
130
233
 
131
234
  class WsSubscribed(BaseModel):
@@ -158,16 +261,16 @@ class WsError(BaseModel):
158
261
 
159
262
 
160
263
  class WsData(BaseModel):
161
- """Data message from server."""
264
+ """Real-time data message from server."""
162
265
 
163
266
  type: Literal["data"]
164
267
  channel: WsChannel
165
268
  coin: str
166
- data: dict
269
+ data: dict[str, Any]
167
270
 
168
271
 
169
272
  # =============================================================================
170
- # WebSocket Replay Types (Option B - Like Tardis.dev)
273
+ # WebSocket Replay Types (Historical Replay Mode)
171
274
  # =============================================================================
172
275
 
173
276
 
@@ -178,9 +281,11 @@ class WsReplayStarted(BaseModel):
178
281
  channel: WsChannel
179
282
  coin: str
180
283
  start: int
284
+ """Start timestamp in milliseconds."""
181
285
  end: int
286
+ """End timestamp in milliseconds."""
182
287
  speed: float
183
- total_records: int
288
+ """Playback speed multiplier."""
184
289
 
185
290
 
186
291
  class WsReplayPaused(BaseModel):
@@ -203,7 +308,7 @@ class WsReplayCompleted(BaseModel):
203
308
  type: Literal["replay_completed"]
204
309
  channel: WsChannel
205
310
  coin: str
206
- records_sent: int
311
+ snapshots_sent: int
207
312
 
208
313
 
209
314
  class WsReplayStopped(BaseModel):
@@ -219,11 +324,11 @@ class WsHistoricalData(BaseModel):
219
324
  channel: WsChannel
220
325
  coin: str
221
326
  timestamp: int
222
- data: dict
327
+ data: dict[str, Any]
223
328
 
224
329
 
225
330
  # =============================================================================
226
- # WebSocket Bulk Stream Types (Option D - Like Databento)
331
+ # WebSocket Bulk Stream Types (Bulk Download Mode)
227
332
  # =============================================================================
228
333
 
229
334
 
@@ -234,35 +339,32 @@ class WsStreamStarted(BaseModel):
234
339
  channel: WsChannel
235
340
  coin: str
236
341
  start: int
342
+ """Start timestamp in milliseconds."""
237
343
  end: int
238
- batch_size: int
239
- total_records: int
344
+ """End timestamp in milliseconds."""
240
345
 
241
346
 
242
347
  class WsStreamProgress(BaseModel):
243
- """Stream progress response."""
348
+ """Stream progress response (sent every ~2 seconds)."""
244
349
 
245
350
  type: Literal["stream_progress"]
246
- records_sent: int
247
- total_records: int
248
- progress_pct: float
351
+ snapshots_sent: int
249
352
 
250
353
 
251
354
  class TimestampedRecord(BaseModel):
252
- """A record with timestamp."""
355
+ """A record with timestamp for batched data."""
253
356
 
254
357
  timestamp: int
255
- data: dict
358
+ data: dict[str, Any]
256
359
 
257
360
 
258
361
  class WsHistoricalBatch(BaseModel):
259
- """Stream batch (bulk data)."""
362
+ """Batch of historical data (bulk streaming)."""
260
363
 
261
364
  type: Literal["historical_batch"]
262
365
  channel: WsChannel
263
366
  coin: str
264
- batch_index: int
265
- records: list[TimestampedRecord]
367
+ data: list[TimestampedRecord]
266
368
 
267
369
 
268
370
  class WsStreamCompleted(BaseModel):
@@ -271,13 +373,14 @@ class WsStreamCompleted(BaseModel):
271
373
  type: Literal["stream_completed"]
272
374
  channel: WsChannel
273
375
  coin: str
274
- records_sent: int
376
+ snapshots_sent: int
275
377
 
276
378
 
277
379
  class WsStreamStopped(BaseModel):
278
380
  """Stream stopped response."""
279
381
 
280
382
  type: Literal["stream_stopped"]
383
+ snapshots_sent: int
281
384
 
282
385
 
283
386
  # =============================================================================
@@ -302,3 +405,4 @@ class OxArchiveError(Exception):
302
405
 
303
406
  # Type alias for timestamp parameters
304
407
  Timestamp = Union[int, str, datetime]
408
+ """Timestamp can be Unix ms (int), ISO string, or datetime object."""
oxarchive/websocket.py CHANGED
@@ -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. "
@@ -41,6 +42,7 @@ except ImportError:
41
42
 
42
43
  from .types import (
43
44
  OrderBook,
45
+ PriceLevel,
44
46
  Trade,
45
47
  WsChannel,
46
48
  WsConnectionState,
@@ -108,14 +110,74 @@ ErrorHandler = Callable[[Exception], None]
108
110
 
109
111
  # Replay handlers
110
112
  HistoricalDataHandler = Callable[[str, int, dict], None]
111
- ReplayStartHandler = Callable[[WsChannel, str, int, float], None]
112
- ReplayCompleteHandler = Callable[[WsChannel, str, int], None]
113
+ ReplayStartHandler = Callable[[WsChannel, str, int, int, float], None] # channel, coin, start, end, speed
114
+ ReplayCompleteHandler = Callable[[WsChannel, str, int], None] # channel, coin, snapshots_sent
113
115
 
114
116
  # Stream handlers
115
117
  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]
118
+ StreamStartHandler = Callable[[WsChannel, str, int, int], None] # channel, coin, start, end
119
+ StreamProgressHandler = Callable[[int], None] # snapshots_sent
120
+ StreamCompleteHandler = Callable[[WsChannel, str, int], None] # channel, coin, snapshots_sent
121
+
122
+
123
+ def _transform_orderbook(coin: str, raw: dict) -> OrderBook:
124
+ """Transform raw Hyperliquid orderbook format to SDK OrderBook type.
125
+
126
+ Raw format: { coin, levels: [[{px, sz, n}, ...], [{px, sz, n}, ...]], time }
127
+ SDK format: { coin, timestamp, bids: [{px, sz, n}], asks: [{px, sz, n}], mid_price, spread, spread_bps }
128
+ """
129
+ from datetime import datetime
130
+
131
+ # Check if already in SDK format (from REST API or historical replay)
132
+ if "bids" in raw and "asks" in raw:
133
+ return OrderBook(**raw)
134
+
135
+ # Transform from Hyperliquid raw format
136
+ # levels is [[{px, sz, n}, ...], [{px, sz, n}, ...]] where [0]=bids, [1]=asks
137
+ levels = raw.get("levels", [[], []])
138
+ time_ms = raw.get("time")
139
+
140
+ bids = []
141
+ asks = []
142
+
143
+ if len(levels) >= 2:
144
+ # levels[0] = bids, levels[1] = asks
145
+ # Each level is already {px, sz, n} object
146
+ for level in levels[0] or []:
147
+ if isinstance(level, dict):
148
+ bids.append(PriceLevel(px=str(level.get("px", "0")), sz=str(level.get("sz", "0")), n=int(level.get("n", 0))))
149
+ for level in levels[1] or []:
150
+ if isinstance(level, dict):
151
+ asks.append(PriceLevel(px=str(level.get("px", "0")), sz=str(level.get("sz", "0")), n=int(level.get("n", 0))))
152
+
153
+ # Calculate mid price and spread
154
+ mid_price = None
155
+ spread = None
156
+ spread_bps = None
157
+
158
+ if bids and asks:
159
+ best_bid = float(bids[0].px)
160
+ best_ask = float(asks[0].px)
161
+ mid = (best_bid + best_ask) / 2
162
+ mid_price = str(mid)
163
+ spread = str(best_ask - best_bid)
164
+ spread_bps = f"{((best_ask - best_bid) / mid * 10000):.2f}"
165
+
166
+ # Convert timestamp
167
+ if time_ms:
168
+ timestamp = datetime.utcfromtimestamp(time_ms / 1000).isoformat() + "Z"
169
+ else:
170
+ timestamp = datetime.utcnow().isoformat() + "Z"
171
+
172
+ return OrderBook(
173
+ coin=coin,
174
+ timestamp=timestamp,
175
+ bids=bids,
176
+ asks=asks,
177
+ mid_price=mid_price,
178
+ spread=spread,
179
+ spread_bps=spread_bps,
180
+ )
119
181
 
120
182
 
121
183
  class OxArchiveWs:
@@ -164,7 +226,7 @@ class OxArchiveWs:
164
226
  @property
165
227
  def is_connected(self) -> bool:
166
228
  """Check if connected."""
167
- return self._ws is not None and self._ws.open
229
+ return self._ws is not None and self._ws.state == WsState.OPEN
168
230
 
169
231
  async def connect(self) -> None:
170
232
  """Connect to the WebSocket server."""
@@ -421,14 +483,14 @@ class OxArchiveWs:
421
483
  def on_replay_start(self, handler: ReplayStartHandler) -> None:
422
484
  """Set handler for replay started event.
423
485
 
424
- Handler receives: (channel, coin, total_records, speed)
486
+ Handler receives: (channel, coin, start, end, speed)
425
487
  """
426
488
  self._on_replay_start = handler
427
489
 
428
490
  def on_replay_complete(self, handler: ReplayCompleteHandler) -> None:
429
491
  """Set handler for replay completed event.
430
492
 
431
- Handler receives: (channel, coin, records_sent)
493
+ Handler receives: (channel, coin, snapshots_sent)
432
494
  """
433
495
  self._on_replay_complete = handler
434
496
 
@@ -444,21 +506,21 @@ class OxArchiveWs:
444
506
  def on_stream_start(self, handler: StreamStartHandler) -> None:
445
507
  """Set handler for stream started event.
446
508
 
447
- Handler receives: (channel, coin, total_records)
509
+ Handler receives: (channel, coin, start, end)
448
510
  """
449
511
  self._on_stream_start = handler
450
512
 
451
513
  def on_stream_progress(self, handler: StreamProgressHandler) -> None:
452
514
  """Set handler for stream progress event.
453
515
 
454
- Handler receives: (records_sent, total_records, progress_pct)
516
+ Handler receives: (snapshots_sent)
455
517
  """
456
518
  self._on_stream_progress = handler
457
519
 
458
520
  def on_stream_complete(self, handler: StreamCompleteHandler) -> None:
459
521
  """Set handler for stream completed event.
460
522
 
461
- Handler receives: (channel, coin, records_sent)
523
+ Handler receives: (channel, coin, snapshots_sent)
462
524
  """
463
525
  self._on_stream_complete = handler
464
526
 
@@ -575,7 +637,8 @@ class OxArchiveWs:
575
637
  raw_data = data.get("data", {})
576
638
 
577
639
  if channel == "orderbook" and self._on_orderbook:
578
- orderbook = OrderBook(**raw_data)
640
+ # Transform raw Hyperliquid format to SDK OrderBook type
641
+ orderbook = _transform_orderbook(coin, raw_data)
579
642
  self._on_orderbook(coin, orderbook)
580
643
 
581
644
  elif channel == "trades" and self._on_trades:
@@ -585,30 +648,28 @@ class OxArchiveWs:
585
648
  # Replay messages (Option B)
586
649
  elif msg_type == "replay_started" and self._on_replay_start:
587
650
  self._on_replay_start(
588
- data["channel"], data["coin"], data["total_records"], data["speed"]
651
+ data["channel"], data["coin"], data["start"], data["end"], data["speed"]
589
652
  )
590
653
 
591
654
  elif msg_type == "historical_data" and self._on_historical_data:
592
655
  self._on_historical_data(data["coin"], data["timestamp"], data["data"])
593
656
 
594
657
  elif msg_type == "replay_completed" and self._on_replay_complete:
595
- self._on_replay_complete(data["channel"], data["coin"], data["records_sent"])
658
+ self._on_replay_complete(data["channel"], data["coin"], data["snapshots_sent"])
596
659
 
597
660
  # Stream messages (Option D)
598
661
  elif msg_type == "stream_started" and self._on_stream_start:
599
- self._on_stream_start(data["channel"], data["coin"], data["total_records"])
662
+ self._on_stream_start(data["channel"], data["coin"], data["start"], data["end"])
600
663
 
601
664
  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
- )
665
+ self._on_stream_progress(data["snapshots_sent"])
605
666
 
606
667
  elif msg_type == "historical_batch" and self._on_batch:
607
- records = [TimestampedRecord(**r) for r in data["records"]]
668
+ records = [TimestampedRecord(**r) for r in data["data"]]
608
669
  self._on_batch(data["coin"], records)
609
670
 
610
671
  elif msg_type == "stream_completed" and self._on_stream_complete:
611
- self._on_stream_complete(data["channel"], data["coin"], data["records_sent"])
672
+ self._on_stream_complete(data["channel"], data["coin"], data["snapshots_sent"])
612
673
 
613
674
  except Exception as e:
614
675
  logger.error(f"Error handling message: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oxarchive
3
- Version: 0.1.1
3
+ Version: 0.3.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
@@ -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:
@@ -0,0 +1,14 @@
1
+ oxarchive/__init__.py,sha256=QqBnxRIGEBXE2wHdmynznUEBoJsePN_xObo_v79lPZI,2181
2
+ oxarchive/client.py,sha256=3P0fvOcyM5BWppkVV4054NduDHKvRg-cWeluoGymmRk,3163
3
+ oxarchive/http.py,sha256=LJgw488_nNb14ixRoDv5ChuhUvyzTGtQ4aTSdEEwXww,3577
4
+ oxarchive/types.py,sha256=8n_R41IjdNnshPscY5zAfrHa855EbvSTSdbi-GZExKo,10434
5
+ oxarchive/websocket.py,sha256=BM6CmUbRdYptNnlicmAVPKBdqlyZBSI1l5iUt_OzWwo,25126
6
+ oxarchive/resources/__init__.py,sha256=WQ4GYQ8p3L0D2Isk4IV4h1DRpvyZlt6tOF1t_CJr6ls,385
7
+ oxarchive/resources/funding.py,sha256=TXkZxodVQTVcVbzNG6SpMQAzf8AkLm2NYZJxnP4MNXw,3500
8
+ oxarchive/resources/instruments.py,sha256=flD1sH6x3P3CTqV1ZwkfwbranVacmhsHn5Dhr7lGQhM,1606
9
+ oxarchive/resources/openinterest.py,sha256=h13yLA72LpfryUf8IqF6W7uE4ObYY2Qbc-auv4LtPqc,3552
10
+ oxarchive/resources/orderbook.py,sha256=o_DTdpzKrZvHL9YXm8cGGUugPM8uUa6r9O_72r1ByV0,4557
11
+ oxarchive/resources/trades.py,sha256=XCi2rXA2hxaTt0KNlWw8f7W0hzAvNWyT7DaivMz_rHw,10012
12
+ oxarchive-0.3.0.dist-info/METADATA,sha256=yq_BurBDJwE1f1Nv6IANatT2KPE0uJ-7OUaY1N-omPk,8749
13
+ oxarchive-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ oxarchive-0.3.0.dist-info/RECORD,,
@@ -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,15 +0,0 @@
1
- oxarchive/__init__.py,sha256=hRUWIJhKmm8d4XBt2gWCSQ6j-9VNaC52y89rXpAKvH0,2253
2
- oxarchive/client.py,sha256=tIvsxR9qE92xIeGwazJdwqY7bawFe_ZlzEWG0NsNxsw,3268
3
- oxarchive/http.py,sha256=LJgw488_nNb14ixRoDv5ChuhUvyzTGtQ4aTSdEEwXww,3577
4
- oxarchive/types.py,sha256=9eCETwCVE_cewap8El2GNSJcYN3BXaeImpyqLneyPUM,7135
5
- oxarchive/websocket.py,sha256=yByFOfeU6yrHhmd5Eo8FFs4hNBHuL1O65ZH-wsmG1vE,22806
6
- oxarchive/resources/__init__.py,sha256=4gt5xVlnPhLUjk2n03ldJ9VCnodgdR75U9d1H88a7BE,447
7
- oxarchive/resources/candles.py,sha256=IhyBCm65CZz4Nsm6IPqfMZHQl_3K4H3jHgChfigc3QU,3147
8
- oxarchive/resources/funding.py,sha256=JrJ1LqXy8GX6U70zOI89lLIX_d5vM_Rq0qFJy0YRof0,3518
9
- oxarchive/resources/instruments.py,sha256=flD1sH6x3P3CTqV1ZwkfwbranVacmhsHn5Dhr7lGQhM,1606
10
- oxarchive/resources/openinterest.py,sha256=trMFr6TEEV88WQkpptQCUEJTy0aaoAy3QnBx9opNZH8,3570
11
- oxarchive/resources/orderbook.py,sha256=ZfK3HhYrpVpELz6GDb9qpGcWzQ3p4-uxUyEY2Emhr0o,4575
12
- oxarchive/resources/trades.py,sha256=4sXgyBVaq6RHFJe0jeOvFBIX-bDCe8FwgoeeeM_He3Q,3923
13
- oxarchive-0.1.1.dist-info/METADATA,sha256=MLL3mweHZ7l9MA0TsdTRvY3qj0V4-_64b5vRGyd0txU,8956
14
- oxarchive-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
15
- oxarchive-0.1.1.dist-info/RECORD,,