Yield Curve Slope

Composite signal measuring the structural shape of the U.S. Treasury curve using 10Y–2Y and 10Y–3M spreads. Captures cyclical steepening and inversion phases that reflect monetary stance and growth expectations.
Last updated: February 9, 2026

Abstract

The yield curve slope signal evaluates macro‑financial regimes via the shape of the Treasury term structure. Two primary spreads are used: T10Y2Y (10‑year minus 2‑year yield) and T10Y3M (10‑year minus 3‑month yield). Each is normalised using a robust rolling z‑score, then averaged to form a composite slope index. Thresholds classify curve phases as Steepening, Flat/Neutral, or Inverted. Version 1.1 adds: (i) an explicit data‑source overview for constant‑maturity yields, (ii) report‑level transparency (a required table), and (iii) a hysteresis rule to reduce “uninversion” whipsaws when the slope hovers near zero.

1. Data Sources

1.1 T10Y2Y (10‑year minus 2‑year, constant‑maturity spread)

1.2 T10Y3M (10‑year minus 3‑month, constant‑maturity spread)

1.3 What “constant‑maturity” means (why it matters)

“Constant‑maturity” Treasury yields are constructed by reading (interpolating) yields off a fitted Treasury yield curve at fixed maturities. This produces a synthetic 2‑year, 10‑year, etc. yield even when no outstanding security has that exact remaining maturity. Because these are estimates, they may be revised and can be influenced by liquidity/market microstructure (especially at inflection points).

Implementation note
  • Resampling: use month‑end last observation (not a monthly average) to preserve turning points.
  • Missing‑data policy: do not forward‑fill or interpolate month‑ends; publish missing‑data flags.

2. Normalisation

z\_{rob}(X)_t = \dfrac{X_t − \mathrm{median}(X)_{t, w}}{1.4826 · \mathrm{MAD}(X)_{t, w}}
where w = 60 months (minimum 24). The constant 1.4826 scales the median absolute deviation to σ under normality, providing resilience to outliers and structural breaks.

Robust z‑scores are used so that unusually large inversions/steepenings are comparable across different rate regimes (e.g., 2003–2007 vs. post‑2020).

3. Composite Construction

  1. Compute z‑scores: derive Z\_{10Y2Y} and Z\_{10Y3M} via the robust rolling method.
  2. Blend: arithmetic mean to reduce idiosyncratic noise:
    YC\_{Composite,t} = \frac{Z\_{10Y2Y,t} + Z\_{10Y3M,t}}{2}

4. Regime Classification (with Hysteresis) — v1.1

4.1 Entry thresholds (unchanged)

\text{If } C_t > 0.75 \rightarrow \textit{Steepening};\quad |C_t| \le 0.75 \rightarrow \textit{Flat/Neutral};\quad C_t < −0.75 \rightarrow \textit{Inverted}

4.2 Hysteresis rule (new)

To reduce false “uninversion” signals when the curve oscillates around zero, regimes are not allowed to switch immediately upon crossing the boundary. Instead, a hysteresis band and a sustained confirmation window are applied.

Parameters (default)
  • Entry band: ±0.75 (as above).
  • Exit band: ±0.50 (tighter band for leaving Steepening/Inverted).
  • Confirmation: require k = 2 consecutive month‑ends beyond the exit band before changing state.

Practical implication: once the model declares Inverted, it will only revert to “uninverted” (Flat/Neutral or Steepening) after the composite rises above −0.50 for two consecutive month‑ends. This requires sustained steepening rather than a single‑month oscillation.

5. Report Transparency — Required Table (new)

Every published report should include the latest observation table so readers can see the raw spreads, the standardised values, and how close the signal is to regime boundaries.

Minimum table fields (recommend last 12 month‑ends)
Date (month‑end) T10Y2Y (pp) T10Y3M (pp) Z_T10Y2Y Z_T10Y3M Composite C Regime (hysteresis) Distance to entry (±0.75) Distance to exit (±0.50) Consecutive confirmations
“Distance” is reported as (C − threshold). Use positive distance as “above” and negative as “below”.

6. Implementation (Python) — updated for hysteresis

import pandas as pd
import numpy as np

def robust_z(s, win=60, min_win=24):
    x = pd.to_numeric(s, errors="coerce").astype(float)
    w = max(min_win, min(win, x.dropna().size))
    med = x.rolling(w, min_periods=min_win).median()
    mad = (x - med).abs().rolling(w, min_periods=min_win).median()
    return (x - med) / (1.4826 * mad.replace(0, np.nan))

def apply_hysteresis(c, entry=0.75, exit=0.50, k=2):
    """Return regime labels with hysteresis + sustained confirmation at month-end."""
    state = None
    above, below = 0, 0
    out = []

    for v in c:
        if pd.isna(v):
            out.append(np.nan)
            continue

        # initialise
        if state is None:
            state = "Steepening" if v > entry else ("Inverted" if v < -entry else "Flat/Neutral")
            out.append(state)
            continue

        # count confirmations for exits
        above = above + 1 if v > -exit else 0   # for leaving Inverted (rise above -exit)
        below = below + 1 if v < exit else 0    # for leaving Steepening (fall below +exit)

        if state == "Inverted":
            if above >= k:
                state = "Steepening" if v > entry else "Flat/Neutral"
        elif state == "Steepening":
            if below >= k:
                state = "Inverted" if v < -entry else "Flat/Neutral"
        else:  # Flat/Neutral
            if v > entry:
                state = "Steepening"
            elif v < -entry:
                state = "Inverted"

        out.append(state)

    return pd.Series(out, index=c.index)

# Inputs are daily; resample to month-end last observation
df = df_yield_curve[["T10Y2Y","T10Y3M"]].resample("M").last()

df["Z_T10Y2Y"] = robust_z(df["T10Y2Y"])
df["Z_T10Y3M"] = robust_z(df["T10Y3M"])
df["YC_Composite"] = df[["Z_T10Y2Y","Z_T10Y3M"]].mean(axis=1)

df["YC_Regime"] = apply_hysteresis(df["YC_Composite"], entry=0.75, exit=0.50, k=2)

# Optional: distance-to-threshold fields for reporting
df["Dist_Entry_Upper"] = df["YC_Composite"] - 0.75
df["Dist_Entry_Lower"] = df["YC_Composite"] + 0.75
df["Dist_Exit_Upper"]  = df["YC_Composite"] - 0.50
df["Dist_Exit_Lower"]  = df["YC_Composite"] + 0.50

7. Model Interpretation (with academic anchors)

7.1 What the slope is proxying for

7.2 How to read the report table

7.3 Practical cautions when interpreting

8. Limitations