Housing Lead

Composite leading indicator for the housing cycle using permits, housing starts momentum, and mortgage‑rate headwinds.

Abstract

Housing Lead blends forward indicators of residential activity (building permits and housing starts) with the financing constraint captured by the 30‑year mortgage rate. Because housing series are volatile and frequently revised (including seasonal factors), v1.1 introduces explicit smoothing (moving averages and percentile ranks) and an Early Warning band designed to flag rapid deterioration before the main regime threshold is crossed.

1. Data (FRED)

PERMIT — Building Permits: New Private Housing Units

  • FRED code: PERMIT
  • Concept: Units authorized by building permits; a pipeline measure that typically leads starts and completions.
  • Frequency: Monthly; Seasonally Adjusted Annual Rate (SAAR).
  • Release & revisions: Released in the U.S. Census/HUD “New Residential Construction” report; prior months are routinely revised.
  • Key measurement issues: High month‑to‑month noise; seasonal adjustment can be difficult around turning points.

HOUST — Housing Starts: Total: New Privately Owned

  • FRED code: HOUST
  • Concept: Groundbreakings / construction starts; tends to follow permits with a lag.
  • Frequency: Monthly; SAAR.
  • Release & revisions: Same report as permits; prior months often revised alongside permits.
  • Key measurement issues: Weather sensitivity, strikes, and calendar effects can create temporary swings.

MORTGAGE30US — 30‑Year Fixed Rate Mortgage Average

  • FRED code: MORTGAGE30US
  • Concept: Financing conditions / affordability headwind; higher rates typically reduce housing demand and activity.
  • Frequency: Weekly average; resampled to month‑end (see below).
  • Release & revisions: Generally stable (minor backfill possible); holiday weeks can shift the effective timing.
  • Key measurement issues: The “month‑end” value may not represent intra‑month dynamics when rates move quickly.

Calendar alignment: All inputs are aligned to a month‑end index. Weekly mortgage data are resampled using the last observed weekly value in each month (i.e., “month‑end last”).

2. Data Quality, Revisions, and Seasonal Adjustment

3. Transformations (v1.1)

v1.1 keeps the original growth definitions but introduces smoothing and percentile ranks to reduce noise from revisions and seasonal adjustment.

  1. Level smoothing (3‑month moving average):
    PERMIT^{MA3}_t = \frac{1}{3}(PERMIT_t + PERMIT_{t-1} + PERMIT_{t-2})
    HOUST^{MA3}_t = \frac{1}{3}(HOUST_t + HOUST_{t-1} + HOUST_{t-2})
  2. Permits momentum: six‑month annualised growth (computed on smoothed levels)
    PERMIT_{6mAnn,t} = (PERMIT^{MA3}_t / PERMIT^{MA3}_{t−6})^2 − 1
  3. Starts momentum: three‑month annualised growth (computed on smoothed levels)
    HOUST_{3mAnn,t} = (HOUST^{MA3}_t / HOUST^{MA3}_{t−3})^4 − 1
  4. Mortgage headwind: month‑end level of MORTGAGE30US (higher is worse).
  5. Percentile ranks (optional companion series): For interpretability and robustness, compute rolling percentile ranks for each transformed input.
    pct(X)_t = \mathrm{PercentileRank}(X_t; X_{t-w..t})
    Typical window: w = 60 months (minimum 24). Use percentiles as a cross‑check, not as the sole trigger.

4. Normalisation

Robust z‑scores are used to standardise components while limiting the influence of outliers and level shifts.

z_{rob}(X)_t = \dfrac{X_t − \mathrm{median}(X)_{t,w}}{1.4826·\mathrm{MAD}(X)_{t,w}}
Rolling window w = 48 months (minimum 18).

5. Composite Construction

Weighting emphasises permits (most forward‑looking), then starts, with a smaller penalty from mortgage rates.

C_t = 0.45·z(PERMIT_{6mAnn,t}) + 0.35·z(HOUST_{3mAnn,t}) + 0.20·z(−MORTGAGE30US_t)

Monitoring outputs (recommended): publish the component z‑scores, the composite C_t, and the companion percentile ranks. This makes it easier to distinguish “broad housing deterioration” from “rate‑only headwinds.”

6. Regime Mapping (with Early Warning)

Expanding: C_t > 0.75;    Neutral: |C_t| ≤ 0.75;    Contracting: C_t < −0.75

Early Warning band (v1.1): flag rapid deterioration even if C_t is still above the contracting threshold. Use one of the two equivalent rules below (pick one for implementation consistency):

Early Warning is a risk flag, not a separate regime. It indicates that the signal is deteriorating quickly and revisions/noise risk is elevated.

7. Interpretation Guide

How to read the composite

  • Permits lead: A rising permits momentum z‑score is the earliest sign of a housing upswing; falling permits often precede a broader slowdown.
  • Starts confirm: Starts momentum typically follows permits and helps confirm whether authorisations are translating into activity.
  • Rates are the constraint: Mortgage rates often act as the dominant headwind/tailwind via affordability and financing conditions.
  • Composite is a balance: A “Neutral” composite can still mask offsetting forces (e.g., strong permits but high rates). Use component contributions.

Interpreting Early Warning

  • Primary use: identify turning‑point risk (downswing) sooner than threshold regimes.
  • Common pattern: permits momentum rolls over first; starts follow with a lag; rate spikes can accelerate the transition.
  • Actionability: treat as “watch‑list” status; require confirmation (e.g., persistent deterioration over 2–3 months) before high‑conviction macro calls.

Typical lead/lag intuition (high level)

In many housing‑cycle frameworks, permits tend to lead starts by months because approvals precede groundbreakings, and construction completion lags further. Mortgage rates affect demand and financing, and can influence both the level and the momentum of permits/starts through affordability and credit conditions.

8. Implementation (Python, reference)

import pandas as pd
import numpy as np

def robust_z(s, win=48, min_win=18):
    x = pd.to_numeric(s, errors="coerce").astype(float)
    med = x.rolling(win, min_periods=min_win).median()
    mad = (x - med).abs().rolling(win, min_periods=min_win).median()
    return (x - med) / (1.4826 * mad.replace(0, np.nan))

def pct_rank(s, win=60, min_win=24):
    x = pd.to_numeric(s, errors="coerce").astype(float)
    def _rank(v):
        if v.isna().all(): return np.nan
        return v.rank(pct=True).iloc[-1]
    return x.rolling(win, min_periods=min_win).apply(_rank, raw=False)

d = df_housing.copy()

# 1) Smooth levels
d["PERMIT_MA3"] = d["PERMIT"].rolling(3, min_periods=3).mean()
d["HOUST_MA3"]  = d["HOUST"].rolling(3, min_periods=3).mean()

# 2) Annualised momentum on smoothed levels
d["PERMIT_6mAnn"] = (d["PERMIT_MA3"] / d["PERMIT_MA3"].shift(6))**2 - 1.0
d["HOUST_3mAnn"]  = (d["HOUST_MA3"] / d["HOUST_MA3"].shift(3))**4 - 1.0

# 3) Standardise
d["Z_PERMIT"] = robust_z(d["PERMIT_6mAnn"])
d["Z_HOUST"]  = robust_z(d["HOUST_3mAnn"])
d["Z_MORT"]   = robust_z(-d["MORTGAGE30US"])  # month-end aligned series

# 4) Composite + contributions
d["Housing_Lead"] = (0.45*d["Z_PERMIT"] + 0.35*d["Z_HOUST"] + 0.20*d["Z_MORT"])
d["dC_3m"] = d["Housing_Lead"] - d["Housing_Lead"].shift(3)
d["pctC"]  = pct_rank(d["Housing_Lead"])

# 5) Regime + Early Warning
hi, lo = 0.75, -0.75

def _regime(v):
    if pd.isna(v): return np.nan
    return "Expanding" if v > hi else ("Contracting" if v < lo else "Neutral")

def _early_warning(C, dC_3m, pctC):
    if pd.isna(C) or pd.isna(dC_3m): return np.nan
    # choose ONE rule-set; keep it stable in production
    return bool(dC_3m < -0.60) or (pd.notna(pctC) and (pctC < 0.25) and (dC_3m < 0))

d["Housing_Regime"] = d["Housing_Lead"].apply(_regime)
d["Early_Warning"]  = [
    _early_warning(C, dc, pc) for C, dc, pc in zip(d["Housing_Lead"], d["dC_3m"], d["pctC"])
]

df_sig_housing = d

Note: keep the Early Warning rule stable (do not switch between rule-sets without documenting a version change).

9. Academic & Institutional References (interpretation anchors)

These references are used to anchor interpretation (directionality and lead/lag logic). The regime thresholds and Early Warning parameters remain heuristic and should be validated against your downstream macro model requirements.

10. Limitations