US Treasury Yield Curve Slope Signal
Abstract
This document formalises a rule-based signal derived from the US Treasury yield curve slope, defined as the difference between the 10-year constant-maturity Treasury rate (GS10) and the effective federal funds rate (FEDFUNDS). The slope is computed at a monthly frequency and paired with a 3-month moving-average trend to classify regimes relevant for risk assets—Bullish, Neutral, Bearish—using transparent thresholds and a simple steepening test.
1. Data
- GS10: 10-Year Treasury Constant Maturity Rate (FRED).
- FEDFUNDS: Effective Federal Funds Rate (FRED).
Both series are converted to numeric, aligned to a monthly index (MS), and forward-filled within each series prior to merging on Date. Analysis is performed on percentages (level rates).
2. Methodology
Data construction note: Constant‑maturity yields are estimates constructed by the U.S. Treasury; they can be revised and occasionally suffer missing values (e.g., holidays). The signal relies only on the slope; including the level of real yields and term premium would provide more nuance.
- Monthly alignment: reindex each series to month-start (
MS) over its span and forward-fill missing months. - Merge & spread: create a panel with columns
Value_GS10,Value_FEDFUNDS; compute the slopeYield_Spreadt = GS10t − FEDFUNDSt - Trend proxy: compute a short-horizon moving average as a steepening/flattening proxy
Yield_Spread_Trendt = Mean(Yield_Spreadt−2:t) (3 months)
- Extremeness (rolling z‑score): compute a rolling z‑score of the slope to measure statistical extremeness
Zt = (Yield_Spreadt − Mean(Yield_Spread)) / Std(Yield_Spread) (rolling window)Default window: 60 months (5 years). Shorter windows adapt faster; longer windows reduce noise.
- Classification: apply deterministic rules (for a silver-risk orientation) using level and steepening tests:
- Bullish: Yield_Spreadt ≤ 0% (curve flat/inverted).
- Bearish: Yield_Spreadt > 1.5% or a sustained steepening condition (e.g., Yield_Spreadt > Yield_Spread_Trendt for multiple months). Use the rolling z‑score to flag unusually steep configurations.
- Neutral: 0 < Yield_Spreadt ≤ 0.5% or 0.5 < Yield_Spreadt ≤ 1.5% with Yield_Spreadt ≤ Yield_Spread_Trendt.
The neutral bands recognise that mildly positive slopes can be benign unless accompanied by steepening momentum.
3. Model Interpretation
The slope (term spread) is commonly interpreted as a summary of (i) expected future short rates and (ii) compensation for duration risk (term premia). In the academic literature, the slope has been shown to contain predictive information about future economic activity and recession risk, but it is not a precise timing tool.
Interpretation heuristics
- Inversion (slope ≤ 0): historically associated with elevated recession risk at 6–18 month horizons; interpret as a risk-warning rather than a trigger for immediate action.
- Re-steepening: can occur via bull steepening (short rates falling) during deteriorating growth; persistence filters (multi-month confirmation) matter.
- Very steep slopes: often align with early-cycle expansions, but may also reflect higher term premia; consider adding real-yield levels and a term-premium estimate.
Using z‑scores and persistence
- Rolling z‑score: interpret Z-scores as “extremeness” rather than direction. Large absolute values indicate unusual configurations relative to recent history.
- Sustained steepening rule: require multiple months of steepening (and/or a z‑score threshold) before regime flips to reduce noise from data revisions and calendar effects.
Academic anchors (selected)
- Estrella & Hardouvelis (1991) — term structure as a predictor of real economic activity.
- Estrella & Mishkin (1998) — financial variables (including term spreads) as leading indicators for U.S. recessions.
- Ang, Piazzesi & Wei (2006) — yield curve and GDP growth forecasting in an arbitrage-free framework.
- Adrian, Crump & Moench (2013) — term premium extraction to separate expectations vs premia components.
4. Implementation Notes (Python)
def fetch_fred_series_df(series_id):
s = fred.get_series(series_id)
if s is None or s.empty:
return pd.DataFrame(columns=['Series_ID','title','Date','Value'])
df = s.reset_index(); df.columns = ['Date','Value']
try:
meta = fred.get_series_info(series_id); title = getattr(meta, 'title', None)
except Exception:
title = None
df['Series_ID'] = series_id; df['title'] = title
df['Date'] = pd.to_datetime(df['Date']); df['Value'] = pd.to_numeric(df['Value'], errors='coerce')
return df[['Series_ID','title','Date','Value']]
gs10_df = fetch_fred_series_df('GS10')
fedfunds_df = fetch_fred_series_df('FEDFUNDS')
for d in (gs10_df, fedfunds_df):
d['Date'] = pd.to_datetime(d['Date']); d.set_index('Date', inplace=True)
d['Value'] = pd.to_numeric(d['Value'], errors='coerce')
rng10 = pd.date_range(gs10_df.index.min(), gs10_df.index.max(), freq='MS')
rngff = pd.date_range(fedfunds_df.index.min(), fedfunds_df.index.max(), freq='MS')
GS10 = gs10_df['Value'].reindex(rng10).ffill()
FF = fedfunds_df['Value'].reindex(rngff).ffill()
merged_yield_df = pd.concat([GS10, FF], axis=1); merged_yield_df.columns = ['Value_GS10','Value_FEDFUNDS']
merged_yield_df.reset_index(inplace=True); merged_yield_df.rename(columns={'index':'Date'}, inplace=True)
merged_yield_df['Yield_Spread'] = merged_yield_df['Value_GS10'] - merged_yield_df['Value_FEDFUNDS']
merged_yield_df['Yield_Spread_Trend'] = merged_yield_df['Yield_Spread'].rolling(3).mean()
def generate_yield_curve_signal(row):
s = row['Yield_Spread']; t = row['Yield_Spread_Trend']
if pd.isna(s) or pd.isna(t):
return 'Neutral'
if s <= 0:
return 'Bullish'
if s > 1.5 or s > t:
return 'Bearish'
if 0.5 < s <= 1.5 and s <= t:
return 'Neutral'
if 0 < s <= 0.5:
return 'Neutral'
return 'Neutral'
merged_yield_df['Yield_Curve_Signal'] = merged_yield_df.apply(generate_yield_curve_signal, axis=1)
5. Sensitivity and Limitations
- Signal stability: use a rolling z‑score for extremeness and require sustained steepening/flattening before regime flips to reduce whipsaw.
- Scope: the slope alone conflates expected-policy and term‑premium channels; add real yields and an explicit term-premium estimate for more nuance.
- Window length: 2–6 months: shorter windows are more reactive but noisier.
- Thresholds: 0.5%/1.5% bands can be shifted to match asset-class betas; steepening test may trigger earlier.
- Data issues: forward-fill assumes missing months carry prior values; consider calendar-month end alignment for alternatives.
6. Reproducibility
- Capture series IDs (
GS10,FEDFUNDS), retrieval timestamps, and library versions. - Persist the merged monthly panel and the final
Yield_Curve_Signalwith timestamps. - Document any changes to window length or threshold tuning.
7. Applications
Useful as a macro overlay for duration/curve trades, as a conditioning variable in metals or cyclical-beta strategies, and for regime annotation in multi-asset dashboards.
8. References
- Estrella, A. & Hardouvelis, G. (1991). The Term Structure as a Predictor of Real Economic Activity. Journal of Finance.
- Estrella, A. & Mishkin, F.S. (1998). Predicting U.S. Recessions: Financial Variables as Leading Indicators. Review of Economics and Statistics.
- Ang, A., Piazzesi, M., & Wei, M. (2006). What Does the Yield Curve Tell Us About GDP Growth?. Journal of Econometrics.
- Adrian, T., Crump, R.K., & Moench, E. (2013). Pricing the Term Structure with Linear Regressions. Journal of Financial Economics.