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
- 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)
- 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 steepening, i.e. Yield_Spreadt > Yield_Spread_Trendt.
- 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. 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)
4. Sensitivity and Limitations
- 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.
5. 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.
6. 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.