{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Do the sources support the same equity thesis?\n", "Review earnings, cash conversion, insider filing activity and short interest without collapsing different dates into one score.\n", "AAPL. Financial statements, returned Form 4 filings and FINRA short interest retain different reporting dates. Form 4 counts measure filing activity, not net insider buying. This evidence dashboard is not a conviction score or recommendation." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Review earnings, cash conversion, insider filing activity and short interest without collapsing different dates into one score.\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", " \"cash\": qj.fmp.get_cash_flow_statement(symbol=\"AAPL\", period=\"annual\", limit=5),\n", " \"sec_insider\": qj.sec.get_insider_transactions(symbol=\"AAPL\", limit=100),\n", " \"short\": qj.finra.get_short_interest(symbol=\"AAPL\"),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " statements = frame(raw[\"income\"])[[\"date\", \"netIncome\"]].merge(\n", " frame(raw[\"cash\"])[[\"date\", \"operatingCashFlow\"]], on=\"date\", validate=\"one_to_one\"\n", " )\n", " if statements.empty:\n", " raise ValueError(\"No matching earnings and cash periods\")\n", " statements[\"Cash conversion %\"] = [\n", " 100 * ratio(cash, earnings)\n", " for cash, earnings in zip(statements[\"operatingCashFlow\"], statements[\"netIncome\"], strict=True)\n", " ]\n", " filings = frame(raw[\"sec_insider\"], \"filingDate\")\n", " filings = filings.loc[(filings[\"form\"] == \"4\") & (filings[\"filingDate\"].dt.date <= AS_OF)]\n", " filings = filings.drop_duplicates(\"accessionNumber\")\n", " if filings.empty:\n", " raise ValueError(\"No returned Form 4 filings\")\n", " monthly = filings.set_index(\"filingDate\").resample(pd.offsets.MonthEnd()).size().tail(12)\n", " short = frame(raw[\"short\"], \"settlement_date\").iloc[-1]\n", " fig, (top, middle, bottom) = canvas(\"Cross-source equity evidence \u00b7 AAPL\", \"\", panels=3)\n", " top.bar(statements[\"date\"].dt.year.astype(str), statements[\"Cash conversion %\"], color=BLUE)\n", " top.axhline(100, color=\"#afc1dc\", linestyle=\"--\", lw=0.8)\n", " top.set_ylabel(\"Cash / earnings %\")\n", " months = pd.DatetimeIndex([day.replace(day=1) for day in monthly.index])\n", " middle.bar(months, monthly, width=18, color=PINK)\n", " middle.set_ylabel(\"Returned Form 4 filings\")\n", " bottom.bar(\n", " [\"Previous\", \"Current\"],\n", " [ratio(short[\"previous_short_position\"], 1000000), ratio(short[\"current_short_position\"], 1000000)],\n", " color=[BLUE, PINK],\n", " )\n", " bottom.set_ylabel(\"Short shares \u00b7 millions\")\n", " bottom.set_xlabel(\"FINRA settlement \" + str(short[\"settlement_date\"].date()))\n", " statements[\"Latest Form 4 date\"] = filings[\"filingDate\"].max()\n", " statements[\"Short settlement date\"] = short[\"settlement_date\"]\n", " return statements, 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 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_equity_conviction.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 }