{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-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": "02-chains-and-surfaces-00",
   "metadata": {},
   "source": [
    "# Chains and surfaces on a past date\n",
    "\n",
    "Everything listed for one stock on one past session: which snapshots and\n",
    "expiries exist, the full option chain, a volatility grid by strike and expiry,\n",
    "the standard constant-maturity tenors, and the ATM term structure.\n",
    "\n",
    "Install and log in as in notebook 1. The example is AAPL on 10 April 2025; change\n",
    "the inputs below to any single-stock symbol and past session your plan covers."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-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": "02-chains-and-surfaces-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": "02-chains-and-surfaces-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": "02-chains-and-surfaces-04",
   "metadata": {},
   "outputs": [],
   "source": [
    "SYMBOL, DATE = \"AAPL\", \"2025-04-10\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-05",
   "metadata": {},
   "source": [
    "## 2. What exists that day\n",
    "\n",
    "`snapshots()` lists the times your plan can retrieve for the session, and which\n",
    "one `\"EOD\"` resolves to. `expirations()` lists every expiry trading that day,\n",
    "with its calendar days to expiry."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-06",
   "metadata": {},
   "outputs": [],
   "source": [
    "snaps = client.snapshots(SYMBOL, DATE)\n",
    "print(len(snaps), \"snapshots from\", snaps[\"snapshot_time\"].iloc[0], \"to\", snaps[\"snapshot_time\"].iloc[-1],\n",
    "      \"| EOD =\", snaps.attrs.get(\"eod\"), \"| your granularity:\", snaps.attrs.get(\"granularity\"))\n",
    "\n",
    "expiries = client.expirations(SYMBOL, DATE)\n",
    "print(len(expiries), \"listed expiries\")\n",
    "expiries"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-07",
   "metadata": {},
   "source": [
    "## 3. The full chain at the close\n",
    "\n",
    "One request returns every fitted option across all expiries at one snapshot.\n",
    "A chain too large for one response raises `ResponseTooLarge` instead of being\n",
    "cut short; narrow it with `expiration=`, `strike_min=`/`strike_max=` or\n",
    "`put_call=`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-08",
   "metadata": {},
   "outputs": [],
   "source": [
    "chain = client.chain(SYMBOL, DATE, \"EOD\")\n",
    "print(f\"{len(chain):,} options at {chain.attrs.get('snapshot_time')} ET\")\n",
    "chain.groupby(\"expiration\").size().rename(\"options\").to_frame().T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-09",
   "metadata": {},
   "source": [
    "## 4. A volatility grid: strikes by expiry\n",
    "\n",
    "Out-of-the-money options only (puts below the forward, calls above), fitted\n",
    "volatility in percent, for strikes within 15% of the forward."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-10",
   "metadata": {},
   "outputs": [],
   "source": [
    "otm = chain[((chain[\"put_call\"] == \"put\") & (chain[\"strike\"] < chain[\"forward\"]))\n",
    "            | ((chain[\"put_call\"] == \"call\") & (chain[\"strike\"] >= chain[\"forward\"]))]\n",
    "near = otm[(otm[\"strike\"] / otm[\"forward\"] - 1).abs() <= 0.15]\n",
    "grid = near.pivot_table(index=\"strike\", columns=\"expiration\", values=\"fitted_vol\", aggfunc=\"first\") * 100\n",
    "grid.round(1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-11",
   "metadata": {},
   "source": [
    "## 5. The same expiry at three times of day\n",
    "\n",
    "`snapshots=` pulls a set of times, one request each. Before it starts, the\n",
    "client checks the pull fits what is left of your daily allowance, and it\n",
    "records the cost in `.attrs[\"budget\"]`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-12",
   "metadata": {},
   "outputs": [],
   "source": [
    "first_expiry = expiries[\"expiration\"].iloc[1]\n",
    "day = client.chain(SYMBOL, DATE, snapshots=[\"10:02\", \"12:32\", \"15:57\"], expiration=first_expiry)\n",
    "print(day.attrs[\"budget\"])\n",
    "atm_by_time = (day.assign(distance=(day[\"strike\"] - day[\"forward\"]).abs())\n",
    "                  .sort_values(\"distance\").groupby(\"snapshot_time\").first())\n",
    "atm_by_time[[\"strike\", \"forward\", \"fitted_vol\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-13",
   "metadata": {},
   "source": [
    "## 6. The constant-maturity tenors\n",
    "\n",
    "`tenor_history()` reads one standard maturity (30, 60, 90, 180 or 360 days)\n",
    "through time. Asking for one session at `\"EOD\"` for each tenor gives the day's\n",
    "tenor grid: at-the-forward volatility and 25-delta skew by maturity."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-14",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "tenors = pd.concat([client.tenor_history(SYMBOL, t, DATE, DATE, snapshot=\"EOD\",\n",
    "                                         fields=[\"atf_iv\", \"skew_25d\"])\n",
    "                    for t in (30, 60, 90, 180, 360)], ignore_index=True)\n",
    "tenors[[\"tenor_days\", \"snapshot_time\", \"atf_iv\", \"skew_25d\"]]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-15",
   "metadata": {},
   "source": [
    "A tenor value is empty when no listed expiries span that maturity on that day.\n",
    "That is expected, not missing data.\n",
    "\n",
    "## 7. The ATM term structure\n",
    "\n",
    "`term_structure()` reads the at-the-money volatility of every listed expiry.\n",
    "It makes one request per expiry (about 20 for AAPL), and checks they fit your\n",
    "remaining allowance before it starts."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-16",
   "metadata": {},
   "outputs": [],
   "source": [
    "from optionspace import charts\n",
    "\n",
    "term = client.term_structure(SYMBOL, DATE)\n",
    "print(term.attrs[\"budget\"])\n",
    "term[[\"expiration\", \"dte\", \"atm_vol\", \"forward\"]]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-17",
   "metadata": {},
   "outputs": [],
   "source": [
    "charts.term_comparison_chart([term]).show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02-chains-and-surfaces-18",
   "metadata": {},
   "source": [
    "## 8. The one-call summary\n",
    "\n",
    "`surface()` returns a one-request, end-of-day summary per listed expiry. It is a\n",
    "separate, older summary: use the term structure above when you need the\n",
    "listed-expiry values themselves."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "02-chains-and-surfaces-19",
   "metadata": {},
   "outputs": [],
   "source": [
    "surface = client.surface(SYMBOL, DATE)\n",
    "surface.head()"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "optionspace": {
   "requests": "About 35 requests (the term structure makes one request per listed expiry)",
   "summary": "For one stock on one past session: the snapshots and expiries available, the full chain, a strike-by-expiry volatility grid, the constant-maturity tenors and the ATM term structure.",
   "title": "Chains and surfaces on a past date"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
