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.
- {oxarchive-0.1.0 → oxarchive-0.2.0}/PKG-INFO +2 -2
- {oxarchive-0.1.0 → oxarchive-0.2.0}/README.md +1 -1
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/client.py +0 -4
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/resources/__init__.py +0 -2
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/types.py +164 -62
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/websocket.py +19 -2
- {oxarchive-0.1.0 → oxarchive-0.2.0}/pyproject.toml +1 -1
- oxarchive-0.1.0/oxarchive/resources/candles.py +0 -100
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/__init__.py +0 -0
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/http.py +0 -0
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/resources/funding.py +0 -0
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/resources/instruments.py +0 -0
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/resources/openinterest.py +0 -0
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/resources/orderbook.py +0 -0
- {oxarchive-0.1.0 → oxarchive-0.2.0}/oxarchive/resources/trades.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: oxarchive
|
|
3
|
-
Version: 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://
|
|
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://
|
|
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
|
-
|
|
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
|
-
"""
|
|
53
|
+
"""L2 order book snapshot."""
|
|
31
54
|
|
|
32
55
|
coin: str
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
timestamp: int
|
|
56
|
-
trade_type: str
|
|
95
|
+
"""Trade size."""
|
|
57
96
|
|
|
97
|
+
timestamp: datetime
|
|
98
|
+
"""Execution timestamp (UTC)."""
|
|
58
99
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
# =============================================================================
|
|
100
|
+
tx_hash: Optional[str] = None
|
|
101
|
+
"""Blockchain transaction hash."""
|
|
62
102
|
|
|
63
|
-
|
|
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
|
-
|
|
67
|
-
"""
|
|
109
|
+
crossed: Optional[bool] = None
|
|
110
|
+
"""True if taker (crossed the spread), false if maker."""
|
|
68
111
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
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
|
-
|
|
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"
|
|
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
|
-
"""
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
360
|
+
"""Batch of historical data (bulk streaming)."""
|
|
260
361
|
|
|
261
362
|
type: Literal["historical_batch"]
|
|
262
363
|
channel: WsChannel
|
|
263
364
|
coin: str
|
|
264
|
-
|
|
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
|
-
|
|
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://
|
|
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
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|