US Treasury Yield Curve Slope Signal

A reproducible specification for a level–trend regime signal using GS10 and FEDFUNDS (monthly, FRED).

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

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

  1. Monthly alignment: reindex each series to month-start (MS) over its span and forward-fill missing months.
  2. Merge & spread: create a panel with columns Value_GS10, Value_FEDFUNDS; compute the slope
    Yield_Spreadt = GS10t − FEDFUNDSt
  3. Trend proxy: compute a short-horizon moving average as a steepening/flattening proxy
    Yield_Spread_Trendt = Mean(Yield_Spreadt−2:t) (3 months)
  4. 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

5. Reproducibility

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.