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

The series is fetched from FRED with title metadata. Values are coerced to numeric and the date index is normalised to calendar month.

2. Preparation

  1. Fetch & typing: parse Date to datetime; coerce Value to numeric.
  2. Missing data policy: apply linear interpolation, then bfill/ffill to cover any edge NaNs.
  3. Sanity checks: confirm no residual nulls after 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

Classify each timestamp using level (YoY) and impulse (3M) thresholds:

  • High growth threshold: high_yoy = 6.0
  • Low growth threshold: low_yoy = 2.0
  • Acceleration threshold (3M): accelerating_3m = 1.0
  • Deceleration threshold (3M): decelerating_3m = -1.0

Rules: if M2_{YoY} > 6.0 or M2_{3M} > 1.0Bullish; if M2_{YoY} < 2.0 or M2_{3M} < -1.0Bearish; else Neutral. Missing inputs default to Neutral.

5. Implementation Notes (Python)

def fetch_fred_series_df(series_id):
    try:
        s = fred.get_series(series_id)
        if s is None or s.empty:
            return pd.DataFrame(columns=['Series_ID','title','Date','Value'])
        s.index = pd.to_datetime(s.index)
        df = s.reset_index(); df.columns = ['Date','Value']
        df['Series_ID'] = series_id
        try:
            meta = fred.get_series_info(series_id)
            df['title'] = getattr(meta, 'title', None)
        except Exception:
            df['title'] = None
        df['Value'] = pd.to_numeric(df['Value'], errors='coerce')
        return df[['Series_ID','title','Date','Value']].sort_values(['Date','Series_ID']).reset_index(drop=True)
    except Exception:
        return pd.DataFrame(columns=['Series_ID','title','Date','Value'])

# Fetch & prepare
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')

# Handle missing values
a = m2sl_df.copy()
a.interpolate(method='linear', inplace=True)
a.fillna(method='bfill', inplace=True)
a.fillna(method='ffill', inplace=True)

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

a['M2SL_3Month_Change']  = a['Value'].pct_change(3)  * 100

# Signal rules
HIGH_YOY = 6.0; LOW_YOY = 2.0; ACC3M = 1.0; DEC3M = -1.0

def generate_m2_signal(row):
    yoy, m3 = row['M2SL_YoY'], row['M2SL_3Month_Change']
    if pd.isna(yoy) or pd.isna(m3):
        return 'Neutral'
    if (yoy > HIGH_YOY) or (m3 > ACC3M):
        return 'Bullish'
    if (yoy < LOW_YOY) or (m3 < DEC3M):
        return 'Bearish'
    return 'Neutral'

a['M2_Signal'] = a.apply(generate_m2_signal, 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.