{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Did earnings surprises move the stock?\n", "Join reported EPS surprises to adjusted-price windows and inspect the size and direction of the subsequent move.\n", "AAPL events within 2020\u20132025 price coverage. Entry is the first closing price after the reported event date; exit is five sessions later. Descriptive, without a causal claim. VIX before the event date supplies market-risk context; it does not explain the return." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Join reported EPS surprises to adjusted-price windows and inspect the size and direction of the subsequent move.\n", "\n", "Research example. Live observations can change.\n", "\"\"\"\n", "\n", "from datetime import UTC, datetime\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", " return {\n", " \"vix_history\": qj.fred.get_fred_data_series_by_id(search_id=\"VIXCLS\", start=\"2020-01-01\", end=\"2025-12-31\"),\n", " \"earnings\": qj.fmp.get_earnings_surprises(symbol=\"AAPL\"),\n", " \"prices\": qj.eod.get_historical_prices(\n", " symbols=[\"AAPL\"], start_date=\"2020-01-01\", end_date=\"2025-12-31\", frequency=\"1d\"\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " events = frame(raw[\"earnings\"])\n", " price = history(raw[\"prices\"], \"AAPL\")[\"price\"]\n", " vix = macro(raw[\"vix_history\"], \"VIXCLS\")\n", " rows = []\n", " for event in events.to_dict(\"records\"):\n", " if event[\"date\"] < price.index.min() or pd.isna(event[\"surprisePercentage\"]):\n", " continue\n", " window = price.loc[price.index > event[\"date\"]]\n", " if len(window) < 6:\n", " continue\n", " prior_vix = vix.loc[vix.index < event[\"date\"]]\n", " if prior_vix.empty:\n", " continue\n", " rows.append(\n", " {\n", " \"Prior VIX\": prior_vix.iloc[-1],\n", " \"Event\": event[\"date\"],\n", " \"EPS surprise %\": float(event[\"surprisePercentage\"]),\n", " \"Next-window return %\": 100 * (ratio(window.iloc[5], window.iloc[0]) - 1),\n", " }\n", " )\n", " result = pd.DataFrame(rows)\n", " if result.empty:\n", " raise ValueError(\"No earnings events within available price coverage\")\n", " fig, (ax,) = canvas(\"AAPL \u00b7 earnings surprise versus subsequent move\", \"Five-session adjusted return %\")\n", " points = ax.scatter(\n", " result[\"EPS surprise %\"], result[\"Next-window return %\"], s=55, c=result[\"Prior VIX\"], cmap=\"cool\", alpha=0.9\n", " )\n", " fig.colorbar(points, ax=ax, label=\"VIX before the event date\")\n", " ax.axhline(0, color=\"#afc1dc\", lw=0.8)\n", " ax.axvline(0, color=\"#afc1dc\", lw=0.8)\n", " ax.set_xlabel(\"Reported EPS surprise % \u00b7 FMP\")\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 macro(response, column):\n", " values = frame(response).set_index(\"date\")[column]\n", " values = pd.to_numeric(values, errors=\"coerce\").dropna()\n", " if values.empty:\n", " raise ValueError(f\"No numeric observations for {column}\")\n", " return values\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 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_earnings_reaction.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 }