{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Is speculative positioning stretched?\n", "Normalize net noncommercial futures-and-options positions by open interest and compare them with the equity-market path.\n", "E-mini S&P 500 legacy combined COT, contract 13874A, 2020\u20132025. The axis uses report dates, not publication dates. SPY is market context, not the futures contract or strategy P&L." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Normalize net noncommercial futures-and-options positions by open interest and compare them with the equity-market path.\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", " \"cot\": qj.cftc.get_cot_data(\n", " symbol=\"13874A\", report_type=\"legacy\", start_date=\"2020-01-01\", end_date=\"2025-12-31\"\n", " ),\n", " \"prices\": qj.eod.get_historical_prices(\n", " symbols=[\"SPY\"], start_date=\"2020-01-01\", end_date=\"2025-12-31\", frequency=\"1d\"\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " cot = frame(raw[\"cot\"], \"report_date_as_yyyy_mm_dd\")\n", " cot = cot.loc[cot[\"futonly_or_combined\"] == \"Combined\"].copy()\n", " cot[\"Net / OI %\"] = [\n", " 100 * ratio(decimal(long) - decimal(short), interest)\n", " for long, short, interest in zip(\n", " cot[\"noncomm_positions_long_all\"], cot[\"noncomm_positions_short_all\"], cot[\"open_interest_all\"], strict=True\n", " )\n", " ]\n", " cot = cot.set_index(\"report_date_as_yyyy_mm_dd\")\n", " prices = history(raw[\"prices\"], \"SPY\")[\"price\"].rename(\"SPY\")\n", " joined = pd.merge_asof(\n", " cot[[\"Net / OI %\"]],\n", " prices.to_frame(),\n", " left_index=True,\n", " right_index=True,\n", " direction=\"backward\",\n", " tolerance=pd.Timedelta(days=7),\n", " ).dropna()\n", " if joined.empty:\n", " raise ValueError(\"No matching report dates and market observations\")\n", " joined[\"SPY index\"] = joined[\"SPY\"].map(lambda x: 100 * ratio(x, joined[\"SPY\"].iloc[0]))\n", " fig, (top, bottom) = canvas(\"Positioning \u00b7 the speculative balance\", \"Net noncommercial / OI %\", panels=2)\n", " top.plot(joined.index, joined[\"Net / OI %\"], color=BLUE)\n", " top.axhline(0, color=\"#afc1dc\", lw=0.8)\n", " bottom.plot(joined.index, joined[\"SPY index\"], color=PINK)\n", " bottom.set_ylabel(\"SPY adjusted index \u00b7 start = 100\")\n", " bottom.set_xlabel(\"CFTC report date \u00b7 publication occurs later\")\n", " return joined[[\"Net / OI %\", \"SPY index\"]].reset_index(), 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", "OUTPUT = \"sdk_cot.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 }