Explicit Slack Gap Signal
Abstract
This document formalises a rule-based signal that measures labour-market slack relative to an estimate of the economy’s noncyclical unemployment rate. The signal is defined as the difference between the US civilian unemployment rate (UNRATE) and a NAIRU proxy (NROU). The gap is computed at monthly frequency and paired with a 3-month change measure to classify regimes relevant for reaction-function modelling—Tight, Neutral, Slackening—plus a momentum label and a deterministic base confidence score.
1. Data
- UNRATE: Civilian Unemployment Rate (BLS-origin; accessed via FRED).
- NROU: Natural Rate of Unemployment (Long-Term) / Noncyclical Rate of Unemployment (CBO estimate; accessed via FRED).
Both series are converted to numeric, aligned to a monthly index, and merged on a common month-end timestamp. Analysis is performed in percentage points (pp). The canonical slack concept is the unemployment gap; supporting labour indicators (e.g., U-6, participation) are reserved for downstream confidence overrides and are not part of the core gap definition.
2. Methodology
- Monthly alignment: coerce both series to monthly frequency (month-end), keeping the latest available observation for each month.
- Merge & gap: create a panel with columns
UNRATEandNROU; compute the slack gapSlackGapt = UNRATEt − NROUt - Momentum proxy: compute a short-horizon gap change (3 months)
SlackGap_Change_3mt = SlackGapt − SlackGapt−3
- Classification: apply deterministic rules using level and momentum:
- Direction:
- Tight: SlackGapt ≤ −0.50 pp
- Slackening: SlackGapt ≥ +0.50 pp
- Neutral: otherwise
- Momentum:
- Widening: SlackGap_Change_3mt ≥ +0.20 pp
- Narrowing: SlackGap_Change_3mt ≤ −0.20 pp
- Stable: otherwise
- Confidence (base):
- Medium: if |SlackGapt| < 0.25 pp (near-neutral zone).
- High: otherwise.
- Direction:
The level thresholds define a clear “tight vs slack” separation, while the 3-month gap change captures the pace of tightening/loosening. The base confidence is intentionally conservative near the neutral zone; portfolio and predictor layers may override confidence based on corroborating labour-market signals.
3. Implementation Notes (Python)
import pandas as pd
# Assumes a configured FRED client instance named `fred`
def fetch_fred_series_df(series_id: str) -> pd.DataFrame:
"""Return DataFrame with columns [Series_ID, title, Date, Value] fetched from FRED."""
s = fred.get_series(series_id) # pandas Series indexed by Timestamp
if s is None or s.empty:
return pd.DataFrame(columns=["Series_ID","title","Date","Value"])
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["Date"] = pd.to_datetime(df["Date"])
df["Value"] = pd.to_numeric(df["Value"], errors="coerce")
return df[["Series_ID","title","Date","Value"]]
def to_monthly_wide(df: pd.DataFrame, col_name: str) -> pd.DataFrame:
"""Align to month-end timestamps and keep the last observation per month."""
out = df[["Date", "Value"]].dropna().copy()
out["Date"] = pd.to_datetime(out["Date"]).dt.to_period("M").dt.to_timestamp("M")
out = out.groupby("Date", as_index=False)["Value"].last()
return out.rename(columns={"Value": col_name})
# 1) Fetch canonical inputs
unrate_df = fetch_fred_series_df("UNRATE")
nrou_df = fetch_fred_series_df("NROU")
# 2) Monthly alignment
unrate_m = to_monthly_wide(unrate_df, "UNRATE")
nrou_m = to_monthly_wide(nrou_df, "NROU")
# 3) Merge + compute gap + momentum
slack = pd.merge(unrate_m, nrou_m, on="Date", how="inner").sort_values("Date")
slack["SlackGap_pp"] = slack["UNRATE"] - slack["NROU"]
slack["SlackGap_change_3m_pp"] = slack["SlackGap_pp"] - slack["SlackGap_pp"].shift(3)
# 4) Classify
def classify_direction(slack_gap_pp: float) -> str:
if slack_gap_pp <= -0.50:
return "TIGHT"
if slack_gap_pp >= 0.50:
return "SLACKENING"
return "NEUTRAL"
def classify_momentum(delta_3m_pp: float) -> str:
if pd.isna(delta_3m_pp):
return "STABLE"
if delta_3m_pp >= 0.20:
return "WIDENING"
if delta_3m_pp <= -0.20:
return "NARROWING"
return "STABLE"
def classify_confidence(slack_gap_pp: float) -> str:
if pd.isna(slack_gap_pp):
return "LOW"
if abs(slack_gap_pp) < 0.25:
return "MEDIUM"
return "HIGH"
slack["SlackGap_Direction"] = slack["SlackGap_pp"].apply(classify_direction)
slack["SlackGap_Momentum"] = slack["SlackGap_change_3m_pp"].apply(classify_momentum)
slack["SlackGap_Confidence"] = slack["SlackGap_pp"].apply(classify_confidence)
4. Sensitivity and Limitations
- NAIRU proxy uncertainty:
NROUis an estimate and may be revised; treat gap values near zero with lower confidence. - Threshold tuning: ±0.50 pp (direction) and ±0.20 pp (momentum) can be tuned by asset-class sensitivity or by historical policy reaction calibration.
- Timing alignment: month-end alignment is used for consistency; alternative choices (month-start or daily interpolation) should be documented if adopted.
5. Reproducibility
- Capture series IDs (
UNRATE,NROU), retrieval timestamps, and library versions. - Persist the merged monthly panel and the classified outputs (
SlackGap_Direction,SlackGap_Momentum,SlackGap_Confidence). - Document any changes to thresholds, momentum horizon, or missing-data handling.
6. Applications
Useful as a canonical slack input to a Fed reaction-function model (e.g., Taylor-style rules), as a conditioning variable in risk-asymmetry weighting (growth downside vs inflation upside), and as an input to forward projections by extrapolating short-horizon slack dynamics. Intended use: a single interpretable slack anchor, with richer labour dashboards used only for corroboration and confidence adjustment.