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.

Revision awareness (v1.1): NROU is a model-based estimate and may be revised when the Congressional Budget Office updates assumptions on potential output, labour-force dynamics, or structural unemployment. Changes in the slack gap may therefore reflect structural reassessment rather than cyclical deterioration or improvement.

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. Model Interpretation

The explicit slack gap is a structural labour-market indicator rather than a short-term growth or recession signal. It measures whether unemployment is above or below an estimated non-cyclical benchmark consistent with stable inflation.

Academic and central-bank research shows that NAIRU-based gap estimates are informative but unreliable in real time due to revisions in estimated natural rates. Accordingly, the signal is intended as a contextual anchor for macro interpretation rather than a precise timing tool.

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.

7. Future Enhancements (Out of Scope)

The following enhancements are conceptually aligned with the signal but are not included in v1.1, as they require additional data series beyond the current scope:

These extensions would reduce reliance on a single NAIRU estimate but require explicit expansion of data inputs and governance.