{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# What is the volatility curve saying?\n", "Compare delayed VIX futures, the latest published VIX close and realised equity volatility with explicit observation dates.\n", "Delayed Cboe futures snapshot captured 2026-10-31. Reference lines show the published VIX close dated 2026-10-29 and 21-session SPY realised volatility through 2026-10-30. Maturities and timestamps differ; these are not interchangeable prices." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Compare delayed VIX futures, the latest published VIX close and realised equity volatility with explicit observation dates.\n", "\n", "Research example. Live observations can change.\n", "\"\"\"\n", "\n", "from datetime import UTC, datetime\n", "from decimal import Decimal\n", "from textwrap import fill\n", "\n", "import matplotlib.pyplot as plt\n", "import numpy as np\n", "import pandas as pd\n", "\n", "AS_OF = datetime.now(UTC).date()\n", "NAVY, BLUE, PINK = \"#132244\", \"#8fb9ff\", \"#e7a2cc\"\n", "\n", "\n", "def fetch(qj):\n", " return {\n", " \"vix_curve\": qj.domains.call(\"derivatives.vol.get_vix_term_structure\", provider=\"cboe\"),\n", " \"vix_current\": qj.domains.call(\n", " \"macro.indicators.get_fred_data_series_by_id\",\n", " provider=\"fred\",\n", " search_id=\"VIXCLS\",\n", " start=\"2026-01-01\",\n", " end=\"2026-10-30\",\n", " ),\n", " \"recent_prices\": qj.domains.call(\n", " \"equity.pricing.get_historical_prices\",\n", " provider=\"eod\",\n", " symbols=[\"SPY\"],\n", " start_date=\"2026-01-01\",\n", " end_date=\"2026-10-30\",\n", " frequency=\"1d\",\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " curve = frame(raw[\"vix_curve\"], \"expiration_date\")\n", " curve = curve.loc[curve[\"expiration_date\"].dt.date >= AS_OF].copy()\n", " curve[\"Settlement\"] = curve[\"settlement\"].map(decimal)\n", " vix = macro(raw[\"vix_current\"], \"VIXCLS\")\n", " returns = change(history(raw[\"recent_prices\"], \"SPY\")[\"price\"]).dropna()\n", " if curve.empty or len(returns) < 21:\n", " raise ValueError(\"Need usable futures and at least 21 realised-return observations\")\n", " realised = 100 * returns.tail(21).std(ddof=1) * np.sqrt(252)\n", " fig, (ax,) = canvas(\"Volatility \u00b7 spot, futures and realised\", \"Volatility points / annualised %\")\n", " ax.plot(curve[\"expiration_date\"], curve[\"Settlement\"], marker=\"o\", label=\"Delayed VX futures settlement\")\n", " ax.axhline(vix.iloc[-1], color=PINK, linestyle=\"--\", label=\"VIX close \u00b7 \" + vix.index[-1].strftime(\"%Y-%m-%d\"))\n", " ax.axhline(realised, color=\"#ba9dea\", linestyle=\":\", label=\"SPY 21-session realised volatility\")\n", " ax.set_xlabel(\"Futures expiration\")\n", " legend(ax)\n", " curve[\"VIX close\"] = vix.iloc[-1]\n", " curve[\"SPY realised %\"] = realised\n", " return curve[[\"expiration_date\", \"Settlement\", \"VIX close\", \"SPY realised %\"]], fig\n", "\n", "\n", "def payload(response):\n", " if hasattr(response, \"to_dict\"):\n", " response = response.to_dict()\n", " while isinstance(response, dict):\n", " if response.get(\"error\") or response.get(\"error_code\") or response.get(\"success\") is False:\n", " raise ValueError(\"Provider request failed\")\n", " if \"data\" in response:\n", " response = response[\"data\"]\n", " elif \"value\" in response:\n", " response = response[\"value\"]\n", " else:\n", " break\n", " return response\n", "\n", "\n", "def frame(response, date=\"date\"):\n", " rows = payload(response)\n", " if not isinstance(rows, list) or not rows:\n", " raise ValueError(\"No provider observations returned\")\n", " result = pd.DataFrame(rows)\n", " if date:\n", " result[date] = pd.to_datetime(result[date], utc=True)\n", " result = result.sort_values(date)\n", " return result\n", "\n", "\n", "def decimal(value):\n", " if pd.isna(value):\n", " raise ValueError(\"Required monetary value or quantity is missing\")\n", " return Decimal(str(value))\n", "\n", "\n", "def ratio(numerator, denominator):\n", " denominator = decimal(denominator)\n", " if denominator == 0:\n", " raise ValueError(\"Required denominator is zero\")\n", " return float(decimal(numerator) / denominator)\n", "\n", "\n", "def change(values, periods=1):\n", " previous = values.shift(periods)\n", " return pd.Series(\n", " [ratio(x, y) - 1 if pd.notna(x) and pd.notna(y) else np.nan for x, y in zip(values, previous, strict=True)],\n", " index=values.index,\n", " )\n", "\n", "\n", "def history(response, symbol, adjusted=True):\n", " values = payload(response)\n", " if not isinstance(values, dict) or symbol not in values:\n", " raise ValueError(f\"No price coverage for {symbol}\")\n", " result = frame(values[symbol]).set_index(\"date\")\n", " field = \"adjusted_close\" if adjusted else \"close\"\n", " result[\"price\"] = result[field].map(decimal)\n", " result[\"volume\"] = result[\"volume\"].map(decimal)\n", " if result.index.has_duplicates:\n", " raise ValueError(\"Duplicate trading dates\")\n", " return result\n", "\n", "\n", "def macro(response, column):\n", " values = frame(response).set_index(\"date\")[column]\n", " values = pd.to_numeric(values, errors=\"coerce\").dropna()\n", " if values.empty:\n", " raise ValueError(f\"No numeric observations for {column}\")\n", " return values\n", "\n", "\n", "def canvas(title, ylabel, panels=1):\n", " plt.rcParams.update(\n", " {\n", " \"figure.facecolor\": NAVY,\n", " \"axes.facecolor\": NAVY,\n", " \"savefig.facecolor\": NAVY,\n", " \"text.color\": \"#e9eef8\",\n", " \"axes.labelcolor\": \"#afc1dc\",\n", " \"xtick.color\": \"#afc1dc\",\n", " \"ytick.color\": \"#afc1dc\",\n", " \"axes.edgecolor\": \"#34486b\",\n", " \"grid.color\": \"#34486b\",\n", " \"font.size\": 10,\n", " \"axes.prop_cycle\": plt.cycler(color=[BLUE, PINK, \"#ba9dea\"]),\n", " }\n", " )\n", " fig, axes = plt.subplots(panels, 1, figsize=(9, 6.1), squeeze=False, layout=\"constrained\")\n", " fig.suptitle(fill(title, width=58), fontsize=17, x=0.04, ha=\"left\", weight=\"medium\")\n", " for ax in axes.flat:\n", " ax.grid(axis=\"y\", alpha=0.45)\n", " ax.set_axisbelow(True)\n", " ax.spines[[\"top\", \"right\"]].set_visible(False)\n", " axes.flat[0].set_ylabel(ylabel)\n", " return fig, list(axes.flat)\n", "\n", "\n", "def legend(ax):\n", " ax.legend(frameon=False, labelcolor=\"#e9eef8\", fontsize=9)\n", "\n", "\n", "def run():\n", " from quantjourney.sdk import QuantJourneyAPI\n", "\n", " qj = QuantJourneyAPI.from_env()\n", " raw = fetch(qj)\n", " table, fig = analyze(raw)\n", " print(table.to_string(index=False))\n", " fig.savefig(OUTPUT, dpi=180)\n", " plt.show()\n", "\n", "\n", "OUTPUT = \"sdk_vix_curve.png\"\n", "\n", "if __name__ == \"__main__\":\n", " run()\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python" } }, "nbformat": 4, "nbformat_minor": 5 }