{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "04-earnings-research-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": "04-earnings-research-00",
   "metadata": {},
   "source": [
    "# Earnings research\n",
    "\n",
    "Three research calls, each answering a whole question in one request:\n",
    "\n",
    "1. **Clean ATM history**: 30-day ATM volatility with and without the next\n",
    "   scheduled earnings report.\n",
    "2. **Earnings history**: the move the options priced before each recent\n",
    "   report, against the move that followed, and how the priced move built up.\n",
    "3. **Earnings backtest**: a simple put spread sold before each report and\n",
    "   closed after it, with fills, fees and the position's daily risk.\n",
    "\n",
    "The example is NVDA as of 16 September 2026. Install and log in as in\n",
    "notebook 1.\n",
    "\n",
    "**Research calls are slower.** A lookup takes well under a second; a research\n",
    "call can take several seconds to a minute. Ask what one can cost with\n",
    "`estimate=True` before running it, and its result says what it cost. They run\n",
    "one at a time per account: starting a second while one is running is refused\n",
    "with a short wait time."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04-earnings-research-01",
   "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": "04-earnings-research-02",
   "metadata": {},
   "outputs": [],
   "source": [
    "import optionspace as osp\n",
    "\n",
    "client = osp.Client()\n",
    "print(\"optionspace\", osp.__version__, \"| key from:\", client.credential_source)\n",
    "import time\n",
    "\n",
    "import pandas as pd\n",
    "from optionspace import charts\n",
    "\n",
    "SYMBOL, AS_OF = \"NVDA\", \"2026-09-16\"\n",
    "\n",
    "\n",
    "def timed(call, *args, **kwargs):\n",
    "    started = time.perf_counter()\n",
    "    result = call(*args, **kwargs)\n",
    "    print(f\"{call.__name__}: {time.perf_counter() - started:.1f} s\")\n",
    "    return result"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04-earnings-research-03",
   "metadata": {},
   "source": [
    "## 2. ATM volatility with and without earnings\n",
    "\n",
    "`clean_atm_history()` returns two series at one constant maturity: the value\n",
    "the market quoted (`dirty_iv`) and the value with the next scheduled earnings\n",
    "report removed (`clean_iv`). Where the removal cannot be vouched for, the clean\n",
    "value repeats the quoted one and `status` says why."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-04",
   "metadata": {},
   "outputs": [],
   "source": [
    "clean = timed(client.clean_atm_history, SYMBOL, \"2026-01-02\", AS_OF, tenor_days=30)\n",
    "rows = pd.DataFrame(clean[\"rows\"])\n",
    "print(\"earnings in the window:\", [e.get(\"date\") for e in clean.get(\"earnings\", [])])\n",
    "rows[[\"data_date\", \"snapshot_time\", \"dirty_iv\", \"clean_iv\", \"clean_iv_se\", \"status\"]].tail(10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-05",
   "metadata": {},
   "outputs": [],
   "source": [
    "charts.clean_atm_chart(clean).show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04-earnings-research-06",
   "metadata": {},
   "source": [
    "Read the `notes` that come with every research result: they say what the\n",
    "numbers are and are not."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-07",
   "metadata": {},
   "outputs": [],
   "source": [
    "for note in clean.get(\"notes\") or []:\n",
    "    print(\"-\", note)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04-earnings-research-08",
   "metadata": {},
   "source": [
    "## 3. Priced against realized, report by report\n",
    "\n",
    "`earnings_history()` returns the most recent reports before the as-of date:\n",
    "`implied_move` is the average size of the move the options priced at the last\n",
    "close before the report (a fraction of the stock price, about 0.80 times the\n",
    "event's volatility), and `actual_move` is the size of the move that followed\n",
    "(`actual_return` keeps its sign)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-09",
   "metadata": {},
   "outputs": [],
   "source": [
    "history = timed(client.earnings_history, SYMBOL, AS_OF, count=8)\n",
    "events = pd.DataFrame(history[\"events\"])\n",
    "events[[\"earnings_date\", \"timing\", \"implied_move\", \"actual_move\", \"actual_return\", \"status\"]]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-10",
   "metadata": {},
   "outputs": [],
   "source": [
    "charts.earnings_comparison_chart(history).show()\n",
    "charts.earnings_runup_chart(history).show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04-earnings-research-11",
   "metadata": {},
   "source": [
    "## 4. A simple earnings backtest\n",
    "\n",
    "`earnings_backtest()` sells a put spread (short near 25 delta, long near 10\n",
    "delta, in the first standard monthly expiry after the report) a set number of\n",
    "sessions before each report and closes it after. Here: four reports, entry\n",
    "five sessions before, exit one session after, fills at the bid and ask, and\n",
    "$0.65 per contract per fill. The rules come back under `assumptions`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-12",
   "metadata": {},
   "outputs": [],
   "source": [
    "backtest = timed(client.earnings_backtest, SYMBOL, AS_OF, count=4,\n",
    "                 entry_offset=-5, exit_offset=1, fill_model=\"quote_side\", fee_per_contract=0.65)\n",
    "print(backtest[\"assumptions\"])\n",
    "print(backtest[\"summary\"])\n",
    "trades = pd.DataFrame(backtest[\"events\"])\n",
    "trades[[c for c in (\"earnings_date\", \"status\", \"reason\", \"entry_date\", \"exit_date\", \"expiration\",\n",
    "                    \"gross_pnl\", \"fees\", \"net_pnl\") if c in trades.columns]]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-13",
   "metadata": {},
   "outputs": [],
   "source": [
    "charts.earnings_backtest_chart(backtest).show()\n",
    "if backtest[\"summary\"].get(\"traded_events\"):\n",
    "    charts.backtest_path_chart(backtest).show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "04-earnings-research-14",
   "metadata": {},
   "source": [
    "P&L is in US dollars for one spread. This is a historical simulation: it does\n",
    "not model assignment, financing, margin or market impact.\n",
    "\n",
    "## 5. If a call is refused\n",
    "\n",
    "Research calls run one at a time per account. If you start one while another\n",
    "is still running (in another notebook, say), the client raises `QuotaExceeded`\n",
    "with the reason and how many seconds to wait. This helper waits and tries\n",
    "again; it makes no request until you use it, for example\n",
    "`with_retry(client.earnings_history, SYMBOL, AS_OF, count=8)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04-earnings-research-15",
   "metadata": {},
   "outputs": [],
   "source": [
    "def with_retry(call, *args, attempts=3, **kwargs):\n",
    "    for attempt in range(attempts):\n",
    "        try:\n",
    "            return call(*args, **kwargs)\n",
    "        except osp.QuotaExceeded as refused:\n",
    "            busy = (refused.reason or \"\").startswith(\"concurrent\")\n",
    "            if not busy or attempt == attempts - 1:\n",
    "                raise\n",
    "            wait = refused.retry_after_seconds or 5\n",
    "            print(f\"refused ({refused.reason}); trying again in {wait} s\")\n",
    "            time.sleep(wait)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  },
  "optionspace": {
   "requests": "3 research calls",
   "summary": "For one stock: ATM volatility with and without its earnings, the move priced before each recent report against the move that followed, and a simple earnings put-spread backtest.",
   "title": "Earnings research"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
