{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# How large are holders against trading volume?\n", "Compare reported positions with normal and high-volatility trading volume observed before the holdings date.\n", "AAPL 2025Q3. Top ten share positions within 100 returned records, labelled by SEC CIK. Volume denominators are the prior 20 sessions and the highest-VIX 15% of the prior 252 sessions. Both windows end at the holdings date. These are 100%-ADV equivalents, not execution forecasts; 13F disclosures arrive later." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Compare reported positions with normal and high-volatility trading volume observed before the holdings date.\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 numpy as np\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.domains.call(\n", " \"macro.indicators.get_fred_data_series_by_id\",\n", " provider=\"fred\",\n", " search_id=\"VIXCLS\",\n", " start=\"2020-01-01\",\n", " end=\"2025-12-31\",\n", " ),\n", " \"holders\": qj.domains.call(\n", " \"regulatory.ownership.get_institutional_holders\",\n", " provider=\"fmp\",\n", " symbol=\"AAPL\",\n", " year=2025,\n", " quarter=3,\n", " limit=100,\n", " ),\n", " \"prices\": qj.domains.call(\n", " \"equity.pricing.get_historical_prices\",\n", " provider=\"eod\",\n", " symbols=[\"AAPL\"],\n", " start_date=\"2020-01-01\",\n", " end_date=\"2025-12-31\",\n", " frequency=\"1d\",\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " holders = frame(raw[\"holders\"])\n", " holders = holders.loc[(holders[\"sharesType\"] == \"SH\") & (holders[\"putCallShare\"] == \"Share\")].copy()\n", " prices = history(raw[\"prices\"], \"AAPL\")\n", " vix = macro(raw[\"vix_history\"], \"VIXCLS\")\n", " rows = []\n", " for holder in holders.to_dict(\"records\"):\n", " volume = prices.loc[prices.index <= holder[\"date\"], \"volume\"].tail(20)\n", " if len(volume) < 20:\n", " continue\n", " mean_volume = sum(volume, Decimal(0)) / Decimal(len(volume))\n", " sample = prices.loc[prices.index <= holder[\"date\"], [\"volume\"]].tail(252)\n", " sample = sample.join(vix.rename(\"VIX\"), how=\"inner\").dropna()\n", " stressed = sample.loc[sample[\"VIX\"] >= sample[\"VIX\"].quantile(0.85), \"volume\"]\n", " if len(sample) < 200 or stressed.empty:\n", " continue\n", " stress_volume = sum(stressed, Decimal(0)) / Decimal(len(stressed))\n", " rows.append(\n", " {\n", " \"Stress ADV-equivalent days\": ratio(holder[\"sharesNumber\"], stress_volume),\n", " \"CIK\": str(holder[\"cik\"]).zfill(10),\n", " \"Holdings date\": holder[\"date\"],\n", " \"ADV-equivalent days\": ratio(holder[\"sharesNumber\"], mean_volume),\n", " }\n", " )\n", " result = pd.DataFrame(rows)\n", " if result.empty:\n", " raise ValueError(\"No equity holdings with sufficient historical volume\")\n", " result = result.nlargest(10, \"ADV-equivalent days\").sort_values(\"ADV-equivalent days\")\n", " fig, (ax,) = canvas(\"AAPL \u00b7 institutional position size in context\", \"SEC CIK\")\n", " y = np.arange(len(result))\n", " ax.barh(y - 0.18, result[\"ADV-equivalent days\"], 0.36, label=\"Trailing 20 sessions\")\n", " ax.barh(y + 0.18, result[\"Stress ADV-equivalent days\"], 0.36, label=\"High-VIX days in prior 252 sessions\")\n", " ax.set_yticks(y, result[\"CIK\"])\n", " legend(ax)\n", " ax.set_xlabel(\"Position shares / mean daily shares traded \u00b7 days\")\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 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_ownership_capacity.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 }