M2 Money Supply Signal

A reproducible specification for an M2‑based macro liquidity signal (FRED M2SL).

Abstract

This document formalises a rule‑based signal derived from the growth and short‑term momentum of the US M2 money stock (M2SL). We compute 12‑month and 3‑month percentage changes, then map observations to Bullish, Neutral, or Bearish regimes using transparent thresholds intended for macro‑risk applications (e.g., precious metals, cyclicals).

1. Data Sources

2. Preparation

3. Transformations

  1. Year‑over‑Year growth:
    M2_{YoY,t} = 100 \times \frac{M2_t - M2_{t-12}}{M2_{t-12}}
  2. 3‑month momentum:
    M2_{3M,t} = 100 \times \frac{M2_t - M2_{t-3}}{M2_{t-3}}

4. Signal Classification

5. Implementation Notes (Python)

# Fetch & prepare (no interpolation by default)
m2sl_df = fetch_fred_series_df('M2SL')
m2sl_df['Date']  = pd.to_datetime(m2sl_df['Date'])
m2sl_df['Value'] = pd.to_numeric(m2sl_df['Value'], errors='coerce')

a = m2sl_df.sort_values('Date').reset_index(drop=True)
a['missing_data_flag'] = a['Value'].isna()

# Transforms (YoY and 3M)
a['M2SL_YoY']           = a['Value'].pct_change(12) * 100
a['M2SL_3M_Change']     = a['Value'].pct_change(3)  * 100

# Optional: rolling z-scores (example: 120 months = 10 years)
WINDOW = 120
a['M2SL_YoY_Z'] = (a['M2SL_YoY'] - a['M2SL_YoY'].rolling(WINDOW).mean()) / a['M2SL_YoY'].rolling(WINDOW).std()
a['M2SL_3M_Z']  = (a['M2SL_3M_Change'] - a['M2SL_3M_Change'].rolling(WINDOW).mean()) / a['M2SL_3M_Change'].rolling(WINDOW).std()

# Regime rules (baseline thresholds)
HIGH_YOY = 6.0; LOW_YOY = 2.0; ACC3M = 1.0; DEC3M = -1.0

def level_regime(yoy):
    if pd.isna(yoy): return 'Neutral'
    if yoy > HIGH_YOY: return 'Bullish'
    if yoy < LOW_YOY:  return 'Bearish'
    return 'Neutral'

def momentum_regime(m3):
    if pd.isna(m3): return 'Neutral'
    if m3 > ACC3M:  return 'Bullish'
    if m3 < DEC3M:  return 'Bearish'
    return 'Neutral'

a['Level_Regime']    = a['M2SL_YoY'].apply(level_regime)
a['Momentum_Regime'] = a['M2SL_3M_Change'].apply(momentum_regime)

# Example overall aggregation: "worst-of" (risk-control)
order = {'Bearish':0,'Neutral':1,'Bullish':2}
inv   = {v:k for k,v in order.items()}
a['Overall_Regime'] = a.apply(lambda r: inv[min(order[r['Level_Regime']], order[r['Momentum_Regime']])], axis=1)

6. Assumptions & Limitations

7. Reproducibility

8. Applications

Use as a standalone macro‑liquidity indicator or as an input to broader composites (e.g., the Federal Reserve Liquidity Composite). Helpful for conditioning risk‑on/off tilts in metals and cyclical assets.

9. Model Interpretation

10. References