{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Do inflation differences line up with the currency move?\n", "Align US inflation, German inflation and ECB exchange rates on a monthly calendar before comparing their relationship.\n", "US CPI from BLS, German CPI from the OECD compilation, and USD per EUR from ECB. Monthly common observations in 2021\u20132025. Current-vintage inflation and reference FX; descriptive, not a causal or release-time trading test." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "\"\"\"Align US inflation, German inflation and ECB exchange rates on a monthly calendar before comparing their relationship.\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", " \"oecd_cpi\": qj.domains.call(\n", " \"macro.series.get_cpi_data\",\n", " provider=\"oecd\",\n", " countries=[\"USA\", \"DEU\"],\n", " start_date=\"2020-01-01\",\n", " end_date=\"2025-12-31\",\n", " ),\n", " \"bls_cpi\": qj.domains.call(\n", " \"macro.us.get_cpi\", provider=\"bls\", start_year=2020, end_year=2025, category=\"all\", seasonally_adjusted=True\n", " ),\n", " \"ecb_fx\": qj.domains.call(\n", " \"macro.eu.get_ecb_series\",\n", " provider=\"ecb\",\n", " series_id=\"EXR.D.USD.EUR.SP00.A\",\n", " start_date=\"2020-01-01\",\n", " end_date=\"2025-12-31\",\n", " ),\n", " }\n", "\n", "\n", "def analyze(raw):\n", " oecd = frame(raw[\"oecd_cpi\"])\n", " germany = oecd.loc[oecd[\"country\"] == \"DEU\"].set_index(\"date\")[\"cpi\"]\n", " germany = pd.to_numeric(germany, errors=\"raise\").resample(pd.offsets.MonthEnd()).last()\n", " us = bls_cpi(raw[\"bls_cpi\"]).resample(pd.offsets.MonthEnd()).last()\n", " rates = frame(raw[\"ecb_fx\"]).set_index(\"date\")[\"value\"].map(decimal).resample(pd.offsets.MonthEnd()).last()\n", " data = pd.DataFrame(\n", " {\n", " \"US CPI YoY %\": 100 * (us / us.shift(12) - 1),\n", " \"German CPI YoY %\": 100 * (germany / germany.shift(12) - 1),\n", " \"EUR/USD return %\": 100 * change(rates),\n", " }\n", " ).dropna()\n", " if len(data) < 24:\n", " raise ValueError(\"Need at least 24 matched inflation and FX observations\")\n", " data[\"Inflation gap pp\"] = data[\"German CPI YoY %\"] - data[\"US CPI YoY %\"]\n", " fig, (top, bottom) = canvas(\"Macro and FX \u00b7 prices across two economies\", \"CPI YoY %\", panels=2)\n", " top.plot(data.index, data[\"US CPI YoY %\"], label=\"US \u00b7 BLS\")\n", " top.plot(data.index, data[\"German CPI YoY %\"], label=\"Germany \u00b7 OECD compilation\")\n", " legend(top)\n", " bottom.scatter(data[\"Inflation gap pp\"], data[\"EUR/USD return %\"], color=BLUE, s=26, alpha=0.8)\n", " bottom.set_xlabel(\"German minus US inflation \u00b7 percentage points\")\n", " bottom.set_ylabel(\"Monthly EUR/USD return %\")\n", " bottom.axhline(0, color=\"#afc1dc\", lw=0.8)\n", " return data.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 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 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", "def bls_cpi(response):\n", " return macro(response, \"cpi_all\")\n", "\n", "\n", "OUTPUT = \"sdk_macro_fx.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 }