{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Which sectors moved with inflation?\n", "Join actual CPI observations with sector returns relative to SPY, then compare descriptive relationships across the sample.\n", "Monthly common observations in 2021\u20132025. CPI YoY level versus sector-minus-SPY monthly return correlations. Current-vintage CPI; no release-time strategy or causal inference." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Join actual CPI observations with sector returns relative to SPY, then compare descriptive relationships across the sample.\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", " \"cpi\": qj.fred.get_fred_data_series_by_id(\n", " search_id=\"CPIAUCSL\", start=\"2020-01-01\", end=\"2025-12-31\", frequency=\"m\"\n", " ),\n", " \"prices\": qj.eod.get_historical_prices(\n", " symbols=[\"SPY\", \"XLE\", \"XLK\", \"XLF\", \"XLU\", \"XLP\", \"XLY\"],\n", " start_date=\"2020-01-01\",\n", " end_date=\"2025-12-31\",\n", " frequency=\"1d\",\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " cpi = macro(raw[\"cpi\"], \"CPIAUCSL\").resample(pd.offsets.MonthEnd()).last()\n", " inflation = (100 * (cpi / cpi.shift(12) - 1)).rename(\"CPI YoY %\")\n", " returns = {}\n", " for symbol in [\"SPY\", \"XLE\", \"XLK\", \"XLF\", \"XLU\", \"XLP\", \"XLY\"]:\n", " monthly = history(raw[\"prices\"], symbol)[\"price\"].resample(pd.offsets.MonthEnd()).last()\n", " returns[symbol] = change(monthly)\n", " joined = pd.DataFrame(returns).join(inflation, how=\"inner\").dropna()\n", " if len(joined) < 24:\n", " raise ValueError(\"Need at least 24 common macro and return observations\")\n", " rows = []\n", " for symbol in [\"XLE\", \"XLK\", \"XLF\", \"XLU\", \"XLP\", \"XLY\"]:\n", " relative = joined[symbol] - joined[\"SPY\"]\n", " correlation = relative.corr(joined[\"CPI YoY %\"])\n", " rows.append({\"Sector ETF\": symbol, \"Correlation\": correlation, \"Months\": len(joined)})\n", " result = pd.DataFrame(rows).sort_values(\"Correlation\")\n", " fig, (ax,) = canvas(\"Inflation \u00b7 sector-relative relationships\", \"Sector ETF\")\n", " ax.barh(result[\"Sector ETF\"], result[\"Correlation\"], color=[PINK if v < 0 else BLUE for v in result[\"Correlation\"]])\n", " ax.axvline(0, color=\"#afc1dc\", lw=0.8)\n", " ax.set_xlabel(\"Correlation with CPI YoY \u00b7 sector return minus SPY return\")\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 change(values, periods=1):\n", " previous = values.shift(periods)\n", " return pd.Series(\n", " [ratio(x, y) - 1 if pd.notna(x) and pd.notna(y) else np.nan for x, y in zip(values, previous, strict=True)],\n", " index=values.index,\n", " )\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 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_inflation.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 }