Explicit Slack Gap Signal

A reproducible specification for a canonical labour-market slack gap signal using UNRATE and NROU (monthly, FRED).

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

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

  1. Monthly alignment: coerce both series to monthly frequency (month-end), keeping the latest available observation for each month.
  2. Merge & gap: create a panel with columns UNRATE and NROU; compute the slack gap
    SlackGapt = UNRATEt − NROUt
  3. Momentum proxy: compute a short-horizon gap change (3 months)
    SlackGap_Change_3mt = SlackGapt − SlackGapt−3
  4. 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.

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

5. Reproducibility

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.