From bd42e42f59305c9271a43fd9919870af8d521752 Mon Sep 17 00:00:00 2001 From: Bradford Levy <5950006+bradfordlynch@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:03:53 -0700 Subject: [PATCH] Add the options market data notebook and the implied-move rescaling helper Notebook 04 reads the option-implied statistics item and uses the implied move as an event-specific scale for a submission's predictions. The score is invariant to a uniform affine remap, so only a per-event factor can move it. The naive rescale (pivot 0.5, linear in the move) lowers R-squared for both reference baselines; pivoting at the submission's own mean with a square-root factor raises it for Gemini and leaves GPT-5 nano flat. The table has eight columns: surprise only, surprise plus the option statistics as regressors, and per baseline the surprise with the prediction as submitted, naively scaled, and adjusted, followed by the competition's own delta R-squared on the full quarter. scale_by_implied_move in examples.option_stats implements both rules, with tests. The bundled 2026Q3 sample now carries the item on its six events so the parsing cells run offline; the regression cells need the live archive. --- README.md | 5 +- data/README.md | 14 +- .../archive_EARNINGS_RELEASE_2026Q3.jsonl.gz | Bin 30545 -> 30801 bytes notebooks/01_historical_archive.ipynb | 2 +- notebooks/03_earnings_previews.ipynb | 2 +- notebooks/04_options_market_data.ipynb | 504 ++++++++++++++++++ src/examples/__init__.py | 2 + src/examples/option_stats.py | 51 ++ tests/test_option_stats.py | 82 +++ 9 files changed, 653 insertions(+), 9 deletions(-) create mode 100644 notebooks/04_options_market_data.ipynb diff --git a/README.md b/README.md index f1a1632..e287d80 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ notebooks stay short and readable. | [`notebooks/01_historical_archive.ipynb`](notebooks/01_historical_archive.ipynb) | Download, cache, and load the historical archive, then reproduce the baseline scoring regressions (Koijen & Levy WP, Table 3) with the competition's exact scoring transform | | [`notebooks/02_earnings_call_facts.ipynb`](notebooks/02_earnings_call_facts.ipynb) | Where each event's ten-fact `disclosure` comes from — the exact prompt, model config, and parsing behind it, and how extraction failures surface | | [`notebooks/03_earnings_previews.ipynb`](notebooks/03_earnings_previews.ipynb) | The **earnings preview** — the result of running [Anthropic's Earnings Preview Skill](https://github.com/anthropics/financial-services/blob/main/plugins/agent-plugins/earnings-reviewer/skills/earnings-preview/SKILL.md) prior to each earnings announcement. Starting Q4 2026, events carry this as a second disclosure item. The notebook covers how it is built, what it looks like, and how it pairs with the facts | +| [`notebooks/04_options_market_data.ipynb`](notebooks/04_options_market_data.ipynb) | The **option-implied statistics** — what the options market priced for each release, why the scorer is blind to a uniform rescaling of predictions, and how the implied move works as an *event-specific* scale for a prediction. Reproduces the effect on the two reference baselines with the exact scoring transform | ### The option-implied statistics @@ -34,11 +35,11 @@ stats = option_implied_stats_from_disclosure(record) # dict, or None wh move = statistic_value(stats, "implied_absolute_earnings_move") # float, or None unless status is "ok" ``` -`examples.option_stats` documents what the three numbers mean and when they are missing. There is no notebook for it yet, and the bundled 2026Q3 sample predates the item, so run against the live archive to see it. +`examples.option_stats` documents what the three numbers mean and when they are missing, and `scale_by_implied_move` turns the move into an event-specific rescaling of a submission's predictions. Notebook 04 walks through both; its regression sections need the live archive (the bundled sample carries the item on its six events but no baseline predictions). If you parse bundles with your own typed model, make sure an item's `content` accepts any JSON type. A model that allows only strings and lists of strings rejects the whole bundle, facts included, the first time it meets this item. `examples.schemas.DisclosureItem` had exactly that annotation until this release. -All four run top-to-bottom. With an API key they hit the live API; **without one they +All five run top-to-bottom. With an API key they hit the live API; **without one they fall back to a small bundled sample** (`data/sample/`), so you can run them — and CI can execute them — with no credentials. diff --git a/data/README.md b/data/README.md index 0528569..000a7cf 100644 --- a/data/README.md +++ b/data/README.md @@ -18,12 +18,16 @@ Just enough data for the notebooks to run top-to-bottom **without an API key** lines may include additional fields. Treat the API as the source of truth. - `archive_EARNINGS_RELEASE_2026Q3.jsonl.gz` — six **real** archive lines (NVDA, WMT, HD, CRM, AVGO, ORCL), copied verbatim from the live 2026Q3 file on - 2026-09-24. Each carries both disclosure items — the ten facts and the - agent-written **earnings preview** — plus the realized `event_returns`, + 2026-09-24. Each carries all three disclosure items — the ten facts, the + agent-written **earnings preview**, and the **option-implied statistics** (the + last spliced in from the same events' archive lines on 2026-09-29, ahead of its + dissemination on the public archive) — plus the realized `event_returns`, `metrics`, and `knowledge_cutoff` the archive adds once an event is scored, so - `notebooks/03_earnings_previews.ipynb` and the preview section of notebook 01 - run offline on genuine content. 2026Q3 was unsealed when this was pulled, so - fields may later change upstream; the API remains the source of truth. + `notebooks/03_earnings_previews.ipynb`, `notebooks/04_options_market_data.ipynb` + and the preview section of notebook 01 run offline on genuine content. The + lines' `baseline_predictions` are empty, so notebook 04's regression sections + skip in sample mode. 2026Q3 was unsealed when this was pulled, so fields may + later change upstream; the API remains the source of truth. - `transcript_sample.json` — a **synthetic** earnings-call transcript for a fictional company (Northwind Logistics). Real transcripts are licensed and are not distributed with this repo; this one exists so diff --git a/data/sample/archive_EARNINGS_RELEASE_2026Q3.jsonl.gz b/data/sample/archive_EARNINGS_RELEASE_2026Q3.jsonl.gz index 88774e4e767031cfd78b99aab68e8752c6d5fa4f..a6ac1c63cad50c46876bc4965c9bbea2e8c13ea9 100644 GIT binary patch literal 30801 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 literal 30545 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 diff --git a/notebooks/01_historical_archive.ipynb b/notebooks/01_historical_archive.ipynb index 6434f98..e21d16c 100644 --- a/notebooks/01_historical_archive.ipynb +++ b/notebooks/01_historical_archive.ipynb @@ -323,7 +323,7 @@ "2. Convert that reaction into a percentile rank in `[0, 1]` **across the quarter's cross-section** of scored event outcomes \u2014 a rank against all the period's events, not against the asset's own history. That's the target the competition scores; `examples.scoring.percentile_ranks` is the exact transform (see section 6).\n", "3. Score your model's predicted percentile against it offline, and iterate.\n", "\n", - "For the ex ante side of each event \u2014 the earnings preview \u2014 see [`03_earnings_previews.ipynb`](03_earnings_previews.ipynb).\n", + "For the ex ante side of each event \u2014 the earnings preview \u2014 see [`03_earnings_previews.ipynb`](03_earnings_previews.ipynb). For what the options market priced for each event, and why only an *event-specific* rescaling can move the score, see [`04_options_market_data.ipynb`](04_options_market_data.ipynb).\n", "\n", "When your model is ready, move it into a deployed webhook handler (see the starter repos) \u2014 that's what receives live events and submits predictions. These notebooks deliberately stop short of `POST /predictions`." ] diff --git a/notebooks/03_earnings_previews.ipynb b/notebooks/03_earnings_previews.ipynb index aa5ecb7..0098654 100644 --- a/notebooks/03_earnings_previews.ipynb +++ b/notebooks/03_earnings_previews.ipynb @@ -327,7 +327,7 @@ "- The preview is **optional** and arrived in 2026Q3. Match on `id` / `kind`, never on position, and build for its absence.\n", "- It is a **public-sources research note with a fixed brief** \u2014 Anthropic's open `earnings-preview` skill, run by Claude in a sandboxed Claude Code session \u2014 not a model prediction and not a proprietary feed. Its sources and dates are cited inline.\n", "- Going forward it is assembled **before the CAR1 window opens**; the 2026Q3 archive previews are not guaranteed to be, so treat them as illustrative.\n", - "- The facts (notebook 02) are the ex post disclosure; the preview is the ex ante expectation; `car1` is the reaction. `examples.scoring` turns the last into the percentile target the competition scores." + "- The facts (notebook 02) are the ex post disclosure; the preview is the ex ante expectation; `car1` is the reaction. `examples.scoring` turns the last into the percentile target the competition scores.\n- The third item, the **option-implied statistics**, is the subject of [`04_options_market_data.ipynb`](04_options_market_data.ipynb)." ] } ], diff --git a/notebooks/04_options_market_data.ipynb b/notebooks/04_options_market_data.ipynb new file mode 100644 index 0000000..fb4644d --- /dev/null +++ b/notebooks/04_options_market_data.ipynb @@ -0,0 +1,504 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a20b0ee9", + "metadata": {}, + "source": [ + "# 04 — Options market data: what the options market expected\n", + "\n", + "From 2026Q3 an event may carry a third disclosure item, the **option-implied statistics**: three numbers read off the stock's listed options just before the release. They say how large a move the options market priced, not which way. The item is `id=\"option-implied-stats\"`, `kind=\"stats\"`, `source=\"option_market\"`, with `content` an **object**. It is optional: fewer than half of events have options liquid enough to measure, and no event before 2026Q3 carries it.\n", + "\n", + "This notebook reads the item, then uses it as an **event-specific scale** for a prediction. The score ignores any uniform rescaling of your predictions, so only something that varies across events can move it. We test that on the two reference baselines with the exact scoring transform.\n", + "\n", + "1. What you receive — coverage in the 2026Q3 archive\n", + "2. The three numbers, on six events\n", + "3. Magnitude, not direction\n", + "4. Why the score needs an event-specific transform\n", + "5. Two rescalings: naive and adjusted\n", + "6. The regression table\n", + "7. The competition's own metric\n", + "8. Caveats\n", + "\n", + "With a live API key it reads the real 2026Q3 archive; without one, the six real archive lines in `data/sample/`, which carry the item. Sections 3 and 5–7 need the whole quarter and skip in sample mode." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5cb3fd2", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from IPython.display import display\n", + "\n", + "from examples import Client, load_config\n", + "from examples.archive import download_archive, read_jsonl_gz\n", + "from examples.option_stats import (\n", + " STATISTICS,\n", + " option_implied_stats_from_disclosure,\n", + " option_stats_frame,\n", + " scale_by_implied_move,\n", + " statistic_value,\n", + ")\n", + "from examples.scoring import (\n", + " BASELINE_LABELS,\n", + " add_percentiles,\n", + " outcomes_frame,\n", + " percentile_ranks,\n", + " score_submission,\n", + ")\n", + "\n", + "REPO = next(p for p in [Path.cwd(), *Path.cwd().parents] if (p / \"data\" / \"sample\").is_dir())\n", + "ARCHIVE_DIR = REPO / \"data\" / \"archive\" # gitignored download cache\n", + "SAMPLE_DIR = REPO / \"data\" / \"sample\"\n", + "\n", + "QUARTER = \"2026Q3\" # the first quarter with option-implied statistics\n", + "TICKERS = [\"NVDA\", \"WMT\", \"HD\", \"CRM\", \"AVGO\", \"ORCL\"] # notebook 03's six, also bundled\n", + "MOVE = \"implied_absolute_earnings_move\"\n", + "REFRESH = False # True re-downloads the quarter even when a copy is cached (section 1)\n", + "MIN_EVENTS = 100 # fewest covered events worth regressing on\n", + "\n", + "config = load_config()\n", + "print(\"Mode:\", \"LIVE\" if config.is_live else \"SAMPLE (no EM_API_KEY — using bundled data)\")" + ] + }, + { + "cell_type": "markdown", + "id": "e17dc48f", + "metadata": {}, + "source": [ + "## 1. What you receive\n", + "\n", + "`option_stats_frame` gives one row per event: the three statistics as float columns (`NaN` when unavailable), `as_of`, `methodology`, and `skew_status`, which says why the skew is missing when the volatility is present.\n", + "\n", + "The live branch downloads only when the quarter is not cached or `REFRESH = True`. The quarter is unsealed, so a cached copy goes stale; a covered count of zero means your copy predates the item." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8aa0efbf", + "metadata": {}, + "outputs": [], + "source": [ + "path = ARCHIVE_DIR / f\"EARNINGS_RELEASE_{QUARTER}.jsonl.gz\"\n", + "if not config.is_live:\n", + " path = SAMPLE_DIR / f\"archive_EARNINGS_RELEASE_{QUARTER}.jsonl.gz\"\n", + " print(f\"Sample mode: reading {path.name}\")\n", + "elif REFRESH or not path.exists():\n", + " with Client.from_env() as client:\n", + " manifest = client.archive_manifest()\n", + " (path,) = download_archive(\n", + " manifest,\n", + " ARCHIVE_DIR,\n", + " client=client,\n", + " only=lambda f: f.event_type == \"EARNINGS_RELEASE\" and f.quarter == QUARTER,\n", + " )\n", + "else:\n", + " print(f\"Using the cached {path.name}; set REFRESH = True to re-download\")\n", + "\n", + "records = list(read_jsonl_gz(path))\n", + "stats = option_stats_frame(records)\n", + "covered = stats[stats[\"has_option_stats\"]]\n", + "n_covered = len(covered)\n", + "\n", + "print(f\"{len(records):,} events in {QUARTER}; {n_covered:,} carry option-implied statistics\")\n", + "if n_covered:\n", + " print(f\" share covered : {n_covered / len(records):.0%}\")\n", + " print(f\" methodology : {sorted(covered['methodology'].dropna().unique())}\")\n", + " print(f\" skew available : {covered['skew_25_delta'].notna().mean():.0%} of covered\")\n", + " print(f\" as_of before cutoff : {bool((covered['as_of'] < covered['knowledge_cutoff']).all())}\")\n", + "else:\n", + " print(\" (none: this copy of the archive predates the item's dissemination)\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0e1d393c", + "metadata": {}, + "outputs": [], + "source": [ + "outcomes = outcomes_frame(records) # one row per (event, asset) that has a car1\n", + "models = list(BASELINE_LABELS.values()) # [\"Gemini 2.5 Flash-Lite\", \"GPT-5 nano\"]\n", + "\n", + "have_scoring_data = (\n", + " n_covered >= MIN_EVENTS\n", + " and not outcomes.empty\n", + " and set(models) <= set(outcomes.columns)\n", + " and int(outcomes[\"surprise\"].notna().sum()) >= MIN_EVENTS\n", + ")\n", + "if have_scoring_data:\n", + " print(\n", + " f\"Regression sections on: {len(outcomes):,} scored outcomes, {n_covered:,} covered events\"\n", + " )\n", + "elif not config.is_live:\n", + " print(\"Sample mode: sections 3 and 5 to 7 need the live archive. Skipping.\")\n", + "else:\n", + " print(\n", + " f\"Live archive, but not enough to regress on ({n_covered:,} covered events; baseline \"\n", + " f\"predictions {'present' if set(models) <= set(outcomes.columns) else 'absent'}). \"\n", + " \"Sections 3 and 5 to 7 skip.\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "383cefc6", + "metadata": {}, + "source": [ + "## 2. The three numbers, on six events\n", + "\n", + "Each statistic is a `{value, status}` pair; `value` is a decimal (`0.0676` is 6.76%) and `null` unless `status` is `\"ok\"`.\n", + "\n", + "- `implied_earnings_volatility` — the risk-neutral standard deviation of the jump on the release, backed out of at-the-money implied volatility on the first two expirations after it (Dubinsky, Johannes, Kaeck and Seeger, *RFS* 2019).\n", + "- `implied_absolute_earnings_move` — the risk-neutral expected absolute return from that jump, `4·Φ(J/2) − 2 ≈ 0.8·J`. Same information, in return units.\n", + "- `skew_25_delta` — 25-delta call minus 25-delta put implied volatility on the nearest expiration. Negative means puts are richer than calls. Missing on its own when the wings are thin.\n", + "\n", + "`as_of` always precedes `knowledge_cutoff`, so the item is a legitimate ex ante input. Below: the raw object for one of notebook 03's six events, then all six." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a929e19d", + "metadata": {}, + "outputs": [], + "source": [ + "def latest_record(ticker: str) -> dict | None:\n", + " hits = [r for r in records if r[\"focal_assets\"][0][\"identifier_value\"] == ticker]\n", + " return max(hits, key=lambda r: r[\"event_datetime\"]) if hits else None\n", + "\n", + "\n", + "chosen = {t: r for t in TICKERS if (r := latest_record(t)) is not None}\n", + "if not chosen: # a different quarter, say: fall back to the first six covered events\n", + " ids = set(covered[\"event_id\"].head(6))\n", + " chosen = {r[\"focal_assets\"][0][\"identifier_value\"]: r for r in records if r[\"event_id\"] in ids}\n", + "\n", + "first = next(iter(chosen.values()))\n", + "raw = option_implied_stats_from_disclosure(first)\n", + "print(f\"{first['event_id']} — the item's content object:\")\n", + "print(json.dumps(raw, indent=2))\n", + "print()\n", + "for name in STATISTICS:\n", + " print(f\"{name:32s} {statistic_value(raw, name)}\") # None unless status is 'ok'" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20127bff", + "metadata": {}, + "outputs": [], + "source": [ + "six = option_stats_frame(chosen.values()).set_index(\"tickers\").loc[list(chosen)]\n", + "six[\"move / vol\"] = six[MOVE] / six[\"implied_earnings_volatility\"]\n", + "display(\n", + " six[[\"as_of\", \"knowledge_cutoff\", *STATISTICS, \"skew_status\", \"move / vol\"]].style.format(\n", + " {\n", + " \"as_of\": \"{:%Y-%m-%d %H:%M}\",\n", + " \"knowledge_cutoff\": \"{:%Y-%m-%d %H:%M}\",\n", + " \"implied_earnings_volatility\": \"{:.2%}\",\n", + " MOVE: \"{:.2%}\",\n", + " \"skew_25_delta\": \"{:+.2%}\",\n", + " \"move / vol\": \"{:.3f}\",\n", + " },\n", + " na_rep=\"\\u2014\",\n", + " )\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "d8b9d51a", + "metadata": {}, + "source": [ + "## 3. Magnitude, not direction\n", + "\n", + "The move is about the size of the reaction, not its sign, and it is risk-neutral rather than an unbiased forecast. The skew is an asymmetry in option prices, not a predicted return. Over the quarter the move correlates strongly with the *absolute* realized abnormal return; the skew's correlation with the *signed* return is nil." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33ee8cf2", + "metadata": {}, + "outputs": [], + "source": [ + "joined = outcomes.merge(stats[[\"event_id\", *STATISTICS]], on=\"event_id\", how=\"left\")\n", + "\n", + "six_ids = {r[\"event_id\"] for r in chosen.values()}\n", + "display(\n", + " joined[joined[\"event_id\"].isin(six_ids)]\n", + " .set_index(\"identifier_value\")[[\"car1\", MOVE, \"skew_25_delta\"]]\n", + " .reindex(list(chosen)) # a very recent live event may have no car1 yet\n", + " .rename(columns={MOVE: \"implied move\", \"skew_25_delta\": \"skew\"})\n", + " .style.format({\"car1\": \"{:+.2%}\", \"implied move\": \"{:.2%}\", \"skew\": \"{:+.2%}\"}, na_rep=\"\\u2014\")\n", + ")\n", + "\n", + "if have_scoring_data:\n", + " cov = joined.dropna(subset=[\"car1\", MOVE])\n", + " with_skew = cov.dropna(subset=[\"skew_25_delta\"])\n", + " print(f\"Covered outcomes: {len(cov):,}\")\n", + " print(f\" corr(|car1|, implied move) = {cov['car1'].abs().corr(cov[MOVE]):+.2f}\")\n", + " print(f\" corr(car1, implied move) = {cov['car1'].corr(cov[MOVE]):+.2f}\")\n", + " print(\n", + " f\" corr(car1, skew) = {with_skew['car1'].corr(with_skew['skew_25_delta']):+.2f}\"\n", + " )\n", + " print(\n", + " f\" |car1| above implied move : {(cov['car1'].abs() > cov[MOVE]).mean():.0%} of outcomes\"\n", + " )\n", + "\n", + " fig, ax = plt.subplots(figsize=(6.5, 4))\n", + " ax.scatter(cov[MOVE] * 100, cov[\"car1\"].abs() * 100, s=9, alpha=0.45, color=\"#0072B2\", lw=0)\n", + " lim = cov[MOVE].max() * 100\n", + " ax.plot([0, lim], [0, lim], color=\"#9AA0A6\", lw=1, ls=\"--\") # realized == implied\n", + " ax.set_xlabel(\"implied absolute earnings move (%)\")\n", + " ax.set_ylabel(\"|car1| realized (%)\")\n", + " ax.set_title(f\"{QUARTER}: the priced move vs. the realized one\")\n", + " ax.grid(color=\"#D7DBE0\", lw=0.6)\n", + " ax.set_axisbelow(True)\n", + " for side in (\"top\", \"right\"):\n", + " ax.spines[side].set_visible(False)\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "ed68cbc5", + "metadata": {}, + "source": [ + "## 4. Why the score needs an event-specific transform\n", + "\n", + "The competition fits `y = α + β₁·ĝ + β₂·s` within a period (`y` the percentile rank of `car1`, `s` that of the surprise, `ĝ` your predicted percentile) and credits the R² gained over the surprise-only fit. That gain is `β₁²·s₁₁.₂ / s_yy` (see `examples.scoring`), so replacing every `p` with `a + b·p` changes nothing: `β₁` absorbs `b`, `α` absorbs `a`. Neither the direction nor the spread of your numbers is scored.\n", + "\n", + "Only a factor that differs across events can move the score. The implied move is one, known before the cutoff. The question is what to multiply, and by how much." + ] + }, + { + "cell_type": "markdown", + "id": "60631c7f", + "metadata": {}, + "source": [ + "## 5. Two rescalings: naive and adjusted\n", + "\n", + "A prediction is a centre plus a deviation. A stock priced for a 3% move cannot reach the tails of the cross-section, so its deviation should count for less; one priced for 12% can. Ranks are bounded, so the effect saturates: `E[y | ĝ, m] ≈ centre + (ĝ − centre)·g(m)`, `g` increasing and concave.\n", + "\n", + "The **naive** rescale, `0.5 + (ĝ − 0.5)·m/m̄`, gets both wrong. The baselines centre near 0.7, not 0.5, so pivoting at 0.5 pushes every low-move event down and every high-move event up whatever the model said. And a linear factor of up to three or four clips about one event in seven at 0 or 1.\n", + "\n", + "The **adjusted** rescale pivots at the submission's own mean and takes a square root: `μ + (ĝ − μ)·√(m/m̄)`. These are the defaults of `scale_by_implied_move`; the naive rule is `pivot=0.5, power=1.0`. Events without the item keep their prediction. The fit sample is the quarter's covered outcomes with a surprise and both baselines, ranked over the **whole** quarter as the scorer does. First the mechanism: the slope of `y` on `ĝ` by tercile of the implied move." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "741c16b7", + "metadata": {}, + "outputs": [], + "source": [ + "if have_scoring_data:\n", + " # Rank over the WHOLE quarter first (the scorer's percentiles), then restrict.\n", + " scored = add_percentiles(outcomes).merge(\n", + " stats[[\"event_id\", *STATISTICS]], on=\"event_id\", how=\"left\"\n", + " )\n", + " fit_df = scored.dropna(subset=[\"y\", \"surprise_pct\", *models, MOVE]).copy()\n", + "\n", + " def ranked(values: pd.Series) -> pd.Series:\n", + " return pd.Series(percentile_ranks(values.tolist()), index=values.index)\n", + "\n", + " fit_df[\"move_pct\"] = ranked(fit_df[MOVE])\n", + " n_no_skew = int(fit_df[\"skew_25_delta\"].isna().sum())\n", + " fit_df[\"skew_pct\"] = ranked(fit_df[\"skew_25_delta\"].fillna(fit_df[\"skew_25_delta\"].mean()))\n", + "\n", + " for m in models:\n", + " fit_df[f\"{m} naive\"] = scale_by_implied_move(fit_df[m], fit_df[MOVE], pivot=0.5, power=1.0)\n", + " fit_df[f\"{m} adjusted\"] = scale_by_implied_move(fit_df[m], fit_df[MOVE])\n", + "\n", + " def share_clipped(column: str) -> float:\n", + " return float(((fit_df[column] <= 0) | (fit_df[column] >= 1)).mean())\n", + "\n", + " print(f\"Fit sample: N = {len(fit_df):,} covered outcomes with a surprise and both baselines\")\n", + " print(\n", + " f\" mean implied move = {fit_df[MOVE].mean():.2%}; skew mean-filled on {n_no_skew:,} rows\"\n", + " )\n", + " for m in models:\n", + " print(\n", + " f\" {m}: mean ĝ = {fit_df[m].mean():.3f} over {fit_df[m].nunique()} distinct values; \"\n", + " f\"clipped {share_clipped(f'{m} naive'):.1%} naive, \"\n", + " f\"{share_clipped(f'{m} adjusted'):.1%} adjusted\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01c9b576", + "metadata": {}, + "outputs": [], + "source": [ + "if have_scoring_data:\n", + " import statsmodels.api as sm\n", + "\n", + " fit_df[\"move tercile\"] = pd.qcut(fit_df[MOVE], 3, labels=[\"low\", \"mid\", \"high\"])\n", + " rows = []\n", + " for tercile, group in fit_df.groupby(\"move tercile\", observed=True):\n", + " row = {\"tercile\": tercile, \"n\": len(group), \"mean move\": group[MOVE].mean()}\n", + " row[\"sd(y)\"] = group[\"y\"].std()\n", + " for m in models:\n", + " X = sm.add_constant(group[[\"surprise_pct\", m]])\n", + " row[f\"slope on ĝ — {m}\"] = sm.OLS(group[\"y\"], X).fit().params[m]\n", + " rows.append(row)\n", + " display(\n", + " pd.DataFrame(rows)\n", + " .set_index(\"tercile\")\n", + " .style.format({\"mean move\": \"{:.1%}\", \"sd(y)\": \"{:.3f}\"}, precision=3)\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "4e0fe301", + "metadata": {}, + "source": [ + "## 6. The regression table\n", + "\n", + "Eight columns on one fit sample: the surprise alone; the surprise plus the two option statistics as plain regressors (percentile-ranked, skew mean-filled); then for each baseline, the surprise with `ĝ` as submitted, naively scaled, and adjusted. Read the R² row.\n", + "\n", + "As of early October 2026: the option statistics add about 0.003 as regressors; the naive rescale costs Gemini about 0.007 and GPT-5 nano about 0.015; the adjusted rescale adds about 0.005 for Gemini and nothing for GPT-5 nano. Modest, but in the right direction, and a freely fitted interaction does no better. The numbers will drift as the quarter fills in." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97af8216", + "metadata": {}, + "outputs": [], + "source": [ + "if have_scoring_data:\n", + " import statsmodels.api as sm\n", + " from stargazer.stargazer import Stargazer\n", + "\n", + " gemini, gpt = models # display names, in BASELINE_LABELS order\n", + " S, RM, RK = \"s (surprise pctile)\", \"implied move (pctile)\", \"skew (pctile)\"\n", + " G, GN, GA = \"ĝ (model pctile)\", \"ĝ naively scaled\", \"ĝ adjusted\"\n", + "\n", + " def fit(cols):\n", + " # cols: {display_name -> source column}; shared names let stargazer stack rows.\n", + " X = sm.add_constant(pd.DataFrame({name: fit_df[src] for name, src in cols.items()}))\n", + " return sm.OLS(fit_df[\"y\"].to_numpy(), X).fit()\n", + "\n", + " fits = [\n", + " fit({S: \"surprise_pct\"}), # (1) surprise only\n", + " fit({S: \"surprise_pct\", RM: \"move_pct\", RK: \"skew_pct\"}), # (2) surprise + options\n", + " ]\n", + " for m in (gemini, gpt):\n", + " fits += [\n", + " fit({S: \"surprise_pct\", G: m}), # surprise + model\n", + " fit({S: \"surprise_pct\", GN: f\"{m} naive\"}), # surprise + model, naively scaled\n", + " fit({S: \"surprise_pct\", GA: f\"{m} adjusted\"}), # surprise + model, adjusted\n", + " ]\n", + "\n", + " star = Stargazer(fits)\n", + " star.title(f\"Rescaling the reference baselines by the option-implied move — {QUARTER}\")\n", + " star.custom_columns([\"Benchmarks\", gemini, gpt], [2, 3, 3])\n", + " star.covariate_order([\"const\", S, RM, RK, G, GN, GA])\n", + " star.rename_covariates({\"const\": \"Intercept\"})\n", + " star.dependent_variable_name(\"r_abn (percentiled CAR1)\")\n", + " star.add_line(\n", + " \"ĝ transform\",\n", + " [\"\", \"\", \"as submitted\", \"naive\", \"adjusted\", \"as submitted\", \"naive\", \"adjusted\"],\n", + " )\n", + " star.show_model_numbers(True)\n", + " display(star)" + ] + }, + { + "cell_type": "markdown", + "id": "6d9fac44", + "metadata": {}, + "source": [ + "## 7. The competition's own metric\n", + "\n", + "Section 6 used a common fit sample. Here each transform is applied as you would submit it, over every scored outcome in the quarter with uncovered events keeping `ĝ`, and `score_submission` reports `delta_r_squared`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ac93475", + "metadata": {}, + "outputs": [], + "source": [ + "if have_scoring_data:\n", + " full = scored.copy() # every scored outcome in the quarter; NaN move -> ĝ unchanged\n", + " rows = []\n", + " for m in models:\n", + " full[f\"{m} naive\"] = scale_by_implied_move(full[m], full[MOVE], pivot=0.5, power=1.0)\n", + " full[f\"{m} adjusted\"] = scale_by_implied_move(full[m], full[MOVE])\n", + " for label, column in (\n", + " (\"as submitted\", m),\n", + " (\"naive\", f\"{m} naive\"),\n", + " (\"adjusted\", f\"{m} adjusted\"),\n", + " ):\n", + " result = score_submission(full, column)\n", + " rows.append(\n", + " {\n", + " \"baseline\": m,\n", + " \"ĝ transform\": label,\n", + " \"n_obs\": result[\"n_obs\"],\n", + " \"R² surprise\": result[\"r_squared_surprise\"],\n", + " \"R²\": result[\"r_squared\"],\n", + " \"ΔR²\": result[\"delta_r_squared\"],\n", + " }\n", + " )\n", + " display(\n", + " pd.DataFrame(rows)\n", + " .set_index([\"baseline\", \"ĝ transform\"])\n", + " .style.format({\"R² surprise\": \"{:.4f}\", \"R²\": \"{:.4f}\", \"ΔR²\": \"{:+.4f}\"})\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "f5a1c982", + "metadata": {}, + "source": [ + "## 8. Caveats\n", + "\n", + "- The move is a **risk-neutral magnitude**: no direction, and the realized absolute return exceeds it more than a third of the time.\n", + "- **Coverage is partial.** Fewer than half of events carry the item, mostly larger companies, so most predictions are untouched; hence section 7's gain is smaller than section 6's.\n", + "- **One quarter, in sample.** A few thousandths of R², with the pivot and reference scale computed on the same data. The lesson is the shape of the result, not its size.\n", + "- A multi-asset event carries one item, attached above to each focal asset.\n", + "\n", + "## What to take away\n", + "\n", + "- The item is optional. Match on `id` and `kind`, read each `status`, and build for its absence.\n", + "- The move is about `0.8` times the volatility, so use one of the two. The skew is not a direction.\n", + "- The score ignores any uniform remap of your predictions; a per-event input helps only through how it differs across events.\n", + "- Pivot at your own centre and let the factor saturate. `scale_by_implied_move` does both by default; `pivot=0.5, power=1.0` shows what the naive version costs.\n", + "\n", + "## Where to go from here\n", + "\n", + "The item reaches a live event before its `knowledge_cutoff`, so a deployed handler can apply `scale_by_implied_move` to its own prediction before `POST /predictions`, with its typical prediction as the pivot and a running mean of past moves as the scale. For the scoring transform see section 6 of [`01_historical_archive.ipynb`](01_historical_archive.ipynb); for the preview beside this item, [`03_earnings_previews.ipynb`](03_earnings_previews.ipynb)." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/examples/__init__.py b/src/examples/__init__.py index 6802faa..eb8e07f 100644 --- a/src/examples/__init__.py +++ b/src/examples/__init__.py @@ -14,6 +14,8 @@ scoring — the competition's exact percentile scoring transform summary — the exact 10-fact extraction behind each event's disclosure preview — the agent-written earnings preview attached from 2026Q3 onward + option_stats — the option-implied statistics attached from 2026Q3 onward, and + the implied-move rescaling of a submission's predictions """ from __future__ import annotations diff --git a/src/examples/option_stats.py b/src/examples/option_stats.py index 3c9d9bb..6b9b4ec 100644 --- a/src/examples/option_stats.py +++ b/src/examples/option_stats.py @@ -47,6 +47,12 @@ thinly to measure, which is most smaller companies. Fewer than half of 2026Q3 events carry it. The skew can be unavailable on its own when only the out-of-the-money options are thin. Events before 2026Q3 never carry the item. + +Using the move: the competition's score is invariant to any uniform affine remap of +a submission's predictions (see :mod:`examples.scoring`), so a number that is the +same for every event cannot change it. The implied move differs across events, and +:func:`scale_by_implied_move` uses it to stretch or shrink each prediction's +distance from the submission's own centre. Notebook 04 shows the effect. """ from __future__ import annotations @@ -157,3 +163,48 @@ def option_stats_frame(records: Iterable[dict]) -> pd.DataFrame: df[column] = pd.to_datetime(df[column], utc=True, format="ISO8601") df = df.sort_values("event_datetime").reset_index(drop=True) return df + + +def scale_by_implied_move( + prediction: pd.Series, + move: pd.Series, + *, + pivot: float | None = None, + power: float = 0.5, +) -> pd.Series: + """Rescale predicted percentiles event by event with the option-implied move. + + Returns ``pivot + (prediction - pivot) * (move / move.mean()) ** power``, clipped + to ``[0, 1]`` and indexed like ``prediction``. ``move`` is aligned to + ``prediction`` by index label first, so pass two columns of the same frame or + two series sharing an index. An event with no move (``NaN``) keeps its + prediction unchanged, and if no event has a usable move the input comes back + as it is. + + Why this shape: the competition scores how well predictions explain the + *percentile rank* of the one-day abnormal return. A rank cannot grow without + bound, so the effect of a larger expected magnitude saturates, and a + submission's own forecasts are rarely centred at 0.5. Hence the two defaults: + + ``pivot=None`` + Pivot at the submission's own mean prediction. Stretching away from 0.5 + instead pushes every low-move event down and every high-move event up + whatever the model thought, which adds the move as a signed regressor the + data do not support. + ``power=0.5`` + A square-root factor, so a move four times the average doubles the + deviation rather than quadrupling it. On 2026Q3 this parameter-free rule + captures essentially all of the gain a freely fitted interaction would. + + ``pivot=0.5, power=1.0`` gives the "naive" rescale, ``0.5 + (p - 0.5) * m / mean(m)``, + which lowers the score for the reference baselines. The reference scale + ``move.mean()`` matters only through the clip: dividing by any other positive + constant is an affine remap, which the score ignores. + """ + move = pd.to_numeric(move.reindex(prediction.index), errors="coerce").astype("float64") + reference = float(move.mean()) + if not reference > 0: + return prediction.copy() + centre = float(prediction.mean()) if pivot is None else pivot + factor = (move / reference).pow(power).where(move.notna(), 1.0) + return (centre + (prediction - centre) * factor).clip(0.0, 1.0) diff --git a/tests/test_option_stats.py b/tests/test_option_stats.py index 7495e6e..b8fe2db 100644 --- a/tests/test_option_stats.py +++ b/tests/test_option_stats.py @@ -4,19 +4,24 @@ import math +import pandas as pd import pytest +from examples.archive import read_jsonl_gz from examples.option_stats import ( OPTION_STATS_ID, OPTION_STATS_SOURCE, + STATISTICS, option_implied_stats_from_disclosure, option_stats_frame, + scale_by_implied_move, statistic_status, statistic_value, ) from examples.preview import earnings_preview_from_disclosure from examples.schemas import Disclosure from examples.summary import facts_from_disclosure +from tests.conftest import SAMPLE_ARCHIVE_2026Q3 from tests.test_preview import FACTS_ITEM, PREVIEW_ITEM STATS = { @@ -140,3 +145,80 @@ def test_option_stats_frame() -> None: assert with_stats["as_of"] < with_stats["knowledge_cutoff"] assert math.isnan(without["implied_earnings_volatility"]) and without["tickers"] == "NKE" assert option_stats_frame([]).empty + + +# ----- scale_by_implied_move --------------------------------------------------- + + +def test_scale_leaves_uncovered_events_unchanged() -> None: + prediction = pd.Series([0.9, 0.3, 0.6, 0.75]) + move = pd.Series([0.05, float("nan"), 0.20, float("nan")]) + out = scale_by_implied_move(prediction, move) + assert out.index.equals(prediction.index) + assert out[1] == prediction[1] and out[3] == prediction[3] + assert out[0] != prediction[0] and out[2] != prediction[2] + # No usable move at all: the input comes back as it is. + nothing = pd.Series([float("nan")] * 4) + pd.testing.assert_series_equal(scale_by_implied_move(prediction, nothing), prediction) + + +def test_scale_pivots_at_the_prediction_mean_by_default() -> None: + prediction = pd.Series([0.9, 0.3, 0.6]) # mean 0.6 + move = pd.Series([0.05, 0.05, 0.20]) # mean 0.10 + out = scale_by_implied_move(prediction, move) + # Below-average move shrinks the deviation from the mean by sqrt(1/2)... + assert out[0] == pytest.approx(0.6 + 0.3 * math.sqrt(0.5)) + assert out[1] == pytest.approx(0.6 - 0.3 * math.sqrt(0.5)) + # ...and a prediction at the pivot is untouched whatever the move. + assert out[2] == pytest.approx(0.6) + # A move equal to the average leaves the prediction exactly as it was. + same = scale_by_implied_move(prediction, pd.Series([0.1, 0.1, 0.1])) + pd.testing.assert_series_equal(same, prediction) + # A move twice the average stretches the deviation by sqrt(2). + assert scale_by_implied_move(prediction, pd.Series([0.1, 0.1, 0.4]))[2] == pytest.approx(0.6) + stretched = scale_by_implied_move(pd.Series([0.8, 0.4]), pd.Series([0.2, 0.05])) + assert stretched[0] == pytest.approx(0.6 + 0.2 * math.sqrt(0.2 / 0.125)) + + +def test_scale_clips_to_the_unit_interval() -> None: + out = scale_by_implied_move( + pd.Series([0.05, 0.95, 0.5]), pd.Series([1.0, 1.0, 0.0001]), pivot=0.5, power=1.0 + ) + assert list(out) == [0.0, 1.0, 0.5] + + +def test_naive_rescale_is_pivot_half_power_one() -> None: + prediction = pd.Series([0.82, 0.55, 0.9, 0.5, 0.2]) + move = pd.Series([0.03, 0.08, 0.15, 0.06, float("nan")]) + naive = (0.5 + (prediction - 0.5) * move / move.mean()).clip(0, 1) + naive[4] = prediction[4] # the uncovered event keeps its prediction + out = scale_by_implied_move(prediction, move, pivot=0.5, power=1.0) + pd.testing.assert_series_equal(out, naive) + + +def test_scale_aligns_on_index() -> None: + prediction = pd.Series([0.9, 0.3, 0.6], index=["a", "b", "c"]) + move = pd.Series([0.05, 0.20, 0.05], index=["b", "c", "a"]) # shuffled labels + out = scale_by_implied_move(prediction, move) + expected = scale_by_implied_move(prediction, move.reindex(prediction.index)) + pd.testing.assert_series_equal(out, expected) + assert list(out.index) == ["a", "b", "c"] + + +def test_bundled_2026q3_sample_carries_option_stats_for_every_event() -> None: + records = list(read_jsonl_gz(SAMPLE_ARCHIVE_2026Q3)) + assert len(records) == 6 + frame = option_stats_frame(records) + assert frame["has_option_stats"].all() + assert (frame["as_of"] < frame["knowledge_cutoff"]).all() + assert frame[list(STATISTICS)].notna().all().all() # all six have liquid wings + for record in records: + stats = option_implied_stats_from_disclosure(record) + j = statistic_value(stats, "implied_earnings_volatility") + phi = 0.5 * (1 + math.erf((j / 2) / math.sqrt(2))) + assert statistic_value(stats, "implied_absolute_earnings_move") == pytest.approx( + 4 * phi - 2, abs=1e-4 + ) + # The other two items are still there and still parse. + assert len(facts_from_disclosure(record)) == 10 + assert earnings_preview_from_disclosure(record)