oxarchive 0.1.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 +101 -0
- oxarchive/client.py +114 -0
- oxarchive/http.py +108 -0
- oxarchive/resources/__init__.py +17 -0
- oxarchive/resources/candles.py +100 -0
- oxarchive/resources/funding.py +113 -0
- oxarchive/resources/instruments.py +55 -0
- oxarchive/resources/openinterest.py +113 -0
- oxarchive/resources/orderbook.py +148 -0
- oxarchive/resources/trades.py +125 -0
- oxarchive/types.py +304 -0
- oxarchive/websocket.py +612 -0
- oxarchive-0.1.0.dist-info/METADATA +401 -0
- oxarchive-0.1.0.dist-info/RECORD +15 -0
- oxarchive-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Open interest 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 OpenInterest, Timestamp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OpenInterestResource:
|
|
13
|
+
"""
|
|
14
|
+
Open interest API resource.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> # Get current open interest
|
|
18
|
+
>>> current = client.open_interest.current("BTC")
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Get open interest history
|
|
21
|
+
>>> history = client.open_interest.history("ETH", start="2024-01-01", end="2024-01-07")
|
|
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 history(
|
|
44
|
+
self,
|
|
45
|
+
coin: str,
|
|
46
|
+
*,
|
|
47
|
+
start: Optional[Timestamp] = None,
|
|
48
|
+
end: Optional[Timestamp] = None,
|
|
49
|
+
limit: Optional[int] = None,
|
|
50
|
+
offset: Optional[int] = None,
|
|
51
|
+
) -> list[OpenInterest]:
|
|
52
|
+
"""
|
|
53
|
+
Get open interest history for a coin.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
57
|
+
start: Start timestamp
|
|
58
|
+
end: End timestamp
|
|
59
|
+
limit: Maximum number of results
|
|
60
|
+
offset: Number of results to skip
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
List of open interest records
|
|
64
|
+
"""
|
|
65
|
+
data = self._http.get(
|
|
66
|
+
f"/v1/openinterest/{coin.upper()}",
|
|
67
|
+
params={
|
|
68
|
+
"start": self._convert_timestamp(start),
|
|
69
|
+
"end": self._convert_timestamp(end),
|
|
70
|
+
"limit": limit,
|
|
71
|
+
"offset": offset,
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
return [OpenInterest.model_validate(item) for item in data["data"]]
|
|
75
|
+
|
|
76
|
+
async def ahistory(
|
|
77
|
+
self,
|
|
78
|
+
coin: str,
|
|
79
|
+
*,
|
|
80
|
+
start: Optional[Timestamp] = None,
|
|
81
|
+
end: Optional[Timestamp] = None,
|
|
82
|
+
limit: Optional[int] = None,
|
|
83
|
+
offset: Optional[int] = None,
|
|
84
|
+
) -> list[OpenInterest]:
|
|
85
|
+
"""Async version of history()."""
|
|
86
|
+
data = await self._http.aget(
|
|
87
|
+
f"/v1/openinterest/{coin.upper()}",
|
|
88
|
+
params={
|
|
89
|
+
"start": self._convert_timestamp(start),
|
|
90
|
+
"end": self._convert_timestamp(end),
|
|
91
|
+
"limit": limit,
|
|
92
|
+
"offset": offset,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
return [OpenInterest.model_validate(item) for item in data["data"]]
|
|
96
|
+
|
|
97
|
+
def current(self, coin: str) -> OpenInterest:
|
|
98
|
+
"""
|
|
99
|
+
Get current open interest for a coin.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Current open interest
|
|
106
|
+
"""
|
|
107
|
+
data = self._http.get(f"/v1/openinterest/{coin.upper()}/current")
|
|
108
|
+
return OpenInterest.model_validate(data["data"])
|
|
109
|
+
|
|
110
|
+
async def acurrent(self, coin: str) -> OpenInterest:
|
|
111
|
+
"""Async version of current()."""
|
|
112
|
+
data = await self._http.aget(f"/v1/openinterest/{coin.upper()}/current")
|
|
113
|
+
return OpenInterest.model_validate(data["data"])
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Order book API resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Optional, Union
|
|
7
|
+
|
|
8
|
+
from ..http import HttpClient
|
|
9
|
+
from ..types import OrderBook, Timestamp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class OrderBookResource:
|
|
13
|
+
"""
|
|
14
|
+
Order book API resource.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> # Get current order book
|
|
18
|
+
>>> orderbook = client.orderbook.get("BTC")
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Get order book at specific timestamp
|
|
21
|
+
>>> historical = client.orderbook.get("ETH", timestamp=1704067200000)
|
|
22
|
+
>>>
|
|
23
|
+
>>> # Get order book history
|
|
24
|
+
>>> history = client.orderbook.history("BTC", start="2024-01-01", end="2024-01-02")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, http: HttpClient):
|
|
28
|
+
self._http = http
|
|
29
|
+
|
|
30
|
+
def _convert_timestamp(self, ts: Optional[Timestamp]) -> Optional[int]:
|
|
31
|
+
"""Convert timestamp to Unix milliseconds."""
|
|
32
|
+
if ts is None:
|
|
33
|
+
return None
|
|
34
|
+
if isinstance(ts, int):
|
|
35
|
+
return ts
|
|
36
|
+
if isinstance(ts, datetime):
|
|
37
|
+
return int(ts.timestamp() * 1000)
|
|
38
|
+
if isinstance(ts, str):
|
|
39
|
+
# Try parsing ISO format
|
|
40
|
+
try:
|
|
41
|
+
dt = datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
42
|
+
return int(dt.timestamp() * 1000)
|
|
43
|
+
except ValueError:
|
|
44
|
+
return int(ts)
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def get(
|
|
48
|
+
self,
|
|
49
|
+
coin: str,
|
|
50
|
+
*,
|
|
51
|
+
timestamp: Optional[Timestamp] = None,
|
|
52
|
+
depth: Optional[int] = None,
|
|
53
|
+
) -> OrderBook:
|
|
54
|
+
"""
|
|
55
|
+
Get order book snapshot for a coin.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
59
|
+
timestamp: Optional timestamp to get historical snapshot
|
|
60
|
+
depth: Number of price levels to return per side
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Order book snapshot
|
|
64
|
+
"""
|
|
65
|
+
data = self._http.get(
|
|
66
|
+
f"/v1/orderbook/{coin.upper()}",
|
|
67
|
+
params={
|
|
68
|
+
"timestamp": self._convert_timestamp(timestamp),
|
|
69
|
+
"depth": depth,
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
return OrderBook.model_validate(data["data"])
|
|
73
|
+
|
|
74
|
+
async def aget(
|
|
75
|
+
self,
|
|
76
|
+
coin: str,
|
|
77
|
+
*,
|
|
78
|
+
timestamp: Optional[Timestamp] = None,
|
|
79
|
+
depth: Optional[int] = None,
|
|
80
|
+
) -> OrderBook:
|
|
81
|
+
"""Async version of get()."""
|
|
82
|
+
data = await self._http.aget(
|
|
83
|
+
f"/v1/orderbook/{coin.upper()}",
|
|
84
|
+
params={
|
|
85
|
+
"timestamp": self._convert_timestamp(timestamp),
|
|
86
|
+
"depth": depth,
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
return OrderBook.model_validate(data["data"])
|
|
90
|
+
|
|
91
|
+
def history(
|
|
92
|
+
self,
|
|
93
|
+
coin: str,
|
|
94
|
+
*,
|
|
95
|
+
start: Optional[Timestamp] = None,
|
|
96
|
+
end: Optional[Timestamp] = None,
|
|
97
|
+
limit: Optional[int] = None,
|
|
98
|
+
offset: Optional[int] = None,
|
|
99
|
+
depth: Optional[int] = None,
|
|
100
|
+
) -> list[OrderBook]:
|
|
101
|
+
"""
|
|
102
|
+
Get historical order book snapshots.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
106
|
+
start: Start timestamp
|
|
107
|
+
end: End timestamp
|
|
108
|
+
limit: Maximum number of results
|
|
109
|
+
offset: Number of results to skip
|
|
110
|
+
depth: Number of price levels per side
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of order book snapshots
|
|
114
|
+
"""
|
|
115
|
+
data = self._http.get(
|
|
116
|
+
f"/v1/orderbook/{coin.upper()}/history",
|
|
117
|
+
params={
|
|
118
|
+
"start": self._convert_timestamp(start),
|
|
119
|
+
"end": self._convert_timestamp(end),
|
|
120
|
+
"limit": limit,
|
|
121
|
+
"offset": offset,
|
|
122
|
+
"depth": depth,
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
return [OrderBook.model_validate(item) for item in data["data"]]
|
|
126
|
+
|
|
127
|
+
async def ahistory(
|
|
128
|
+
self,
|
|
129
|
+
coin: str,
|
|
130
|
+
*,
|
|
131
|
+
start: Optional[Timestamp] = None,
|
|
132
|
+
end: Optional[Timestamp] = None,
|
|
133
|
+
limit: Optional[int] = None,
|
|
134
|
+
offset: Optional[int] = None,
|
|
135
|
+
depth: Optional[int] = None,
|
|
136
|
+
) -> list[OrderBook]:
|
|
137
|
+
"""Async version of history()."""
|
|
138
|
+
data = await self._http.aget(
|
|
139
|
+
f"/v1/orderbook/{coin.upper()}/history",
|
|
140
|
+
params={
|
|
141
|
+
"start": self._convert_timestamp(start),
|
|
142
|
+
"end": self._convert_timestamp(end),
|
|
143
|
+
"limit": limit,
|
|
144
|
+
"offset": offset,
|
|
145
|
+
"depth": depth,
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
return [OrderBook.model_validate(item) for item in data["data"]]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Trades API resource."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import Literal, Optional
|
|
7
|
+
|
|
8
|
+
from ..http import HttpClient
|
|
9
|
+
from ..types import Trade, Timestamp
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TradesResource:
|
|
13
|
+
"""
|
|
14
|
+
Trades API resource.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
>>> # Get recent trades
|
|
18
|
+
>>> trades = client.trades.recent("BTC")
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Get trade history with time range
|
|
21
|
+
>>> history = client.trades.list("ETH", start="2024-01-01", end="2024-01-02")
|
|
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
|
+
start: Optional[Timestamp] = None,
|
|
48
|
+
end: Optional[Timestamp] = None,
|
|
49
|
+
limit: Optional[int] = None,
|
|
50
|
+
offset: Optional[int] = None,
|
|
51
|
+
side: Optional[Literal["buy", "sell"]] = None,
|
|
52
|
+
) -> list[Trade]:
|
|
53
|
+
"""
|
|
54
|
+
Get trade history for a coin.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
58
|
+
start: Start timestamp
|
|
59
|
+
end: End timestamp
|
|
60
|
+
limit: Maximum number of results
|
|
61
|
+
offset: Number of results to skip
|
|
62
|
+
side: Filter by trade side
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of trades
|
|
66
|
+
"""
|
|
67
|
+
data = self._http.get(
|
|
68
|
+
f"/v1/trades/{coin.upper()}",
|
|
69
|
+
params={
|
|
70
|
+
"start": self._convert_timestamp(start),
|
|
71
|
+
"end": self._convert_timestamp(end),
|
|
72
|
+
"limit": limit,
|
|
73
|
+
"offset": offset,
|
|
74
|
+
"side": side,
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
return [Trade.model_validate(item) for item in data["data"]]
|
|
78
|
+
|
|
79
|
+
async def alist(
|
|
80
|
+
self,
|
|
81
|
+
coin: str,
|
|
82
|
+
*,
|
|
83
|
+
start: Optional[Timestamp] = None,
|
|
84
|
+
end: Optional[Timestamp] = None,
|
|
85
|
+
limit: Optional[int] = None,
|
|
86
|
+
offset: Optional[int] = None,
|
|
87
|
+
side: Optional[Literal["buy", "sell"]] = None,
|
|
88
|
+
) -> list[Trade]:
|
|
89
|
+
"""Async version of list()."""
|
|
90
|
+
data = await self._http.aget(
|
|
91
|
+
f"/v1/trades/{coin.upper()}",
|
|
92
|
+
params={
|
|
93
|
+
"start": self._convert_timestamp(start),
|
|
94
|
+
"end": self._convert_timestamp(end),
|
|
95
|
+
"limit": limit,
|
|
96
|
+
"offset": offset,
|
|
97
|
+
"side": side,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
return [Trade.model_validate(item) for item in data["data"]]
|
|
101
|
+
|
|
102
|
+
def recent(self, coin: str, limit: Optional[int] = None) -> list[Trade]:
|
|
103
|
+
"""
|
|
104
|
+
Get most recent trades for a coin.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
coin: The coin symbol (e.g., 'BTC', 'ETH')
|
|
108
|
+
limit: Number of trades to return (default: 100)
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
List of recent trades
|
|
112
|
+
"""
|
|
113
|
+
data = self._http.get(
|
|
114
|
+
f"/v1/trades/{coin.upper()}/recent",
|
|
115
|
+
params={"limit": limit},
|
|
116
|
+
)
|
|
117
|
+
return [Trade.model_validate(item) for item in data["data"]]
|
|
118
|
+
|
|
119
|
+
async def arecent(self, coin: str, limit: Optional[int] = None) -> list[Trade]:
|
|
120
|
+
"""Async version of recent()."""
|
|
121
|
+
data = await self._http.aget(
|
|
122
|
+
f"/v1/trades/{coin.upper()}/recent",
|
|
123
|
+
params={"limit": limit},
|
|
124
|
+
)
|
|
125
|
+
return [Trade.model_validate(item) for item in data["data"]]
|
oxarchive/types.py
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
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]
|