{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Which reported positions would take longest to exit?\n", "Combine a common institutional filer\u2019s three-stock holdings sample with report-date trading volume and peer ownership.\n", "AAPL, MSFT and NVDA, holdings dated 2025-09-30. The selected CIK is the first common filer in the returned samples. Exit days assume 10% participation in trailing 20-session volume. Peer coverage is limited to returned records. This scenario does not predict forced selling or reconstruct the filer\u2019s full portfolio." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Combine a common institutional filer\u2019s three-stock holdings sample with report-date trading volume and peer ownership.\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", " raw = {\n", " \"book_prices\": qj.eod.get_historical_prices(\n", " symbols=[\"AAPL\", \"MSFT\", \"NVDA\"], start_date=\"2025-01-01\", end_date=\"2025-12-31\", frequency=\"1d\"\n", " )\n", " }\n", " for symbol, key in [(\"AAPL\", \"holders\"), (\"MSFT\", \"holders_msft\"), (\"NVDA\", \"holders_nvda\")]:\n", " raw[key] = qj.fmp.get_institutional_holders(symbol=symbol, year=2025, quarter=3, limit=100)\n", " return raw\n", "\n", "\n", "def analyze(raw):\n", " result = selected_filing_book(raw).sort_values(\"Exit days at 10% ADV\")\n", " fig, (top, bottom) = canvas(\"Crowding and exit liquidity \u00b7 reported holdings\", \"\", panels=2)\n", " top.barh(result[\"Symbol\"], result[\"Exit days at 10% ADV\"], color=BLUE)\n", " top.set_xlabel(\"Scenario days \u00b7 position / (10% \u00d7 trailing ADV)\")\n", " top.set_title(\"Selected filer CIK \" + result[\"CIK\"].iloc[0], loc=\"left\", color=\"#e9eef8\", fontsize=11)\n", " bottom.barh(result[\"Symbol\"], result[\"Reported peer shares / ADV\"], color=PINK)\n", " bottom.set_xlabel(\"Other returned holders\u2019 shares / trailing ADV\")\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 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", "def selected_filing_book(raw):\n", " books = {}\n", " for symbol, key in [(\"AAPL\", \"holders\"), (\"MSFT\", \"holders_msft\"), (\"NVDA\", \"holders_nvda\")]:\n", " book = frame(raw[key])\n", " book = book.loc[(book[\"sharesType\"] == \"SH\") & (book[\"putCallShare\"] == \"Share\")].copy()\n", " book[\"CIK\"] = book[\"cik\"].astype(str).str.zfill(10)\n", " book[\"filingDate\"] = pd.to_datetime(book[\"filingDate\"], utc=True)\n", " book = book.sort_values(\"filingDate\").drop_duplicates([\"CIK\", \"date\"], keep=\"last\")\n", " books[symbol] = book\n", " common = set.intersection(*(set(book[\"CIK\"]) for book in books.values()))\n", " if not common:\n", " raise ValueError(\"No common filer across the three returned holdings samples\")\n", " selected = min(common)\n", " rows = []\n", " for symbol, book in books.items():\n", " observations = book.loc[book[\"CIK\"] == selected]\n", " if len(observations) != 1:\n", " raise ValueError(\"Ambiguous filing position\")\n", " position = observations.iloc[0]\n", " prices = history(raw[\"book_prices\"], symbol)\n", " volume = prices.loc[prices.index <= position[\"date\"], \"volume\"].tail(20)\n", " if len(volume) != 20:\n", " raise ValueError(\"Need 20 sessions before the holdings date\")\n", " adv = sum(volume, Decimal(0)) / Decimal(20)\n", " if adv <= 0:\n", " raise ValueError(\"Average volume must be positive\")\n", " participation = Decimal(\"0.10\") # Explicit execution scenario, not observed trading capacity.\n", " rows.append(\n", " {\n", " \"Symbol\": symbol,\n", " \"CIK\": selected,\n", " \"Holdings date\": position[\"date\"],\n", " \"Filing date\": position[\"filingDate\"],\n", " \"Exit days at 10% ADV\": ratio(position[\"sharesNumber\"], adv * participation),\n", " \"Reported peer shares / ADV\": ratio(\n", " sum(book[\"sharesNumber\"].map(decimal), Decimal(0)) - decimal(position[\"sharesNumber\"]), adv\n", " ),\n", " }\n", " )\n", " result = pd.DataFrame(rows)\n", " if result[\"Holdings date\"].nunique() != 1:\n", " raise ValueError(\"Holdings dates differ\")\n", " return result\n", "\n", "\n", "OUTPUT = \"sdk_crowded_exit.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 }