cryptovol 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.
- cryptovol/__init__.py +60 -0
- cryptovol/client.py +443 -0
- cryptovol/exceptions.py +64 -0
- cryptovol/models.py +194 -0
- cryptovol/py.typed +0 -0
- cryptovol-0.1.0.dist-info/METADATA +281 -0
- cryptovol-0.1.0.dist-info/RECORD +9 -0
- cryptovol-0.1.0.dist-info/WHEEL +4 -0
- cryptovol-0.1.0.dist-info/licenses/LICENSE +21 -0
cryptovol/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""CryptoVol — Python SDK for the CryptoVol crypto implied volatility API.
|
|
2
|
+
|
|
3
|
+
Quick start
|
|
4
|
+
-----------
|
|
5
|
+
|
|
6
|
+
from cryptovol import CryptoVol
|
|
7
|
+
|
|
8
|
+
cv = CryptoVol(api_key="YOUR_RAPIDAPI_KEY")
|
|
9
|
+
idx = cv.vol_index(ccy="BTC", tenor="30D")
|
|
10
|
+
print(idx.data[-1].vol)
|
|
11
|
+
|
|
12
|
+
See https://github.com/YOUR_GH_USERNAME/cryptovol-python for full docs.
|
|
13
|
+
"""
|
|
14
|
+
from .client import CryptoVol
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
CryptoVolError,
|
|
18
|
+
NotFoundError,
|
|
19
|
+
PlanLimitError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
ServerError,
|
|
22
|
+
TimeoutError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
)
|
|
25
|
+
from .models import (
|
|
26
|
+
Analytics,
|
|
27
|
+
BulkResultItem,
|
|
28
|
+
BulkVolResponse,
|
|
29
|
+
Greeks,
|
|
30
|
+
VolHistoryPoint,
|
|
31
|
+
VolHistoryResponse,
|
|
32
|
+
VolIndexPoint,
|
|
33
|
+
VolIndexResponse,
|
|
34
|
+
VolSurfacePoint,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"CryptoVol",
|
|
41
|
+
# Exceptions
|
|
42
|
+
"CryptoVolError",
|
|
43
|
+
"AuthenticationError",
|
|
44
|
+
"PlanLimitError",
|
|
45
|
+
"NotFoundError",
|
|
46
|
+
"ValidationError",
|
|
47
|
+
"RateLimitError",
|
|
48
|
+
"ServerError",
|
|
49
|
+
"TimeoutError",
|
|
50
|
+
# Models
|
|
51
|
+
"Analytics",
|
|
52
|
+
"Greeks",
|
|
53
|
+
"VolIndexResponse",
|
|
54
|
+
"VolIndexPoint",
|
|
55
|
+
"VolSurfacePoint",
|
|
56
|
+
"BulkVolResponse",
|
|
57
|
+
"BulkResultItem",
|
|
58
|
+
"VolHistoryResponse",
|
|
59
|
+
"VolHistoryPoint",
|
|
60
|
+
]
|
cryptovol/client.py
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
"""Synchronous client for the CryptoVol API.
|
|
2
|
+
|
|
3
|
+
Quick start
|
|
4
|
+
-----------
|
|
5
|
+
|
|
6
|
+
from cryptovol import CryptoVol
|
|
7
|
+
|
|
8
|
+
cv = CryptoVol(api_key="YOUR_RAPIDAPI_KEY")
|
|
9
|
+
|
|
10
|
+
# Daily 30-day BTC vol index
|
|
11
|
+
idx = cv.vol_index(ccy="BTC", tenor="30D")
|
|
12
|
+
print(idx.data[-1].vol)
|
|
13
|
+
|
|
14
|
+
# ATM vol for a specific expiry
|
|
15
|
+
pt = cv.vol_surface(
|
|
16
|
+
ccy="BTC", expiry="2026-12-26",
|
|
17
|
+
strike_type="moneyness", strike_value=1.0,
|
|
18
|
+
include_analytics=True,
|
|
19
|
+
)
|
|
20
|
+
print(pt.vol, pt.analytics.greeks.delta)
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
26
|
+
|
|
27
|
+
import httpx
|
|
28
|
+
|
|
29
|
+
from .exceptions import (
|
|
30
|
+
AuthenticationError,
|
|
31
|
+
CryptoVolError,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
PlanLimitError,
|
|
34
|
+
RateLimitError,
|
|
35
|
+
ServerError,
|
|
36
|
+
TimeoutError as CryptoVolTimeoutError,
|
|
37
|
+
ValidationError,
|
|
38
|
+
)
|
|
39
|
+
from .models import (
|
|
40
|
+
BulkVolResponse,
|
|
41
|
+
VolHistoryResponse,
|
|
42
|
+
VolIndexResponse,
|
|
43
|
+
VolSurfacePoint,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
DEFAULT_BASE_URL = "https://cryptovol.p.rapidapi.com"
|
|
47
|
+
DEFAULT_HOST = "cryptovol.p.rapidapi.com"
|
|
48
|
+
DEFAULT_TIMEOUT = 30.0
|
|
49
|
+
DEFAULT_MAX_RETRIES = 3
|
|
50
|
+
DEFAULT_USER_AGENT = "cryptovol-python/0.1.0"
|
|
51
|
+
|
|
52
|
+
StrikeType = Literal["strike", "moneyness", "delta"]
|
|
53
|
+
OptionType = Literal["C", "P"]
|
|
54
|
+
Session = Literal["asia", "london", "us"]
|
|
55
|
+
Tenor = Literal[
|
|
56
|
+
"1D", "1W", "2W", "3W", "1M", "2M", "3M", "6M", "9M", "1Y",
|
|
57
|
+
"7D", "14D", "21D", "30D", "60D", "90D", "180D", "270D", "365D",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CryptoVol:
|
|
62
|
+
"""Synchronous client for the CryptoVol REST API.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
api_key:
|
|
67
|
+
Your RapidAPI key. Get one at https://www.cryptovol.io/api.
|
|
68
|
+
Sent as ``X-RapidAPI-Key`` on every request.
|
|
69
|
+
base_url:
|
|
70
|
+
Override the API base URL. Defaults to the RapidAPI gateway.
|
|
71
|
+
host:
|
|
72
|
+
Value for the ``X-RapidAPI-Host`` header. You won't need to change
|
|
73
|
+
this unless you're routing through a different gateway.
|
|
74
|
+
timeout:
|
|
75
|
+
Per-request timeout in seconds. Defaults to 30s.
|
|
76
|
+
max_retries:
|
|
77
|
+
How many times to retry transient failures (network errors and 5xx
|
|
78
|
+
responses) with exponential backoff. Set to 0 to disable.
|
|
79
|
+
user_agent:
|
|
80
|
+
Custom User-Agent header. Useful if you're embedding the SDK in your
|
|
81
|
+
own product and want clean attribution in server logs.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
api_key: str,
|
|
87
|
+
*,
|
|
88
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
89
|
+
host: str = DEFAULT_HOST,
|
|
90
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
91
|
+
max_retries: int = DEFAULT_MAX_RETRIES,
|
|
92
|
+
user_agent: str = DEFAULT_USER_AGENT,
|
|
93
|
+
) -> None:
|
|
94
|
+
if not api_key:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
"api_key is required. Get a key at https://www.cryptovol.io/api"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self.api_key = api_key
|
|
100
|
+
self.base_url = base_url.rstrip("/")
|
|
101
|
+
self.host = host
|
|
102
|
+
self.timeout = timeout
|
|
103
|
+
self.max_retries = max_retries
|
|
104
|
+
|
|
105
|
+
self._client = httpx.Client(
|
|
106
|
+
base_url=self.base_url,
|
|
107
|
+
timeout=timeout,
|
|
108
|
+
headers={
|
|
109
|
+
"X-RapidAPI-Key": api_key,
|
|
110
|
+
"X-RapidAPI-Host": host,
|
|
111
|
+
"User-Agent": user_agent,
|
|
112
|
+
"Accept": "application/json",
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# ── Context manager support ──────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
def __enter__(self) -> "CryptoVol":
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
122
|
+
self.close()
|
|
123
|
+
|
|
124
|
+
def close(self) -> None:
|
|
125
|
+
"""Close the underlying HTTP client. Safe to call multiple times."""
|
|
126
|
+
self._client.close()
|
|
127
|
+
|
|
128
|
+
# ── Endpoints ────────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
def vol_index(
|
|
131
|
+
self,
|
|
132
|
+
ccy: str = "BTC",
|
|
133
|
+
tenor: str = "30D",
|
|
134
|
+
start_date: Optional[str] = None,
|
|
135
|
+
end_date: Optional[str] = None,
|
|
136
|
+
*,
|
|
137
|
+
raw: bool = False,
|
|
138
|
+
) -> Union[VolIndexResponse, Dict[str, Any]]:
|
|
139
|
+
"""Daily implied volatility index (CryptoVIX-style time series).
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
ccy:
|
|
144
|
+
Asset symbol — one of ``BTC``, ``ETH``, ``SOL``, ``XRP``, ``AVAX``,
|
|
145
|
+
``TRX``. Subject to your plan tier.
|
|
146
|
+
tenor:
|
|
147
|
+
Constant-maturity tenor (e.g. ``7D``, ``30D``, ``1M``, ``3M``).
|
|
148
|
+
start_date / end_date:
|
|
149
|
+
Optional ``YYYY-MM-DD`` bounds. If omitted, defaults to roughly
|
|
150
|
+
the last year (or the most that your plan allows).
|
|
151
|
+
raw:
|
|
152
|
+
If True, return the parsed JSON dict instead of a typed model.
|
|
153
|
+
"""
|
|
154
|
+
params = _drop_none({
|
|
155
|
+
"ccy": ccy,
|
|
156
|
+
"tenor": tenor,
|
|
157
|
+
"start_date": start_date,
|
|
158
|
+
"end_date": end_date,
|
|
159
|
+
})
|
|
160
|
+
data = self._request("GET", "/v1/vol-index", params=params)
|
|
161
|
+
return data if raw else VolIndexResponse.model_validate(data)
|
|
162
|
+
|
|
163
|
+
def vol_surface(
|
|
164
|
+
self,
|
|
165
|
+
ccy: str,
|
|
166
|
+
expiry: str,
|
|
167
|
+
strike_type: StrikeType,
|
|
168
|
+
strike_value: float,
|
|
169
|
+
*,
|
|
170
|
+
option_type: OptionType = "C",
|
|
171
|
+
session: Session = "us",
|
|
172
|
+
date: Optional[str] = None,
|
|
173
|
+
include_analytics: bool = False,
|
|
174
|
+
raw: bool = False,
|
|
175
|
+
) -> Union[VolSurfacePoint, Dict[str, Any]]:
|
|
176
|
+
"""Single-point SABR-interpolated implied vol query.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
ccy:
|
|
181
|
+
Asset symbol.
|
|
182
|
+
expiry:
|
|
183
|
+
Option expiry date, ``YYYY-MM-DD``. Must be in the future
|
|
184
|
+
relative to the snapshot date.
|
|
185
|
+
strike_type:
|
|
186
|
+
How ``strike_value`` is interpreted:
|
|
187
|
+
|
|
188
|
+
* ``"strike"`` — absolute strike price (e.g. ``100_000``)
|
|
189
|
+
* ``"moneyness"`` — K/S ratio (e.g. ``1.0`` for ATM, ``0.9`` for 10% OTM put)
|
|
190
|
+
* ``"delta"`` — signed/unsigned BS delta magnitude (e.g. ``0.25``)
|
|
191
|
+
strike_value:
|
|
192
|
+
The numeric strike, moneyness, or delta — see ``strike_type``.
|
|
193
|
+
option_type:
|
|
194
|
+
``"C"`` (call, default) or ``"P"`` (put). Required when
|
|
195
|
+
``strike_type="delta"``; ignored otherwise.
|
|
196
|
+
session:
|
|
197
|
+
``"asia"``, ``"london"``, or ``"us"``. Subject to your plan tier.
|
|
198
|
+
date:
|
|
199
|
+
Optional snapshot date (``YYYY-MM-DD``). Defaults to the latest
|
|
200
|
+
available snapshot.
|
|
201
|
+
include_analytics:
|
|
202
|
+
If True, also return spot, forward, yield rate, BS price, and Greeks.
|
|
203
|
+
Requires a PRO or ULTRA plan.
|
|
204
|
+
raw:
|
|
205
|
+
If True, return the parsed JSON dict instead of a typed model.
|
|
206
|
+
"""
|
|
207
|
+
params = _drop_none({
|
|
208
|
+
"ccy": ccy,
|
|
209
|
+
"expiry": expiry,
|
|
210
|
+
"strike_type": strike_type,
|
|
211
|
+
"strike_value": strike_value,
|
|
212
|
+
"option_type": option_type,
|
|
213
|
+
"session": session,
|
|
214
|
+
"date": date,
|
|
215
|
+
"include_analytics": str(include_analytics).lower() if include_analytics else None,
|
|
216
|
+
})
|
|
217
|
+
data = self._request("GET", "/v1/vol-surface", params=params)
|
|
218
|
+
return data if raw else VolSurfacePoint.model_validate(data)
|
|
219
|
+
|
|
220
|
+
def vol_surface_bulk(
|
|
221
|
+
self,
|
|
222
|
+
ccy: str,
|
|
223
|
+
queries: List[Dict[str, Any]],
|
|
224
|
+
*,
|
|
225
|
+
session: Session = "us",
|
|
226
|
+
date: Optional[str] = None,
|
|
227
|
+
raw: bool = False,
|
|
228
|
+
) -> Union[BulkVolResponse, Dict[str, Any]]:
|
|
229
|
+
"""Resolve many (expiry, strike) points in a single request.
|
|
230
|
+
|
|
231
|
+
Far more efficient than looping over :meth:`vol_surface` — one
|
|
232
|
+
snapshot is loaded once and reused for every query.
|
|
233
|
+
|
|
234
|
+
Parameters
|
|
235
|
+
----------
|
|
236
|
+
ccy:
|
|
237
|
+
Asset symbol.
|
|
238
|
+
queries:
|
|
239
|
+
List of dicts, each with keys:
|
|
240
|
+
|
|
241
|
+
* ``expiry`` (str, required) — ``YYYY-MM-DD``
|
|
242
|
+
* ``strike_type`` (str, required) — ``"strike"``, ``"moneyness"``, or ``"delta"``
|
|
243
|
+
* ``strike_value`` (float, required)
|
|
244
|
+
* ``option_type`` (str, optional) — ``"C"`` or ``"P"`` (default ``"C"``)
|
|
245
|
+
* ``include_analytics`` (bool, optional) — default False
|
|
246
|
+
session:
|
|
247
|
+
``"asia"``, ``"london"``, or ``"us"``.
|
|
248
|
+
date:
|
|
249
|
+
Optional snapshot date.
|
|
250
|
+
raw:
|
|
251
|
+
If True, return the parsed JSON dict instead of a typed model.
|
|
252
|
+
|
|
253
|
+
Example
|
|
254
|
+
-------
|
|
255
|
+
|
|
256
|
+
response = cv.vol_surface_bulk(
|
|
257
|
+
ccy="BTC",
|
|
258
|
+
queries=[
|
|
259
|
+
{"expiry": "2026-09-26", "strike_type": "moneyness", "strike_value": 0.9},
|
|
260
|
+
{"expiry": "2026-09-26", "strike_type": "moneyness", "strike_value": 1.0},
|
|
261
|
+
{"expiry": "2026-09-26", "strike_type": "moneyness", "strike_value": 1.1},
|
|
262
|
+
{"expiry": "2026-09-26", "strike_type": "delta",
|
|
263
|
+
"strike_value": 0.25, "option_type": "C", "include_analytics": True},
|
|
264
|
+
],
|
|
265
|
+
)
|
|
266
|
+
for item in response.successful:
|
|
267
|
+
print(item.expiry, item.strike, item.vol)
|
|
268
|
+
"""
|
|
269
|
+
body = _drop_none({
|
|
270
|
+
"ccy": ccy,
|
|
271
|
+
"session": session,
|
|
272
|
+
"date": date,
|
|
273
|
+
"queries": queries,
|
|
274
|
+
})
|
|
275
|
+
data = self._request("POST", "/v1/vol-surface/bulk", json=body)
|
|
276
|
+
return data if raw else BulkVolResponse.model_validate(data)
|
|
277
|
+
|
|
278
|
+
def vol_history(
|
|
279
|
+
self,
|
|
280
|
+
ccy: str = "BTC",
|
|
281
|
+
tenor: str = "1M",
|
|
282
|
+
strike_type: Literal["moneyness", "delta"] = "moneyness",
|
|
283
|
+
strike_value: float = 1.0,
|
|
284
|
+
*,
|
|
285
|
+
option_type: OptionType = "C",
|
|
286
|
+
start_date: Optional[str] = None,
|
|
287
|
+
end_date: Optional[str] = None,
|
|
288
|
+
raw: bool = False,
|
|
289
|
+
) -> Union[VolHistoryResponse, Dict[str, Any]]:
|
|
290
|
+
"""Historical time series at a constant maturity and constant strike.
|
|
291
|
+
|
|
292
|
+
Ideal for backtesting, regression analysis, and vol regime monitoring.
|
|
293
|
+
|
|
294
|
+
Requires a PRO or ULTRA plan (BASIC plans get 403 on this endpoint).
|
|
295
|
+
|
|
296
|
+
Parameters
|
|
297
|
+
----------
|
|
298
|
+
ccy:
|
|
299
|
+
Asset symbol.
|
|
300
|
+
tenor:
|
|
301
|
+
Constant-maturity tenor: ``1D``, ``1W``, ``2W``, ``3W``, ``1M``,
|
|
302
|
+
``2M``, ``3M``, ``6M``, ``9M``, or ``1Y``.
|
|
303
|
+
strike_type:
|
|
304
|
+
``"moneyness"`` (K/S) or ``"delta"``. Absolute strikes are not
|
|
305
|
+
supported for history queries.
|
|
306
|
+
strike_value:
|
|
307
|
+
Must be a value on the published grid — see the API docs for the
|
|
308
|
+
full list. Common moneyness points: 0.9, 0.95, 1.0, 1.05, 1.1.
|
|
309
|
+
Common deltas: 0.10, 0.25.
|
|
310
|
+
option_type:
|
|
311
|
+
``"C"`` or ``"P"``. Required when ``strike_type="delta"``.
|
|
312
|
+
start_date / end_date:
|
|
313
|
+
``YYYY-MM-DD`` bounds. PRO plans are limited to the last year.
|
|
314
|
+
raw:
|
|
315
|
+
If True, return the parsed JSON dict instead of a typed model.
|
|
316
|
+
"""
|
|
317
|
+
params = _drop_none({
|
|
318
|
+
"ccy": ccy,
|
|
319
|
+
"tenor": tenor,
|
|
320
|
+
"strike_type": strike_type,
|
|
321
|
+
"strike_value": strike_value,
|
|
322
|
+
"option_type": option_type,
|
|
323
|
+
"start_date": start_date,
|
|
324
|
+
"end_date": end_date,
|
|
325
|
+
})
|
|
326
|
+
data = self._request("GET", "/v1/vol-history", params=params)
|
|
327
|
+
return data if raw else VolHistoryResponse.model_validate(data)
|
|
328
|
+
|
|
329
|
+
# ── HTTP plumbing ────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
def _request(
|
|
332
|
+
self,
|
|
333
|
+
method: str,
|
|
334
|
+
path: str,
|
|
335
|
+
*,
|
|
336
|
+
params: Optional[Dict[str, Any]] = None,
|
|
337
|
+
json: Optional[Dict[str, Any]] = None,
|
|
338
|
+
) -> Any:
|
|
339
|
+
"""Send a request with retries and convert HTTP errors to typed exceptions."""
|
|
340
|
+
last_error: Optional[Exception] = None
|
|
341
|
+
|
|
342
|
+
for attempt in range(self.max_retries + 1):
|
|
343
|
+
try:
|
|
344
|
+
response = self._client.request(
|
|
345
|
+
method, path, params=params, json=json
|
|
346
|
+
)
|
|
347
|
+
except httpx.TimeoutException as e:
|
|
348
|
+
last_error = e
|
|
349
|
+
if attempt < self.max_retries:
|
|
350
|
+
time.sleep(_backoff(attempt))
|
|
351
|
+
continue
|
|
352
|
+
raise CryptoVolTimeoutError(
|
|
353
|
+
f"Request to {path} timed out after {self.timeout}s"
|
|
354
|
+
) from e
|
|
355
|
+
except httpx.HTTPError as e:
|
|
356
|
+
last_error = e
|
|
357
|
+
if attempt < self.max_retries:
|
|
358
|
+
time.sleep(_backoff(attempt))
|
|
359
|
+
continue
|
|
360
|
+
raise CryptoVolError(f"Network error calling {path}: {e}") from e
|
|
361
|
+
|
|
362
|
+
# Retry 5xx
|
|
363
|
+
if 500 <= response.status_code < 600 and attempt < self.max_retries:
|
|
364
|
+
time.sleep(_backoff(attempt))
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
# Retry 429 with backoff
|
|
368
|
+
if response.status_code == 429 and attempt < self.max_retries:
|
|
369
|
+
retry_after = response.headers.get("Retry-After")
|
|
370
|
+
delay = float(retry_after) if retry_after else _backoff(attempt)
|
|
371
|
+
time.sleep(delay)
|
|
372
|
+
continue
|
|
373
|
+
|
|
374
|
+
return _handle_response(response)
|
|
375
|
+
|
|
376
|
+
# Should be unreachable, but just in case
|
|
377
|
+
raise CryptoVolError( # pragma: no cover
|
|
378
|
+
f"Exhausted retries for {path}: {last_error}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _drop_none(d: Dict[str, Any]) -> Dict[str, Any]:
|
|
386
|
+
"""Strip keys whose value is None so we don't send them as query params."""
|
|
387
|
+
return {k: v for k, v in d.items() if v is not None}
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def _backoff(attempt: int) -> float:
|
|
391
|
+
"""Exponential backoff: 0.5s, 1s, 2s, 4s..."""
|
|
392
|
+
return 0.5 * (2 ** attempt)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _handle_response(response: httpx.Response) -> Any:
|
|
396
|
+
"""Raise typed exceptions for error responses; return parsed JSON otherwise."""
|
|
397
|
+
if response.is_success:
|
|
398
|
+
try:
|
|
399
|
+
return response.json()
|
|
400
|
+
except ValueError as e:
|
|
401
|
+
raise CryptoVolError(
|
|
402
|
+
f"Server returned non-JSON response (status {response.status_code})",
|
|
403
|
+
status_code=response.status_code,
|
|
404
|
+
response_body=response.text,
|
|
405
|
+
) from e
|
|
406
|
+
|
|
407
|
+
# Pull a human-readable message out of the body
|
|
408
|
+
try:
|
|
409
|
+
body = response.json()
|
|
410
|
+
message = (
|
|
411
|
+
body.get("detail")
|
|
412
|
+
or body.get("error")
|
|
413
|
+
or body.get("message")
|
|
414
|
+
or str(body)
|
|
415
|
+
)
|
|
416
|
+
except ValueError:
|
|
417
|
+
body = response.text
|
|
418
|
+
message = body or response.reason_phrase
|
|
419
|
+
|
|
420
|
+
status = response.status_code
|
|
421
|
+
kwargs = {"status_code": status, "response_body": body}
|
|
422
|
+
|
|
423
|
+
if status in (401,):
|
|
424
|
+
raise AuthenticationError(message, **kwargs)
|
|
425
|
+
if status == 403:
|
|
426
|
+
# The API uses 403 for two distinct cases:
|
|
427
|
+
# 1. Missing/invalid RapidAPI proxy secret -> AuthenticationError
|
|
428
|
+
# 2. Plan tier limit violation -> PlanLimitError
|
|
429
|
+
# The plan-limit messages all mention "plan" or "Upgrade"; auth ones don't.
|
|
430
|
+
text = str(message).lower()
|
|
431
|
+
if "plan" in text or "upgrade" in text or "tier" in text:
|
|
432
|
+
raise PlanLimitError(message, **kwargs)
|
|
433
|
+
raise AuthenticationError(message, **kwargs)
|
|
434
|
+
if status == 404:
|
|
435
|
+
raise NotFoundError(message, **kwargs)
|
|
436
|
+
if status in (400, 422):
|
|
437
|
+
raise ValidationError(message, **kwargs)
|
|
438
|
+
if status == 429:
|
|
439
|
+
raise RateLimitError(message, **kwargs)
|
|
440
|
+
if 500 <= status < 600:
|
|
441
|
+
raise ServerError(message, **kwargs)
|
|
442
|
+
|
|
443
|
+
raise CryptoVolError(message, **kwargs)
|
cryptovol/exceptions.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Exception hierarchy for the CryptoVol SDK.
|
|
2
|
+
|
|
3
|
+
Catch `CryptoVolError` to handle any API-related failure. Catch a more specific
|
|
4
|
+
subclass when you want to react to a particular condition (e.g. retry on
|
|
5
|
+
rate limits, prompt the user to upgrade on plan-limit errors).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CryptoVolError(Exception):
|
|
13
|
+
"""Base exception for all errors raised by the CryptoVol SDK."""
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
message: str,
|
|
18
|
+
*,
|
|
19
|
+
status_code: Optional[int] = None,
|
|
20
|
+
response_body: Optional[Any] = None,
|
|
21
|
+
) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.message = message
|
|
24
|
+
self.status_code = status_code
|
|
25
|
+
self.response_body = response_body
|
|
26
|
+
|
|
27
|
+
def __str__(self) -> str: # pragma: no cover — trivial
|
|
28
|
+
if self.status_code is not None:
|
|
29
|
+
return f"[{self.status_code}] {self.message}"
|
|
30
|
+
return self.message
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuthenticationError(CryptoVolError):
|
|
34
|
+
"""Raised on 401 / 403 — invalid or missing API key, or non-allowed origin."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class PlanLimitError(CryptoVolError):
|
|
38
|
+
"""Raised on 403 when the request hits a plan-tier limit.
|
|
39
|
+
|
|
40
|
+
Examples: querying ETH on a BASIC plan, requesting Greeks without PRO,
|
|
41
|
+
querying dates older than your plan's history window.
|
|
42
|
+
|
|
43
|
+
The server's error message typically tells you which tier unlocks the feature.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NotFoundError(CryptoVolError):
|
|
48
|
+
"""Raised on 404 — no data available for the requested asset/date/session."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ValidationError(CryptoVolError):
|
|
52
|
+
"""Raised on 400 / 422 — malformed parameters (bad ccy, bad date format, etc.)."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RateLimitError(CryptoVolError):
|
|
56
|
+
"""Raised on 429 — daily/monthly quota exceeded."""
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ServerError(CryptoVolError):
|
|
60
|
+
"""Raised on 5xx — transient server-side failure."""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class TimeoutError(CryptoVolError):
|
|
64
|
+
"""Raised when a request exceeds the configured timeout."""
|
cryptovol/models.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Typed response models for the CryptoVol API.
|
|
2
|
+
|
|
3
|
+
All models are Pydantic v2 ``BaseModel`` subclasses, so they give you:
|
|
4
|
+
|
|
5
|
+
* IDE autocomplete on response fields
|
|
6
|
+
* Runtime validation of types
|
|
7
|
+
* ``.model_dump()`` to round-trip back to plain dicts
|
|
8
|
+
|
|
9
|
+
If you'd rather work with raw dicts, every client method accepts
|
|
10
|
+
``raw=True`` to skip parsing.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import List, Optional
|
|
15
|
+
|
|
16
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ── Shared building blocks ────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Greeks(BaseModel):
|
|
23
|
+
"""Black-Scholes Greeks computed at the resolved (K, T, vol) point."""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(extra="allow")
|
|
26
|
+
|
|
27
|
+
delta: Optional[float] = None
|
|
28
|
+
gamma: Optional[float] = None
|
|
29
|
+
vega: Optional[float] = None
|
|
30
|
+
theta: Optional[float] = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Analytics(BaseModel):
|
|
34
|
+
"""Spot, forward, rate, BS price, and Greeks (PRO and ULTRA only)."""
|
|
35
|
+
|
|
36
|
+
model_config = ConfigDict(extra="allow")
|
|
37
|
+
|
|
38
|
+
spot: Optional[float] = None
|
|
39
|
+
forward: Optional[float] = None
|
|
40
|
+
yield_rate: Optional[float] = None
|
|
41
|
+
price: Optional[float] = None
|
|
42
|
+
greeks: Optional[Greeks] = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ── /v1/vol-index ─────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class VolIndexPoint(BaseModel):
|
|
49
|
+
"""One day of the constant-maturity vol index."""
|
|
50
|
+
|
|
51
|
+
model_config = ConfigDict(extra="allow")
|
|
52
|
+
|
|
53
|
+
date: str
|
|
54
|
+
vol: float
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class VolIndexResponse(BaseModel):
|
|
58
|
+
"""Response from ``GET /v1/vol-index``."""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(extra="allow")
|
|
61
|
+
|
|
62
|
+
ccy: str
|
|
63
|
+
tenor: str
|
|
64
|
+
start_date: Optional[str] = None
|
|
65
|
+
end_date: Optional[str] = None
|
|
66
|
+
count: int
|
|
67
|
+
data: List[VolIndexPoint] = Field(default_factory=list)
|
|
68
|
+
|
|
69
|
+
def to_dataframe(self): # pragma: no cover — optional dep
|
|
70
|
+
"""Convert ``.data`` to a pandas DataFrame indexed by date.
|
|
71
|
+
|
|
72
|
+
Requires pandas to be installed (``pip install cryptovol[pandas]``).
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
import pandas as pd
|
|
76
|
+
except ImportError as e:
|
|
77
|
+
raise ImportError(
|
|
78
|
+
"pandas is required for .to_dataframe(). "
|
|
79
|
+
"Install with: pip install cryptovol[pandas]"
|
|
80
|
+
) from e
|
|
81
|
+
df = pd.DataFrame([p.model_dump() for p in self.data])
|
|
82
|
+
if not df.empty:
|
|
83
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
84
|
+
df = df.set_index("date").sort_index()
|
|
85
|
+
return df
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── /v1/vol-surface (single point) ────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class VolSurfacePoint(BaseModel):
|
|
92
|
+
"""A single SABR-interpolated vol point on the surface."""
|
|
93
|
+
|
|
94
|
+
model_config = ConfigDict(extra="allow")
|
|
95
|
+
|
|
96
|
+
ccy: str
|
|
97
|
+
session: str
|
|
98
|
+
date: str
|
|
99
|
+
expiry: str
|
|
100
|
+
strike_type: str
|
|
101
|
+
strike_value: float
|
|
102
|
+
strike: float
|
|
103
|
+
option_type: Optional[str] = None
|
|
104
|
+
vol: float
|
|
105
|
+
analytics: Optional[Analytics] = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ── /v1/vol-surface/bulk ──────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class BulkResultItem(BaseModel):
|
|
112
|
+
"""One item in a bulk response — either a resolved point or an error."""
|
|
113
|
+
|
|
114
|
+
model_config = ConfigDict(extra="allow")
|
|
115
|
+
|
|
116
|
+
expiry: str
|
|
117
|
+
strike_type: Optional[str] = None
|
|
118
|
+
strike_value: Optional[float] = None
|
|
119
|
+
strike: Optional[float] = None
|
|
120
|
+
option_type: Optional[str] = None
|
|
121
|
+
vol: Optional[float] = None
|
|
122
|
+
analytics: Optional[Analytics] = None
|
|
123
|
+
error: Optional[str] = None
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def ok(self) -> bool:
|
|
127
|
+
"""True if this query resolved successfully (no error field)."""
|
|
128
|
+
return self.error is None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class BulkVolResponse(BaseModel):
|
|
132
|
+
"""Response from ``POST /v1/vol-surface/bulk``."""
|
|
133
|
+
|
|
134
|
+
model_config = ConfigDict(extra="allow")
|
|
135
|
+
|
|
136
|
+
ccy: str
|
|
137
|
+
session: str
|
|
138
|
+
date: str
|
|
139
|
+
spot: Optional[float] = None
|
|
140
|
+
count: int
|
|
141
|
+
results: List[BulkResultItem] = Field(default_factory=list)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def successful(self) -> List[BulkResultItem]:
|
|
145
|
+
"""All bulk results that resolved without error."""
|
|
146
|
+
return [r for r in self.results if r.ok]
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def failed(self) -> List[BulkResultItem]:
|
|
150
|
+
"""All bulk results that came back with an error field."""
|
|
151
|
+
return [r for r in self.results if not r.ok]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ── /v1/vol-history ───────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class VolHistoryPoint(BaseModel):
|
|
158
|
+
"""One day of constant-maturity, constant-strike historical vol."""
|
|
159
|
+
|
|
160
|
+
model_config = ConfigDict(extra="allow")
|
|
161
|
+
|
|
162
|
+
date: str
|
|
163
|
+
vol: float
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class VolHistoryResponse(BaseModel):
|
|
167
|
+
"""Response from ``GET /v1/vol-history``."""
|
|
168
|
+
|
|
169
|
+
model_config = ConfigDict(extra="allow")
|
|
170
|
+
|
|
171
|
+
ccy: str
|
|
172
|
+
tenor: str
|
|
173
|
+
strike_type: str
|
|
174
|
+
strike_value: float
|
|
175
|
+
option_type: Optional[str] = None
|
|
176
|
+
start_date: Optional[str] = None
|
|
177
|
+
end_date: Optional[str] = None
|
|
178
|
+
count: int
|
|
179
|
+
data: List[VolHistoryPoint] = Field(default_factory=list)
|
|
180
|
+
|
|
181
|
+
def to_dataframe(self): # pragma: no cover — optional dep
|
|
182
|
+
"""Convert ``.data`` to a pandas DataFrame indexed by date."""
|
|
183
|
+
try:
|
|
184
|
+
import pandas as pd
|
|
185
|
+
except ImportError as e:
|
|
186
|
+
raise ImportError(
|
|
187
|
+
"pandas is required for .to_dataframe(). "
|
|
188
|
+
"Install with: pip install cryptovol[pandas]"
|
|
189
|
+
) from e
|
|
190
|
+
df = pd.DataFrame([p.model_dump() for p in self.data])
|
|
191
|
+
if not df.empty:
|
|
192
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
193
|
+
df = df.set_index("date").sort_index()
|
|
194
|
+
return df
|
cryptovol/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cryptovol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the CryptoVol API — institutional-grade crypto implied volatility data.
|
|
5
|
+
Project-URL: Homepage, https://www.cryptovol.io
|
|
6
|
+
Project-URL: Documentation, https://www.cryptovol.io/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/FlyCapital/cryptovol-python
|
|
8
|
+
Project-URL: Issues, https://github.com/FlyCapital/cryptovol-python/issues
|
|
9
|
+
Project-URL: Get an API key, https://www.cryptovol.io/api
|
|
10
|
+
Author-email: CryptoVol <support@cryptovol.io>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: bitcoin,crypto,cryptovol,ethereum,implied-volatility,options,quant,sabr,trading,volatility
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Requires-Dist: httpx<1.0,>=0.24
|
|
29
|
+
Requires-Dist: pydantic<3.0,>=2.0
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest-httpx>=0.26; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
|
|
38
|
+
Requires-Dist: mkdocs>=1.5; extra == 'docs'
|
|
39
|
+
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
40
|
+
Provides-Extra: pandas
|
|
41
|
+
Requires-Dist: pandas>=1.5; extra == 'pandas'
|
|
42
|
+
Description-Content-Type: text/markdown
|
|
43
|
+
|
|
44
|
+
# cryptovol-python
|
|
45
|
+
|
|
46
|
+
[](https://pypi.org/project/cryptovol/)
|
|
47
|
+
[](https://pypi.org/project/cryptovol/)
|
|
48
|
+
[](https://github.com/FlyCapital/cryptovol-python/actions/workflows/ci.yml)
|
|
49
|
+
[](https://opensource.org/licenses/MIT)
|
|
50
|
+
[](https://www.cryptovol.io/docs)
|
|
51
|
+
|
|
52
|
+
The official Python SDK for the **[CryptoVol API](https://www.cryptovol.io)** — institutional-grade implied volatility data for crypto options.
|
|
53
|
+
|
|
54
|
+
> SABR-calibrated surfaces · 3 sessions/day (Asia, London, US) · BTC, ETH, SOL, XRP, AVAX, TRX · Constant-maturity history for backtesting
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install cryptovol
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Optional pandas integration (for `.to_dataframe()`):
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install "cryptovol[pandas]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Get an API key
|
|
71
|
+
|
|
72
|
+
Sign up at **[cryptovol.io/api](https://www.cryptovol.io/api)** — BASIC, PRO, and ULTRA tiers are available.
|
|
73
|
+
|
|
74
|
+
## Quick start
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from cryptovol import CryptoVol
|
|
78
|
+
|
|
79
|
+
cv = CryptoVol(api_key="YOUR_RAPIDAPI_KEY")
|
|
80
|
+
|
|
81
|
+
# Daily BTC 30-day implied vol index
|
|
82
|
+
idx = cv.vol_index(ccy="BTC", tenor="30D")
|
|
83
|
+
print(f"Latest BTC 30D IV: {idx.data[-1].vol}%")
|
|
84
|
+
|
|
85
|
+
# ATM vol for a specific expiry, with Greeks
|
|
86
|
+
pt = cv.vol_surface(
|
|
87
|
+
ccy="BTC",
|
|
88
|
+
expiry="2026-12-26",
|
|
89
|
+
strike_type="moneyness",
|
|
90
|
+
strike_value=1.0,
|
|
91
|
+
include_analytics=True,
|
|
92
|
+
)
|
|
93
|
+
print(f"BTC ATM Dec 26: {pt.vol}% delta={pt.analytics.greeks.delta:.3f}")
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
That's it. The client is typed end-to-end, so your IDE autocompletes every field.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## What you can query
|
|
101
|
+
|
|
102
|
+
| Method | Endpoint | What it returns |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `cv.vol_index(...)` | `GET /v1/vol-index` | Daily IV index time series (CryptoVIX-style) |
|
|
105
|
+
| `cv.vol_surface(...)` | `GET /v1/vol-surface` | One SABR-interpolated vol point |
|
|
106
|
+
| `cv.vol_surface_bulk(...)` | `POST /v1/vol-surface/bulk` | Many points in one round-trip — efficient |
|
|
107
|
+
| `cv.vol_history(...)` | `GET /v1/vol-history` | Constant-maturity historical IV for backtests |
|
|
108
|
+
|
|
109
|
+
### Strike conventions
|
|
110
|
+
|
|
111
|
+
`vol_surface` and `vol_surface_bulk` accept three strike types:
|
|
112
|
+
|
|
113
|
+
- **`"moneyness"`** — `strike_value` is the K/S ratio. `1.0` = ATM, `0.9` = 10% OTM put strike.
|
|
114
|
+
- **`"delta"`** — `strike_value` is the BS delta magnitude. `0.25` with `option_type="C"` gives the 25-delta call strike.
|
|
115
|
+
- **`"strike"`** — `strike_value` is the absolute strike (e.g. `100_000`).
|
|
116
|
+
|
|
117
|
+
For `"delta"`, the SDK solves K via SABR-interpolated Newton iteration on the surface — same calibration the API serves.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Common recipes
|
|
122
|
+
|
|
123
|
+
### Plot the BTC 30-day vol index
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import matplotlib.pyplot as plt
|
|
127
|
+
from cryptovol import CryptoVol
|
|
128
|
+
|
|
129
|
+
cv = CryptoVol(api_key="...")
|
|
130
|
+
df = cv.vol_index(ccy="BTC", tenor="30D",
|
|
131
|
+
start_date="2025-01-01", end_date="2026-05-01").to_dataframe()
|
|
132
|
+
|
|
133
|
+
df["vol"].plot(title="BTC 30D Implied Vol")
|
|
134
|
+
plt.ylabel("IV (%)"); plt.show()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Reconstruct a vol smile for one expiry
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
expiry = "2026-09-26"
|
|
141
|
+
moneyness_grid = [0.7, 0.8, 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.3]
|
|
142
|
+
|
|
143
|
+
resp = cv.vol_surface_bulk(
|
|
144
|
+
ccy="BTC",
|
|
145
|
+
queries=[
|
|
146
|
+
{"expiry": expiry, "strike_type": "moneyness", "strike_value": m}
|
|
147
|
+
for m in moneyness_grid
|
|
148
|
+
],
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for pt in resp.successful:
|
|
152
|
+
print(f"{pt.strike_value:.2f}x K={pt.strike:>10,.0f} vol={pt.vol:.2f}%")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Build a 25-delta risk-reversal time series
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import pandas as pd
|
|
159
|
+
|
|
160
|
+
call = cv.vol_history(ccy="BTC", tenor="1M",
|
|
161
|
+
strike_type="delta", strike_value=0.25, option_type="C").to_dataframe()
|
|
162
|
+
put = cv.vol_history(ccy="BTC", tenor="1M",
|
|
163
|
+
strike_type="delta", strike_value=0.25, option_type="P").to_dataframe()
|
|
164
|
+
|
|
165
|
+
rr = call["vol"] - put["vol"]
|
|
166
|
+
rr.plot(title="BTC 1M 25Δ Risk-Reversal")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Greeks on a 25-delta call
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
pt = cv.vol_surface(
|
|
173
|
+
ccy="BTC", expiry="2026-12-26",
|
|
174
|
+
strike_type="delta", strike_value=0.25, option_type="C",
|
|
175
|
+
include_analytics=True,
|
|
176
|
+
)
|
|
177
|
+
g = pt.analytics.greeks
|
|
178
|
+
print(f"K={pt.strike:.0f} vol={pt.vol:.2f}% Δ={g.delta:.3f} Γ={g.gamma:.6f} V={g.vega:.2f} Θ={g.theta:.2f}")
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Error handling
|
|
184
|
+
|
|
185
|
+
Every API error becomes a typed exception you can catch precisely:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
from cryptovol import CryptoVol, PlanLimitError, ValidationError, RateLimitError
|
|
189
|
+
|
|
190
|
+
cv = CryptoVol(api_key="...")
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
cv.vol_history(ccy="ETH", tenor="1M",
|
|
194
|
+
strike_type="moneyness", strike_value=1.0)
|
|
195
|
+
except PlanLimitError as e:
|
|
196
|
+
print(f"Plan limit hit — upgrade needed: {e}")
|
|
197
|
+
except ValidationError as e:
|
|
198
|
+
print(f"Bad params: {e}")
|
|
199
|
+
except RateLimitError as e:
|
|
200
|
+
print(f"Slow down: {e}")
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Hierarchy:
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
CryptoVolError # base — catch this to handle anything
|
|
207
|
+
├── AuthenticationError # 401, or 403 without "plan" in the body
|
|
208
|
+
├── PlanLimitError # 403 — tier blocks asset/session/history/Greeks
|
|
209
|
+
├── NotFoundError # 404 — no data for that date/session
|
|
210
|
+
├── ValidationError # 400 / 422 — malformed params
|
|
211
|
+
├── RateLimitError # 429 — quota exceeded
|
|
212
|
+
├── ServerError # 5xx
|
|
213
|
+
└── TimeoutError # request exceeded `timeout`
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## Configuration
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from cryptovol import CryptoVol
|
|
222
|
+
|
|
223
|
+
cv = CryptoVol(
|
|
224
|
+
api_key="...",
|
|
225
|
+
timeout=30.0, # per-request timeout (seconds)
|
|
226
|
+
max_retries=3, # retries 5xx and 429 with exponential backoff
|
|
227
|
+
user_agent="my-app/1.2.3",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Or as a context manager — closes the HTTP client on exit
|
|
231
|
+
with CryptoVol(api_key="...") as cv:
|
|
232
|
+
idx = cv.vol_index()
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Raw JSON
|
|
236
|
+
|
|
237
|
+
Every method accepts `raw=True` if you'd rather skip the typed models and work with plain dicts:
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
data = cv.vol_index(ccy="BTC", tenor="30D", raw=True)
|
|
241
|
+
# data is just the parsed JSON
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Plan tiers (at a glance)
|
|
247
|
+
|
|
248
|
+
| | BASIC | PRO | ULTRA |
|
|
249
|
+
|---|---|---|---|
|
|
250
|
+
| Assets | BTC | + ETH, SOL | + XRP, AVAX, TRX |
|
|
251
|
+
| Sessions | US | US | Asia, London, US |
|
|
252
|
+
| History window | 30 days | 1 year | Full archive |
|
|
253
|
+
| `vol_history` endpoint | — | ✓ | ✓ |
|
|
254
|
+
| Greeks / analytics | — | ✓ | ✓ |
|
|
255
|
+
| Quota | 500/month | 70k/day | 100k/day |
|
|
256
|
+
|
|
257
|
+
Hitting a limit raises `PlanLimitError` — the message tells you which tier unlocks it. Full details at **[cryptovol.io/api](https://www.cryptovol.io/api)**.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Development
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
git clone https://github.com/FlyCapital/cryptovol-python.git
|
|
265
|
+
cd cryptovol-python
|
|
266
|
+
pip install -e ".[dev]"
|
|
267
|
+
pytest
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Links
|
|
273
|
+
|
|
274
|
+
- **API docs:** https://www.cryptovol.io/docs
|
|
275
|
+
- **Methodology:** https://www.cryptovol.io/methodology
|
|
276
|
+
- **Research / blog:** https://www.cryptovol.io/blog
|
|
277
|
+
- **Get an API key:** https://www.cryptovol.io/api
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
MIT — see [LICENSE](./LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
cryptovol/__init__.py,sha256=LqRvvmaf3bZFSLqorZJmPp00Vx0pB4AJjmfik3FeOcM,1211
|
|
2
|
+
cryptovol/client.py,sha256=P7br4NcRQeJTFRosGe861A5wWI4oXcslFAAx-iJzsag,15803
|
|
3
|
+
cryptovol/exceptions.py,sha256=y9yZOyHtj6rKFgTqxJargNWzxIiVYNflKD72S__vM8k,1953
|
|
4
|
+
cryptovol/models.py,sha256=a7oJJA9PiSw8SM6lRe3LhusJ_bynRgHOaJDgKY9PJEM,5978
|
|
5
|
+
cryptovol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
cryptovol-0.1.0.dist-info/METADATA,sha256=f4gwlQ1sKNy4H9oI9tv1QMNeXY-17YcuJCs_ad4UvlA,8845
|
|
7
|
+
cryptovol-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
cryptovol-0.1.0.dist-info/licenses/LICENSE,sha256=FMzcu41qarAz1usGqXROQXOillPMTqyoIOcECW94RMg,1066
|
|
9
|
+
cryptovol-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CryptoVol
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|