"""Download Deribit's BTC implied-volatility index (DVOL), hourly, full history. DVOL is the options market's own 30-day expectation of BTC volatility, in annualised percent, published since March 2021. It is information the price series does not contain directly: what option traders are paying for future movement. Public endpoint, no key needed. Rows are hourly OHLC of the index keyed by the hour's open time (UTC), the same convention as the Binance candles, so the close of row t is known when candle t closes and may be used for the forecast issued then. Writes outputs/volatility/btc_dvol_1h.csv. Usage: python scripts/fetch_dvol.py """ from __future__ import annotations import json import time import urllib.request from pathlib import Path import pandas as pd ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "outputs" / "volatility" / "btc_dvol_1h.csv" URL = ("https://www.deribit.com/api/v2/public/get_volatility_index_data" "?currency=BTC&start_timestamp={start}&end_timestamp={end}&resolution=3600") START_MS = 1609459200000 # 2021-01-01; the index begins in March 2021 def fetch() -> pd.DataFrame: end, rows = int(time.time() * 1000), [] while end > START_MS: with urllib.request.urlopen(URL.format(start=START_MS, end=end), timeout=30) as resp: result = json.load(resp)["result"] batch = result.get("data") or [] if not batch: break rows += batch nxt = result.get("continuation") if not nxt or nxt >= end: break end = nxt time.sleep(0.2) # be polite to a public endpoint df = pd.DataFrame(rows, columns=["ts", "open", "high", "low", "close"]) df["timestamp"] = pd.to_datetime(df["ts"], unit="ms", utc=True) return (df.drop(columns="ts").drop_duplicates("timestamp") .sort_values("timestamp").reset_index(drop=True)) def main() -> None: df = fetch() OUT.parent.mkdir(parents=True, exist_ok=True) df.to_csv(OUT, index=False) full = pd.date_range(df["timestamp"].min(), df["timestamp"].max(), freq="1h") print(f"{len(df):,} hours, {df['timestamp'].min()} -> {df['timestamp'].max()}; " f"{len(full) - len(df):,} missing hours; DVOL range {df['close'].min():.1f}-{df['close'].max():.1f}") print(f"written {OUT.relative_to(ROOT)}") if __name__ == "__main__": main()