Federal Reserve Liquidity Composite

A reproducible specification blending M2, Fed balance‑sheet channels, and ON RRP into a monthly liquidity regime indicator.

Abstract

This document formalises a Fed liquidity composite built from four inputs: money supply (M2SL), Federal Reserve total assets (WALCL), reserve balances (WRESBAL), and overnight reverse repo usage (RRPONTSYD). Each series is aligned to month‑end, transformed into year‑over‑year and 3‑month percentage changes, and classified via symmetric thresholds. A z‑score blend across inputs yields a continuous composite that is discretised into Expansion, Neutral, or Contraction regimes. ON RRP is treated as a tightening gauge (falls are easing).

1. Data Sources

The composite blends monetary quantity (M2SL), Federal Reserve balance‑sheet scale (WALCL), reserve balances (WRESBAL), and a money‑market drain channel via the Overnight Reverse Repo facility (RRPONTSYD). Series are sourced via FRED and ultimately reported by the Federal Reserve System / Federal Reserve Bank of New York.

1.1 M2 Money Stock (M2SL)

1.2 Fed Total Assets (WALCL)

1.3 Reserve Balances (WRESBAL)

1.4 ON RRP Usage (RRPONTSYD)

All inputs are trimmed to a rolling window of the most recent N years (N = 3 by default) using the latest available date per series to focus on the current regime and reduce sensitivity to distant structural episodes.

2. Alignment & Transformations

  1. Month‑end alignment: resample each series to month‑end and select the last observation for that month.
  2. Rate of change: compute percentage changes for YoY and 3‑month windows:
    X_{YoY,t} = 100 × (X_t − X_{t-12}) / X_{t-12}
    X_{3M,t} = 100 × (X_t − X_{t-3}) / X_{t-3}
  3. Sign convention for ON RRP: interpret declines as easing; when forming signals and the composite, use −RRP_{YoY} and −RRP_{3M}.

3. Signal Classification

Each input is mapped to categorical Bullish/Neutral/Bearish using symmetric thresholds on level (YoY) and impulse (3M):

  • M2: high_yoy=6.0, low_yoy=2.0, accel_3m=1.0, decel_3m=−1.0
  • Fed Assets (WALCL): high_yoy=5.0, low_yoy=0.0, accel_3m=0.8, decel_3m=−0.5
  • Reserves (WRESBAL): high_yoy=8.0, low_yoy=1.0, accel_3m=1.5, decel_3m=−0.8
  • ON RRP (inverted): high_yoy=−5.0, low_yoy=0.0, accel_3m=−1.0, decel_3m=0.5

Rule: if YoY > high_yoy or 3M > accel_3mBullish; if YoY < low_yoy or 3M < decel_3mBearish; else Neutral. Missing inputs default to Neutral.

4. Composite Construction

  1. Standardise: form z‑scores of YoY and 3M for each input (population standard deviation):
    z(s) = (s − μ(s)) / σ(s)
  2. Per‑input score: sum level and impulse components (e.g., z(M2_{YoY}) + z(M2_{3M})).
  3. Liquidity Composite: arithmetic mean of the four per‑input scores:
    LC_t = mean( z(M2_{YoY})+z(M2_{3M}), z(FED_{YoY})+z(FED_{3M}), z(RES_{YoY})+z(RES_{3M}), z(−RRP_{YoY})+z(−RRP_{3M}) )
  4. Regime mapping: discretise using fixed cut‑offs:
    LC ≤ −0.75 ⇒ Contraction; |LC| < 0.75 ⇒ Neutral; LC ≥ 0.75 ⇒ Expansion

5. Model Interpretation (How to Read the Composite)

5.1 What the composite is (and is not)

5.2 Reading the sign and magnitude

5.3 Decomposing “what moved” (diagnostics)

For interpretation, report both the aggregate LC and the per‑input contributions:

Contribution_i,t = z(X_{i,YoY,t}) + z(X_{i,3M,t})
A “broad easing” profile shows multiple positive contributions. A “plumbing‑driven” profile often shows reserves/RRP moving without corroboration from M2.

5.4 Why PCA / dynamic weighting is a sensible extension

Equal weights are transparent but impose a fixed information structure. In macroeconometrics, principal‑components and dynamic factor methods are standard ways to summarise co‑movement across multiple series and can improve robustness when relationships evolve. A practical extension is to estimate weights (or a single factor) from the standardized panel and use the first component as LC (or as a cross‑check).

5.5 Practical interpretation rules

6. Implementation Notes (Python)

# Window: last N years
years_back = 3

def _keep_last_n_years(df, years):
    if df is None or df.empty: return df
    df = df.copy(); df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
    max_date = df['Date'].max()
    if pd.isna(max_date): return df
    cutoff = max_date - pd.DateOffset(years=years)
    return df.loc[df['Date'] >= cutoff].reset_index(drop=True)

# Align to month-end and compute changes
def align_to_month_end(df, date_col='Date', value_cols=['Value']):
    dfa = df.copy(); dfa[date_col] = pd.to_datetime(dfa[date_col])
    dfa = dfa.set_index(date_col).sort_index()
    dfa = dfa[value_cols].resample('M').last().reset_index().rename(columns={'index':'Date'})
    return dfa

import numpy as np

def pct_change(s, periods):
    out = s.pct_change(periods=periods) * 100
    return out.replace([np.inf, -np.inf], np.nan)

def add_change_columns(df, value_col='Value', prefix='X'):
    df = df.copy()
    df[f'{prefix}_YoY'] = pct_change(df[value_col], 12)
    df[f'{prefix}_3M']  = pct_change(df[value_col], 3)
    return df

def classify_signal(yoy, m3, high_yoy, low_yoy, accel_3m, decel_3m):
    if pd.isna(yoy) or pd.isna(m3): return 'Neutral'
    if (yoy > high_yoy) or (m3 > accel_3m): return 'Bullish'
    if (yoy < low_yoy) or (m3 < decel_3m): return 'Bearish'
    return 'Neutral'

def zscore(s):
    return (s - s.mean()) / s.std(ddof=0)

# After fetching: m2sl_df, fed_assets_df (WALCL), reserves_df (WRESBAL), on_rrp_df (RRPONTSYD)
m2_aligned    = align_to_month_end(m2sl_df, 'Date', ['Value'])
fed_assets_al = align_to_month_end(fed_assets_df, 'Date', ['Value'])
reserves_al   = align_to_month_end(reserves_df, 'Date', ['Value'])
on_rrp_al     = align_to_month_end(on_rrp_df, 'Date', ['Value'])

m2_aligned    = add_change_columns(m2_aligned,    'Value', 'M2')
fed_assets_al = add_change_columns(fed_assets_al, 'Value', 'FED')
reserves_al   = add_change_columns(reserves_al,   'Value', 'RES')
on_rrp_al     = add_change_columns(on_rrp_al,     'Value', 'RRP')

# Merge panel (last years_back years)
base = m2_aligned[['Date','Value','M2_YoY','M2_3M']].rename(columns={'Value':'M2_Value'})
base = base.merge(fed_assets_al.rename(columns={'Value':'FED_Value'}), on='Date', how='outer')
base = base.merge(reserves_al.rename(columns={'Value':'RES_Value'}),   on='Date', how='outer')
base = base.merge(on_rrp_al.rename(columns={'Value':'RRP_Value'}),     on='Date', how='outer')
base = base.sort_values('Date').reset_index(drop=True)
cutoff = pd.to_datetime(base['Date'].max()) - pd.DateOffset(years=years_back)
base = base.loc[base['Date'] >= cutoff].reset_index(drop=True)

# Classify each input (note inverted RRP)
M2_THR  = dict(high_yoy=6.0,  low_yoy=2.0,  accel_3m=1.0,  decel_3m=-1.0)
FED_THR = dict(high_yoy=5.0,  low_yoy=0.0,  accel_3m=0.8,  decel_3m=-0.5)
RES_THR = dict(high_yoy=8.0,  low_yoy=1.0,  accel_3m=1.5,  decel_3m=-0.8)
RRP_THR = dict(high_yoy=-5.0, low_yoy=0.0,  accel_3m=-1.0, decel_3m=0.5)

base['M2_Signal'] = base.apply(lambda r: classify_signal(r['M2_YoY'],  r['M2_3M'],  **M2_THR), axis=1)
base['FED_Signal']= base.apply(lambda r: classify_signal(r['FED_YoY'], r['FED_3M'], **FED_THR), axis=1)
base['RES_Signal']= base.apply(lambda r: classify_signal(r['RES_YoY'], r['RES_3M'], **RES_THR), axis=1)
base['RRP_Signal']= base.apply(lambda r: classify_signal(-r['RRP_YoY'] if pd.notna(r['RRP_YoY']) else np.nan,
                                                        -r['RRP_3M']  if pd.notna(r['RRP_3M'])  else np.nan,
                                                        **RRP_THR), axis=1)

# Composite (z-score blend) and regime
comp_cols = {
    'M2':  zscore(base['M2_YoY']) + zscore(base['M2_3M']),
    'FED': zscore(base['FED_YoY']) + zscore(base['FED_3M']),
    'RES': zscore(base['RES_YoY']) + zscore(base['RES_3M']),
    'RRP': zscore(-base['RRP_YoY']) + zscore(-base['RRP_3M']),
}
base['Liquidity_Composite'] = pd.DataFrame(comp_cols).mean(axis=1)
base['Liquidity_Regime'] = pd.cut(base['Liquidity_Composite'],
                                  bins=[-np.inf,-0.75,0.75,np.inf],
                                  labels=['Contraction','Neutral','Expansion'])

7. Assumptions & Limitations

8. Reproducibility

9. Applications

Suitable for macro dashboards, conditioning variables in cross‑asset models, and overlay rules for risk‑on/off tilts. Pairs naturally with price‑based signals (e.g., real‑yield or CPI–PPI divergence) for triangulation.

10. References (Selected)