{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 📘 Fincept Notebook — Yield Curve & Recession Signal\n",
    "\n",
    "**Economics · Hard · ~30 min · pandas + numpy**\n",
    "\n",
    "The shape of the U.S. Treasury yield curve is one of the most-watched macro indicators. When short-term yields rise above long-term yields (an *inversion*), it has historically preceded recessions. In this notebook we build curves for a normal day and an inverted day, interpolate yields at any maturity, and compute the classic recession spreads.\n",
    "\n",
    "**What you'll learn**\n",
    "- Build a pandas DataFrame of Treasury yields across tenors (3M to 30Y)\n",
    "- Interpolate yields at off-grid maturities with `np.interp`\n",
    "- Compute the 10y-2y and 10y-3m spreads and classify the curve shape\n",
    "- Explain why a negative 10y-2y spread is read as a recession signal\n",
    "\n",
    "> **Requires** `pandas` and `numpy` (bundled with Fincept Terminal's Python environment).\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. The yield-curve data\n",
    "\n",
    "We embed two real-world-shaped snapshots of constant-maturity Treasury (CMT) yields, quoted in percent. The **2021-06-30** curve is upward-sloping (*normal*): you get paid more to lend for longer. The **2023-07-03** curve is *inverted*: the 3M bill out-yields the 10Y note — a textbook recession warning.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "Build curve DataFrame"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "# Tenors in years, and CMT yields (%) for two dates.\n",
    "TENORS = {\"3M\": 0.25, \"6M\": 0.5, \"1Y\": 1, \"2Y\": 2, \"3Y\": 3,\n",
    "          \"5Y\": 5, \"7Y\": 7, \"10Y\": 10, \"20Y\": 20, \"30Y\": 30}\n",
    "YIELDS = {\n",
    "    \"2021-06-30\": [0.05, 0.06, 0.07, 0.25, 0.46, 0.87, 1.21, 1.45, 2.00, 2.06],  # normal\n",
    "    \"2023-07-03\": [5.43, 5.47, 5.40, 4.94, 4.66, 4.36, 4.23, 3.86, 4.16, 3.91],  # inverted\n",
    "}\n",
    "\n",
    "df = pd.DataFrame(YIELDS, index=pd.Index(TENORS.keys(), name=\"Tenor\"))\n",
    "df.insert(0, \"Maturity(yr)\", list(TENORS.values()))\n",
    "\n",
    "print(\"U.S. Treasury constant-maturity yields (%)\")\n",
    "print(\"=\" * 48)\n",
    "print(df.to_string())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Interpolating yields at any maturity\n",
    "\n",
    "The market only quotes a handful of tenors, but you often need a yield at an *off-grid* point — say a bond with 4 years left, or an 18-month liability. Because the curve is smooth, simple **linear interpolation** between the two neighbouring quotes is a reasonable first approximation. `np.interp(x, xp, fp)` does exactly this: given the grid maturities `xp` and their yields `fp`, it returns the yield at each requested maturity `x`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "Interpolate off-grid"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "TENORS = {\"3M\": 0.25, \"6M\": 0.5, \"1Y\": 1, \"2Y\": 2, \"3Y\": 3,\n",
    "          \"5Y\": 5, \"7Y\": 7, \"10Y\": 10, \"20Y\": 20, \"30Y\": 30}\n",
    "YIELDS = {\n",
    "    \"2021-06-30\": [0.05, 0.06, 0.07, 0.25, 0.46, 0.87, 1.21, 1.45, 2.00, 2.06],\n",
    "    \"2023-07-03\": [5.43, 5.47, 5.40, 4.94, 4.66, 4.36, 4.23, 3.86, 4.16, 3.91],\n",
    "}\n",
    "\n",
    "xp = np.array(list(TENORS.values()))  # grid maturities in years\n",
    "targets = [0.75, 1.5, 4.0, 8.0, 15.0, 25.0]  # off-grid maturities we want\n",
    "\n",
    "rows = []\n",
    "for date, ys in YIELDS.items():\n",
    "    fp = np.array(ys)\n",
    "    interp = np.interp(targets, xp, fp)\n",
    "    for m, y in zip(targets, interp):\n",
    "        rows.append({\"Date\": date, \"Maturity(yr)\": m, \"Interp yield (%)\": round(float(y), 3)})\n",
    "\n",
    "out = pd.DataFrame(rows).pivot(index=\"Maturity(yr)\", columns=\"Date\", values=\"Interp yield (%)\")\n",
    "print(\"Linearly interpolated yields at off-grid maturities\")\n",
    "print(\"=\" * 52)\n",
    "print(out.to_string())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. The recession spreads & curve shape\n",
    "\n",
    "Two spreads matter most:\n",
    "\n",
    "- **10y − 2y**: the classic slope. It has gone negative before *every* U.S. recession since the 1970s.\n",
    "- **10y − 3m**: favoured by the Fed's own research as the cleanest predictor.\n",
    "\n",
    "We classify the overall shape from the 10y−2y spread: clearly positive → **normal**, near zero → **flat**, negative → **inverted**.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "Spreads & shape"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "TENORS = [\"3M\", \"6M\", \"1Y\", \"2Y\", \"3Y\", \"5Y\", \"7Y\", \"10Y\", \"20Y\", \"30Y\"]\n",
    "YIELDS = {\n",
    "    \"2021-06-30\": [0.05, 0.06, 0.07, 0.25, 0.46, 0.87, 1.21, 1.45, 2.00, 2.06],\n",
    "    \"2023-07-03\": [5.43, 5.47, 5.40, 4.94, 4.66, 4.36, 4.23, 3.86, 4.16, 3.91],\n",
    "}\n",
    "df = pd.DataFrame(YIELDS, index=TENORS)\n",
    "\n",
    "def classify(spread_10_2):\n",
    "    if spread_10_2 < -0.10:\n",
    "        return \"INVERTED\"\n",
    "    if spread_10_2 <= 0.25:\n",
    "        return \"FLAT\"\n",
    "    return \"NORMAL\"\n",
    "\n",
    "rows = []\n",
    "for date in df.columns:\n",
    "    s = df[date]\n",
    "    sp_10_2 = s[\"10Y\"] - s[\"2Y\"]\n",
    "    sp_10_3m = s[\"10Y\"] - s[\"3M\"]\n",
    "    rows.append({\n",
    "        \"Date\": date,\n",
    "        \"2Y (%)\": s[\"2Y\"], \"10Y (%)\": s[\"10Y\"],\n",
    "        \"10y-2y (bp)\": round(sp_10_2 * 100, 1),\n",
    "        \"10y-3m (bp)\": round(sp_10_3m * 100, 1),\n",
    "        \"Shape\": classify(sp_10_2),\n",
    "        \"Recession signal\": \"YES\" if sp_10_2 < 0 else \"no\",\n",
    "    })\n",
    "\n",
    "summary = pd.DataFrame(rows).set_index(\"Date\")\n",
    "print(\"Yield-curve spreads and classification\")\n",
    "print(\"=\" * 64)\n",
    "print(summary.to_string())\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Reading the signal\n",
    "\n",
    "Compare the two dates side by side. On the normal day the slope is steeply positive (the 10Y yields ~120bp more than the 2Y); on the inverted day the 2Y *out-yields* the 10Y by over 100bp. We print a tenor-by-tenor difference so you can see where the inversion bites hardest.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {"fincept_title": "Compare two dates"},
   "outputs": [],
   "source": [
    "try:\n",
    "    import numpy as np\n",
    "    import pandas as pd\n",
    "except ImportError:\n",
    "    raise SystemExit(\"Fincept Notebook needs pandas + numpy — install with: pip install pandas numpy\")\n",
    "\n",
    "TENORS = [\"3M\", \"6M\", \"1Y\", \"2Y\", \"3Y\", \"5Y\", \"7Y\", \"10Y\", \"20Y\", \"30Y\"]\n",
    "MAT = [0.25, 0.5, 1, 2, 3, 5, 7, 10, 20, 30]\n",
    "YIELDS = {\n",
    "    \"2021-06-30\": [0.05, 0.06, 0.07, 0.25, 0.46, 0.87, 1.21, 1.45, 2.00, 2.06],\n",
    "    \"2023-07-03\": [5.43, 5.47, 5.40, 4.94, 4.66, 4.36, 4.23, 3.86, 4.16, 3.91],\n",
    "}\n",
    "df = pd.DataFrame(YIELDS, index=TENORS)\n",
    "df[\"Change (bp)\"] = ((df[\"2023-07-03\"] - df[\"2021-06-30\"]) * 100).round(0).astype(int)\n",
    "\n",
    "print(\"Normal vs inverted curve, tenor by tenor\")\n",
    "print(\"=\" * 56)\n",
    "print(df.to_string())\n",
    "print()\n",
    "\n",
    "# Slope of each curve: yield(30Y) - yield(3M)\n",
    "for date in [\"2021-06-30\", \"2023-07-03\"]:\n",
    "    slope = df.loc[\"30Y\", date] - df.loc[\"3M\", date]\n",
    "    direction = \"upward (normal)\" if slope > 0 else \"downward (inverted)\"\n",
    "    print(f\"{date}: 30Y-3M slope = {slope:+.2f} pts -> {direction}\")\n",
    "\n",
    "print()\n",
    "print(\"Takeaway: a negative 10y-2y spread means investors expect the Fed to\")\n",
    "print(\"cut rates in the future (weaker growth), so they lock in long yields now —\")\n",
    "print(\"the curve inverts. Historically this has led recessions by ~6-18 months.\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "*— Fincept Notebook · part of Fincept Terminal. Edit any cell and press Ctrl+Enter to run.*\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
  "language_info": {"name": "python"}
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
