{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Where is the option market pricing the skew?\n", "Find a live expiration, filter actual option observations and compare call and put implied volatility against a daily-price reference.\n", "Observed SPY chain, expiry 2026-11-02; EOD reference close dated 2026-10-30. Zero IV and invalid bid/ask observations are excluded. The daily close is not a synchronized option quote." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Find a live expiration, filter actual option observations and compare call and put implied volatility against a daily-price reference.\n", "\n", "Research example. Live observations can change.\n", "\"\"\"\n", "\n", "from datetime import UTC, datetime, timedelta\n", "from decimal import Decimal\n", "from textwrap import fill\n", "\n", "import matplotlib.pyplot as plt\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", " available = payload(qj.cboe.get_options_expirations(symbol=\"SPY\"))\n", " if isinstance(available, dict):\n", " available = available[\"expirations\"]\n", " dates = sorted((str(item)[:10] for item in available if str(item)[:10] >= AS_OF.isoformat()))\n", " if not dates:\n", " raise ValueError(\"No nonexpired SPY option expiration returned\")\n", " return {\n", " \"chain\": qj.cboe.get_options_chain(symbol=\"SPY\", expiration_date=dates[0]),\n", " \"spy_spot\": qj.eod.get_historical_prices(\n", " symbols=[\"SPY\"],\n", " start_date=(AS_OF - timedelta(days=14)).isoformat(),\n", " end_date=(AS_OF - timedelta(days=1)).isoformat(),\n", " frequency=\"1d\",\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " options = frame(payload(raw[\"chain\"])[\"options\"], date=None)\n", " spot = history(raw[\"spy_spot\"], \"SPY\", adjusted=False)[\"price\"]\n", " reference = spot.iloc[-1]\n", " rows = []\n", " for option in options.to_dict(\"records\"):\n", " if any(pd.isna(option.get(key)) for key in [\"iv\", \"strike\", \"bid\", \"ask\"]):\n", " continue\n", " strike_ratio = ratio(option[\"strike\"], reference)\n", " iv = float(option[\"iv\"])\n", " bid, ask = decimal(option[\"bid\"]), decimal(option[\"ask\"])\n", " if iv <= 0 or bid <= 0 or ask < bid or not 0.85 <= strike_ratio <= 1.15:\n", " continue\n", " rows.append(\n", " {\n", " \"Type\": option[\"option_type\"],\n", " \"Strike / reference\": strike_ratio,\n", " \"IV %\": 100 * iv,\n", " \"Expiry\": option[\"expiry\"],\n", " }\n", " )\n", " result = pd.DataFrame(rows)\n", " if result.empty:\n", " raise ValueError(\"No usable observed option IV coverage\")\n", " fig, (ax,) = canvas(\"SPY \u00b7 observed volatility smile\", \"Provider implied volatility %\")\n", " for kind in [\"call\", \"put\"]:\n", " points = result.loc[result[\"Type\"] == kind].sort_values(\"Strike / reference\")\n", " ax.scatter(points[\"Strike / reference\"], points[\"IV %\"], s=24, label=kind.title(), alpha=0.85)\n", " ax.axvline(1, color=\"#afc1dc\", lw=0.8, linestyle=\"--\")\n", " ax.set_xlabel(\"Strike / last EOD daily close \u00b7 \" + spot.index[-1].strftime(\"%Y-%m-%d\"))\n", " legend(ax)\n", " return result, 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 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 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_smile.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 }