{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Is capital earning more than the benchmark?\n", "Calculate a reported-capital return proxy and compare it with the Treasury yield observed during each fiscal year.\n", "AAPL FY2022\u20132025. After-tax operating income / average debt plus equity less cash. The 10Y Treasury yield is a benchmark, not WACC." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Calculate a reported-capital return proxy and compare it with the Treasury yield observed during each fiscal year.\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", " \"income\": qj.fmp.get_income_statement(symbol=\"AAPL\", period=\"annual\", limit=5),\n", " \"balance\": qj.fmp.get_balance_sheet_statement(symbol=\"AAPL\", period=\"annual\", limit=5),\n", " \"rates_all\": qj.fred.get_fred_data_series_by_id(search_id=\"DGS10\", start=\"2020-01-01\", end=\"2025-12-31\"),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " income, balance = frame(raw[\"income\"]), frame(raw[\"balance\"])\n", " data = income.merge(\n", " balance[[\"date\", \"totalDebt\", \"totalStockholdersEquity\", \"cashAndCashEquivalents\"]],\n", " on=\"date\",\n", " validate=\"one_to_one\",\n", " )\n", " rates = macro(raw[\"rates_all\"], \"DGS10\")\n", " rows = []\n", " for previous, current in zip(data.to_dict(\"records\"), data.to_dict(\"records\")[1:], strict=False):\n", "\n", " def capital(row):\n", " return (\n", " decimal(row[\"totalDebt\"])\n", " + decimal(row[\"totalStockholdersEquity\"])\n", " - decimal(row[\"cashAndCashEquivalents\"])\n", " )\n", "\n", " average_capital = (capital(previous) + capital(current)) / Decimal(2)\n", " pretax = decimal(current[\"incomeBeforeTax\"])\n", " if pretax <= 0 or average_capital <= 0:\n", " continue\n", " tax = decimal(current[\"incomeTaxExpense\"]) / pretax\n", " nopat = decimal(current[\"operatingIncome\"]) * (Decimal(1) - tax)\n", " window = rates.loc[(rates.index > previous[\"date\"]) & (rates.index <= current[\"date\"])]\n", " if window.empty:\n", " continue\n", " rows.append(\n", " {\n", " \"Fiscal year\": current[\"date\"].year,\n", " \"ROIC proxy %\": 100 * ratio(nopat, average_capital),\n", " \"10Y Treasury %\": window.mean(),\n", " }\n", " )\n", " result = pd.DataFrame(rows)\n", " if result.empty:\n", " raise ValueError(\"No comparable capital and Treasury observations\")\n", " fig, (ax,) = canvas(\"AAPL \u00b7 capital returns and the Treasury benchmark\", \"% per year\")\n", " for column in [\"ROIC proxy %\", \"10Y Treasury %\"]:\n", " ax.plot(result[\"Fiscal year\"].astype(str), result[column], marker=\"o\", label=column)\n", " legend(ax)\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 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 legend(ax):\n", " ax.legend(frameon=False, labelcolor=\"#e9eef8\", fontsize=9)\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_capital_returns.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 }