{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-notice",
   "metadata": {},
   "source": [
    "*OptionSpace example notebook. Copyright (c) 2026 Options Universe Inc. (d/b/a OptionSpace). All rights reserved.*\n",
    "\n",
    "*This notebook is part of the OptionSpace documentation. You may run it, and copy and adapt its code into your own programs, only for use with OptionSpace data under your account and the [OptionSpace Terms of Service](https://optionspace.ai/terms). Please do not publish or redistribute this notebook, or use its code with data that does not come from OptionSpace without our written permission. It needs an OptionSpace account and API key. Nothing in it is investment advice.*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-00",
   "metadata": {},
   "source": [
    "# Intraday ATM volatility history\n",
    "\n",
    "How at-the-money volatility moved through the day: first one expiry through one\n",
    "session, then three weeks of the 30-day constant-maturity value at every\n",
    "snapshot your plan allows (every minute on Pro). The example is SPY from 24\n",
    "March to 11 April 2025, which includes the April 2025 tariff selloff.\n",
    "\n",
    "Install and log in as in notebook 1."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-01",
   "metadata": {},
   "source": [
    "**Usage.** Your daily allowance is counted in credits. `client.prices()` lists\n",
    "what each call costs, a research call says what it cost in `result['credits']`,\n",
    "and `client.entitlements()` shows what you have used today."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-02",
   "metadata": {},
   "source": [
    "## 1. Connect\n",
    "\n",
    "`osp.Client()` uses the key you saved with `uv run optionspace login`. If\n",
    "you prefer an environment variable, set `OPTIONSPACE_API_KEY` in the terminal\n",
    "you start Jupyter from. **Never paste your key into a notebook cell**: notebooks\n",
    "get shared, and a key in a cell travels with them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-03",
   "metadata": {},
   "outputs": [],
   "source": [
    "import optionspace as osp\n",
    "\n",
    "client = osp.Client()\n",
    "print(\"optionspace\", osp.__version__, \"| key from:\", client.credential_source)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-04",
   "metadata": {},
   "outputs": [],
   "source": [
    "SYMBOL = \"SPY\"\n",
    "START, END = \"2025-03-24\", \"2025-04-11\"\n",
    "DAY, EXPIRY = \"2025-04-09\", \"2025-04-17\"\n",
    "\n",
    "GRAIN = client.snapshots(SYMBOL, DAY).attrs.get(\"granularity\")\n",
    "STEP = {\"1m\": 1, \"5m\": 5, \"30m\": 30}.get(GRAIN)\n",
    "print(\"Your plan's finest grain:\", GRAIN)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-05",
   "metadata": {},
   "source": [
    "## 2. One expiry through one session\n",
    "\n",
    "`atm_intraday()` returns the ATM volatility of one listed expiry at every\n",
    "snapshot of a session. 9 April 2025 is the day the tariff pause was announced\n",
    "in the early afternoon."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-06",
   "metadata": {},
   "outputs": [],
   "source": [
    "from optionspace import charts\n",
    "\n",
    "atm = client.atm_intraday(SYMBOL, DAY, expiration=EXPIRY)\n",
    "print(len(atm), \"snapshots\")\n",
    "charts.atm_chart(atm).show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-07",
   "metadata": {},
   "source": [
    "## 3. Three weeks at every snapshot\n",
    "\n",
    "`tenor_history()` with no `snapshot` returns every snapshot your plan allows,\n",
    "for one constant maturity. One request covers the whole range; a range too\n",
    "large for your grain is refused with the estimated row count, never cut short."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-08",
   "metadata": {},
   "outputs": [],
   "source": [
    "hist = client.tenor_history(SYMBOL, 30, START, END, fields=[\"atf_iv\", \"skew_25d\"])\n",
    "print(f\"{len(hist):,} rows over {hist['data_date'].nunique()} sessions\")\n",
    "hist.groupby(\"data_date\").size().rename(\"snapshots\").to_frame().T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-09",
   "metadata": {},
   "source": [
    "## 4. Chart it on a trading-time axis\n",
    "\n",
    "Nights and weekends are removed so each session sits next to the last. Where a\n",
    "snapshot is missing, the line breaks instead of drawing across the hole."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-10",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import plotly.graph_objects as go\n",
    "\n",
    "\n",
    "def trading_axis(df, column, step):\n",
    "    sessions = sorted(df[\"data_date\"].unique())\n",
    "    xs, ys = [], []\n",
    "    for i, session in enumerate(sessions):\n",
    "        rows = df[df[\"data_date\"] == session].sort_values(\"snapshot_time\")\n",
    "        previous = None\n",
    "        for t, value in zip(rows[\"snapshot_time\"], rows[column]):\n",
    "            minute = int(t[:2]) * 60 + int(t[3:]) - 9 * 60 - 30\n",
    "            if previous is not None and step and minute - previous > step:\n",
    "                xs.append(None)\n",
    "                ys.append(None)\n",
    "            xs.append(i * 400 + minute)\n",
    "            ys.append(None if pd.isna(value) else value * 100)\n",
    "            previous = minute\n",
    "        xs.append(None)\n",
    "        ys.append(None)\n",
    "    return xs, ys, sessions\n",
    "\n",
    "\n",
    "x, y, sessions = trading_axis(hist, \"atf_iv\", STEP)\n",
    "fig = go.Figure(go.Scatter(x=x, y=y, mode=\"lines\", line=dict(width=1), name=\"30-day ATM vol\"))\n",
    "fig.update_layout(title=f\"{SYMBOL} 30-day ATM volatility, every {GRAIN} snapshot\",\n",
    "                  yaxis_title=\"Volatility (%)\", template=\"simple_white\", height=420)\n",
    "fig.update_xaxes(tickvals=[i * 400 for i in range(len(sessions))][::3], ticktext=sessions[::3])\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-11",
   "metadata": {},
   "outputs": [],
   "source": [
    "x, y, sessions = trading_axis(hist, \"skew_25d\", STEP)\n",
    "fig = go.Figure(go.Scatter(x=x, y=y, mode=\"lines\", line=dict(width=1), name=\"25-delta skew\"))\n",
    "fig.update_layout(title=f\"{SYMBOL} 30-day 25-delta skew (put minus call, vol points)\",\n",
    "                  yaxis_title=\"Vol points\", template=\"simple_white\", height=420)\n",
    "fig.update_xaxes(tickvals=[i * 400 for i in range(len(sessions))][::3], ticktext=sessions[::3])\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-12",
   "metadata": {},
   "source": [
    "## 5. Compare with the closing values\n",
    "\n",
    "`snapshot=\"EOD\"` gives one value per session, the last snapshot at or before\n",
    "the close. `charts.history_chart` draws it with gaps left as gaps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-13",
   "metadata": {},
   "outputs": [],
   "source": [
    "closes = client.tenor_history(SYMBOL, 30, START, END, snapshot=\"EOD\", fields=[\"atf_iv\", \"skew_25d\"])\n",
    "charts.history_chart([closes]).show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03-intraday-atm-history-14",
   "metadata": {},
   "source": [
    "## 6. The busiest minutes\n",
    "\n",
    "The ten largest one-snapshot moves in 30-day ATM volatility over the window, in\n",
    "vol points."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "03-intraday-atm-history-15",
   "metadata": {},
   "outputs": [],
   "source": [
    "moves = hist.sort_values([\"data_date\", \"snapshot_time\"]).copy()\n",
    "moves[\"change\"] = moves.groupby(\"data_date\")[\"atf_iv\"].diff() * 100\n",
    "moves.reindex(moves[\"change\"].abs().sort_values(ascending=False).index)[\n",
    "    [\"data_date\", \"snapshot_time\", \"atf_iv\", \"change\"]].head(10)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "optionspace": {
   "requests": "About 4 requests",
   "summary": "ATM volatility through one session for one expiry, then three weeks of 30-day ATM volatility and skew at every snapshot your plan allows, charted without bridging gaps.",
   "title": "Intraday ATM volatility history"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
