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 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)
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/cryptovol.svg)](https://pypi.org/project/cryptovol/)
47
+ [![Python versions](https://img.shields.io/pypi/pyversions/cryptovol.svg)](https://pypi.org/project/cryptovol/)
48
+ [![CI](https://github.com/FlyCapital/cryptovol-python/actions/workflows/ci.yml/badge.svg)](https://github.com/FlyCapital/cryptovol-python/actions/workflows/ci.yml)
49
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
50
+ [![Docs](https://img.shields.io/badge/docs-cryptovol.io-blue)](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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.