Overview

This canonical Paper was reconstructed from the original manuscript preserved in source/original-manuscript/. The sections below retain the manuscript’s formal argument, proofs, and simulation discussion; the executable demonstration above remains the reader-facing computational companion.

This page provides a reproducible environment for anchored Kalman filtering inside a Markov credit rating model.

  • Minimal stack: NumPy, Pandas, Matplotlib.
  • Each section explains the code, runs it, shows the result inline, and saves files under outputs/.
  • Scenarios: baseline, stress, pandemic.

0) Imports & Paths

We import our minimal stack and set a portable output directory.

Show the research code
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path
from typing import List, Dict, Tuple
import zlib  # stable, deterministic offsets (better than built-in hash)

# Output directory (portable for local/Thebe/Binder)
OUTDIR = Path("outputs")
OUTDIR.mkdir(parents=True, exist_ok=True)
OUTDIR.resolve()

1) Universe & Determinism

We define the rating states and a deterministic RNG helper.

Show the research code
RATINGS = ["A", "B", "C", "D"]
IDX = {r: i for i, r in enumerate(RATINGS)}
T_DEFAULT = 20    # quarters
N_DEFAULT = 10_000

def set_seed(seed: int) -> np.random.Generator:
    """Set global determinism and return a local RNG."""
    np.random.seed(seed)
    return np.random.default_rng(seed)

# Demo: create a reproducible RNG
rng = set_seed(42)
rng

2) Portfolio Initialisation

We sample initial ratings for N obligors from a user-supplied cross-sectional distribution.

Show the research code
def gen_initial_portfolio(N: int, pi0: np.ndarray, rng: np.random.Generator=None) -> np.ndarray:
    """Sample initial ratings for N obligors from distribution pi0."""
    if rng is None:
        rng = np.random.default_rng()
    labels = rng.choice(len(RATINGS), size=N, p=pi0)
    return labels

# Demo: quick cross-section snapshot
pi0_demo = np.array([0.50, 0.35, 0.10, 0.05], dtype=float)
labels0_demo = gen_initial_portfolio(N_DEFAULT, pi0_demo, rng=rng)
(pd.Series(labels0_demo)
   .map({i:r for r,i in IDX.items()})
   .value_counts(normalize=True)
   .rename("share")
   .to_frame()
   .sort_index())

3) Through-the-Cycle (TTC) Transition Matrix

We use a quarterly TTC matrix with D absorbing.

Show the research code
def build_P_TTC() -> np.ndarray:
    """Return the quarterly TTC transition matrix."""
    P_TTC = np.array([
        [0.975, 0.022, 0.002, 0.001],
        [0.030, 0.935, 0.030, 0.005],
        [0.010, 0.060, 0.915, 0.015],
        [0.000, 0.000, 0.000, 1.000],
    ], dtype=float)
    return P_TTC

P_TTC = build_P_TTC()
# Verify rows sum to 1
assert np.allclose(P_TTC.sum(axis=1), 1.0)
P_TTC

Visualise & save

Show the research code
fig, ax = plt.subplots(figsize=(4.5, 3.5))
im = ax.imshow(P_TTC, aspect="auto")
ax.set_xticks(range(len(RATINGS))); ax.set_xticklabels(RATINGS)
ax.set_yticks(range(len(RATINGS))); ax.set_yticklabels(RATINGS)
ax.set_title("Quarterly TTC Transition Matrix")
fig.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
fpath = OUTDIR / "ttc_matrix.png"
fig.savefig(fpath, bbox_inches="tight")
plt.show()
fpath

4) Macroeconomic Scenarios & Index

We define three stylised macro forecasts and simulate realisations by adding noise. We also build a macro index Mt=0.5z(GDPt)−0.5z(UNEMPt)M_t = 0.5\,z(\text{GDP}_t) - 0.5\,z(\text{UNEMP}_t).

Show the research code
def _baseline_paths(T: int) -> Tuple[np.ndarray, np.ndarray]:
    t = np.arange(T)
    gdp = 0.5 + 0.3*np.sin(2*np.pi*t/12)
    unemp = 5.0 + 0.2*np.cos(2*np.pi*(t+3)/10)
    return gdp, unemp

def _stress_paths(T: int) -> Tuple[np.ndarray, np.ndarray]:
    t = np.arange(T)
    gdp = 0.6 - 0.8*np.exp(-t/2.0)
    gdp[:4] -= 0.8
    unemp = 4.5 + 1.8*(1 - np.exp(-t/4.0))
    return gdp, unemp

def _pandemic_paths(T: int) -> Tuple[np.ndarray, np.ndarray]:
    t = np.arange(T)
    gdp = 0.5*np.ones(T)
    gdp[0:2] = -2.0
    gdp[2] = -0.5
    unemp = 4.2*np.ones(T)
    unemp[0:3] = 5.5
    unemp[3:6] = 5.0
    return gdp, unemp

def macro_index_from_gdp_unemp(GDP: np.ndarray, UNEMP: np.ndarray) -> np.ndarray:
    def z(x: np.ndarray) -> np.ndarray:
        mu = np.mean(x); sigma = np.std(x, ddof=0); sigma = sigma if sigma > 1e-12 else 1.0
        return (x - mu) / sigma
    return 0.5*z(GDP) - 0.5*z(UNEMP)

def gen_macro_forecasts_and_realised(scenario: str, T: int=T_DEFAULT, rng: np.random.Generator=None) -> Dict[str, np.ndarray]:
    if rng is None:
        rng = np.random.default_rng()
    sc = scenario.lower()
    if sc == "baseline":
        gdp_f, unemp_f = _baseline_paths(T)
    elif sc == "stress":
        gdp_f, unemp_f = _stress_paths(T)
    elif sc in {"pandemic", "pandemic_shock"}:
        gdp_f, unemp_f = _pandemic_paths(T)
    else:
        raise ValueError("Unknown scenario")

    # Realised = forecast + Gaussian noise
    gdp_r = gdp_f + rng.normal(0.0, 0.2, size=T)
    unemp_r = unemp_f + rng.normal(0.0, 0.2, size=T)

    # Pandemic rebound at t=3
    if sc in {"pandemic", "pandemic_shock"} and T >= 4:
        gdp_r[3] += 2.5
        unemp_r[3] -= 0.8

    M_hat = macro_index_from_gdp_unemp(gdp_f, unemp_f)
    M_real = macro_index_from_gdp_unemp(gdp_r, unemp_r)
    return {"gdp_forecast": gdp_f, "unemp_forecast": unemp_f,
            "gdp_realised": gdp_r, "unemp_realised": unemp_r,
            "M_hat": M_hat, "M_real": M_real}

Quick visual check

Show the research code
scenarios = ["baseline", "stress", "pandemic"]
fig, axes = plt.subplots(3, 2, figsize=(10, 8), sharex=True)
for i, sc in enumerate(scenarios):
    res = gen_macro_forecasts_and_realised(sc, T=20, rng=rng)
    axes[i,0].plot(res["gdp_forecast"], label="Forecast"); axes[i,0].plot(res["gdp_realised"], label="Realised", alpha=0.7)
    axes[i,0].set_title(f"{sc.title()} GDP"); axes[i,0].legend()
    axes[i,1].plot(res["unemp_forecast"], label="Forecast"); axes[i,1].plot(res["unemp_realised"], label="Realised", alpha=0.7)
    axes[i,1].set_title(f"{sc.title()} Unemployment")
plt.tight_layout()
fpath = OUTDIR / "macro_scenarios_overview.png"
plt.savefig(fpath, bbox_inches="tight"); plt.show(); fpath

5) PIT Overlay (Logit Tilt)

We tilt TTC with a β\beta matrix using W=P⊙exp⁡(β⋅Mt)W = P \odot \exp(\beta \cdot M_t), then row-normalise (and keep D absorbing).

Show the research code
def _build_betas() -> np.ndarray:
    beta = np.zeros((4,4), dtype=float)
    beta[IDX["A"], IDX["B"]] = 2.0; beta[IDX["A"], IDX["C"]] = 2.5; beta[IDX["A"], IDX["D"]] = 3.0
    beta[IDX["B"], IDX["C"]] = 1.5; beta[IDX["B"], IDX["D"]] = 2.0
    beta[IDX["C"], IDX["D"]] = 1.2
    # upgrades negative
    beta[IDX["B"], IDX["A"]] = -beta[IDX["A"], IDX["B"]]
    beta[IDX["C"], IDX["B"]] = -beta[IDX["B"], IDX["C"]]
    beta[IDX["C"], IDX["A"]] = -beta[IDX["A"], IDX["C"]]
    return beta

def pit_overlay(P_TTC: np.ndarray, M_t: float, betas: np.ndarray) -> np.ndarray:
    P = np.array(P_TTC, dtype=float)
    W = P * np.exp(betas * M_t)
    for i in range(W.shape[0]):
        if i == IDX["D"]:
            W[i, :] = 0.0; W[i, IDX["D"]] = 1.0
        else:
            s = W[i, :].sum()
            if s <= 0 or not np.isfinite(s): W[i, :] = P[i, :]
            else: W[i, :] = W[i, :] / s
    return W

# Demo: effect of negative vs positive M on an A-row slice
betas = _build_betas()
mvals = [-1.0, 0.0, 1.0]
fig, ax = plt.subplots(figsize=(6,3))
for mv in mvals:
    W = pit_overlay(P_TTC, mv, betas)
    ax.plot(W[IDX["A"], :], marker="o", label=f"M={mv:+.1f}")
ax.set_xticks(range(4)); ax.set_xticklabels(RATINGS); ax.set_title("A-row PIT probabilities vs macro index")
ax.legend(); fpath = OUTDIR / "pit_row_A_vs_M.png"; plt.savefig(fpath, bbox_inches="tight"); plt.show(); fpath

6) Kalman Filters (Naïve vs Anchored)

We compare a naïve KF yt=Mt̂y_t = M^{\hat{}}_t to an anchored KF with a second observation (anchor at 0), soft before TanchorT_{\text{anchor}} and hard after.

Show the research code
def kalman_naive(M_hat: np.ndarray, rho: float=0.90, Q: float=None, R: float=0.25) -> np.ndarray:
    T = len(M_hat)
    if Q is None: Q = 1 - rho**2
    m, P, H = 0.0, 1.0, 1.0
    m_filt = np.zeros(T)
    for t in range(T):
        m_pred = rho * m; P_pred = rho * P * rho + Q
        y = M_hat[t]; S = H * P_pred * H + R; K = P_pred * H / S
        m = m_pred + K * (y - H * m_pred); P = (1 - K * H) * P_pred
        m_filt[t] = m
    return m_filt

def kalman_anchored(M_hat: np.ndarray, T_anchor: int=20, rho: float=0.90, Q: float=None, R: float=0.25, sigma_star2_pre: float=0.25) -> np.ndarray:
    T = len(M_hat)
    if Q is None: Q = 1 - rho**2
    m, P = 0.0, 1.0
    H = np.array([[1.0],[1.0]])  # (2,1)
    m_filt = np.zeros(T)
    for t in range(T):
        m_pred = rho * m; P_pred = rho * P * rho + Q
        y = np.array([M_hat[t], 0.0])
        R_aug = np.diag([R, sigma_star2_pre]) if t < T_anchor else np.diag([R, 1e-12])
        S = H @ np.array([[P_pred]]) @ H.T + R_aug
        K = (np.array([[P_pred]]) @ H.T) @ np.linalg.solve(S, np.eye(2))
        innov = y - (H[:,0]*m_pred)
        m = m_pred + (K @ innov)[0]; P = (1 - (K @ H)[0,0]) * P_pred
        m_filt[t] = m
    return m_filt

# Demo: compare filters on baseline forecast index
demo = gen_macro_forecasts_and_realised("baseline", T=40, rng=rng)
Mhat_demo = demo["M_hat"]
m_naive = kalman_naive(Mhat_demo, rho=0.9, R=0.25)
m_anchor = kalman_anchored(Mhat_demo, T_anchor=20, rho=0.9, R=0.25, sigma_star2_pre=0.25)

fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(Mhat_demo, label="Forecast index  $M^\\hat{}_t$", alpha=0.6)
ax.plot(m_naive, label="Naïve KF  $m_t$")
ax.plot(m_anchor, label="Anchored KF  $m_t$")
ax.axvline(20, ls="--", alpha=0.5, label="Anchor switch")
ax.set_title("Naïve vs Anchored Kalman Filter (Baseline)")
ax.legend(); fpath = OUTDIR / "kf_naive_vs_anchored.png"; plt.savefig(fpath, bbox_inches="tight"); plt.show(); fpath

7) Distribution Propagation & PD Series

We propagate πt=πt−1Pt\pi_t = \pi_{t-1} P_t and extract default mass Yt=πt[D]Y_t = \pi_t[D].

Show the research code
def propagate_distribution(pi0: np.ndarray, P_ts: List[np.ndarray]) -> np.ndarray:
    T = len(P_ts); pi = np.zeros((T+1, len(RATINGS))); pi[0, :] = pi0; current = pi0.copy()
    for t in range(T):
        current = current @ P_ts[t]; pi[t+1, :] = current
    return pi

def compute_pd_series(pi_ts: np.ndarray) -> np.ndarray:
    return pi_ts[:, IDX["D"]]

# Demo: one-quarter step under TTC from the earlier labels cross-section
P1 = [P_TTC.copy()]
pi_one = propagate_distribution(pi0_demo, P1)
pd.Series(pi_one[-1], index=RATINGS).rename("π_1")

8) Experiment Runner (Scenario × Method)

We wrap the pipeline and save CSVs + a macro figure per run.

Show the research code
def _stable_offset(scenario: str, method: str, mod: int = 10_000) -> int:
    key = f"{scenario}|{method}".encode(); return zlib.crc32(key) % mod

def run_experiment(scenario: str, method: str, N: int=N_DEFAULT, T: int=T_DEFAULT, seed: int=12345) -> Dict[str, object]:
    rng = set_seed(seed + _stable_offset(scenario, method))
    pi0 = np.array([0.45, 0.40, 0.15, 0.00], dtype=float)
    labels0 = gen_initial_portfolio(N, pi0, rng=rng)
    P_TTC = build_P_TTC(); betas = _build_betas()

    macro = gen_macro_forecasts_and_realised(scenario, T=T, rng=rng)
    M_hat, M_real = macro["M_hat"], macro["M_real"]

    if method == "raw":
        M_est = M_real.copy()
    elif method == "naive":
        M_est = kalman_naive(M_hat, rho=0.90, Q=None, R=0.25)
    elif method == "anchored":
        M_est = kalman_anchored(M_hat, T_anchor=T, rho=0.90, Q=None, R=0.25, sigma_star2_pre=0.25)
    else:
        raise ValueError("Unknown method")

    P_ts = [pit_overlay(P_TTC, float(M_est[t]), betas) for t in range(T)]
    pi_ts = propagate_distribution(pi0, P_ts); Y_t = compute_pd_series(pi_ts)

    P_neutral = [P_TTC.copy() for _ in range(T)]
    pi_ttc = propagate_distribution(pi0, P_neutral); Y_ttc = compute_pd_series(pi_ttc)

    # Save transition matrices
    tm_rows = []
    for t in range(T):
        for i, ri in enumerate(RATINGS):
            for j, rj in enumerate(RATINGS):
                tm_rows.append({"t": t+1, "from": ri, "to": rj, "P": P_ts[t][i,j]})
    df_tm = pd.DataFrame(tm_rows); f_tm = OUTDIR / f"transition_matrices_{scenario}_{method}.csv"; df_tm.to_csv(f_tm, index=False)

    # Save macro paths
    df_macro = pd.DataFrame({"t": np.arange(1, T+1), "M_forecast": M_hat, "M_realised": M_real, "M_estimate": M_est})
    f_macro = OUTDIR / f"macro_paths_{scenario}_{method}.csv"; df_macro.to_csv(f_macro, index=False)

    # Save PD term structures
    df_pd = pd.DataFrame({"t": np.arange(0, T+1), "Y_t": Y_t, "Y_ttc": Y_ttc})
    f_pd = OUTDIR / f"pd_term_structures_{scenario}_{method}.csv"; df_pd.to_csv(f_pd, index=False)

    # Figure: macro filter (save + show)
    fig = plt.figure(figsize=(7,3.5))
    tt = np.arange(1, T+1)
    plt.plot(tt, M_hat, label="Forecast (M̂)"); plt.plot(tt, M_real, label="Realised M"); plt.plot(tt, M_est, label=f"Estimate: {method}")
    plt.axhline(0.0, color="k", lw=0.7, alpha=0.4); plt.xlabel("Quarter"); plt.ylabel("Macro index (z)"); plt.title(f"Macro filter: {scenario} × {method}"); plt.legend()
    f_fig = OUTDIR / f"macro_filter_{scenario}_{method}.png"; fig.savefig(f_fig, bbox_inches="tight"); plt.show()

    return {"labels0": labels0, "P_ts": P_ts, "pi_ts": pi_ts, "Y_t": Y_t, "Y_ttc": Y_ttc,
            "M_hat": M_hat, "M_real": M_real, "M_est": M_est,
            "files": {"transition_matrices": str(f_tm), "macro_paths": str(f_macro), "pd_term_structures": str(f_pd), "macro_figure": str(f_fig)}}

9) PD Bands & Metrics

Helper to draw PD bands for a fixed method and compute cross-scenario variance.

Show the research code
def variance_across_scenarios(Y_by_scn: Dict[str, np.ndarray]) -> pd.DataFrame:
    Ys = np.stack([v for v in Y_by_scn.values()], axis=0)
    return pd.DataFrame({"t": np.arange(Ys.shape[1]), "var_Y_t": Ys.var(axis=0, ddof=0)})

Show PD bands inline + save

Show the research code
# Run three scenarios quickly for "anchored"
scenarios = ["baseline", "stress", "pandemic"]; method = "anchored"; T_demo = 20
results = {sc: run_experiment(sc, method=method, T=T_demo, seed=2025) for sc in scenarios}

tgrid = np.arange(0, T_demo+1)
fig, ax = plt.subplots(figsize=(8, 4))
any_key = next(iter(results)); Y_ttc = results[any_key]["Y_ttc"]; ax.plot(tgrid, Y_ttc, label="TTC baseline")
for scn, res in results.items(): ax.plot(tgrid, res["Y_t"], label=scn)
ax.set_xlabel("Quarter"); ax.set_ylabel("Cumulative PD (π_t[D])"); ax.set_title(f"PD term-structure bands — {method}"); ax.legend()
f_bands = OUTDIR / f"pd_bands_{method}.png"; fig.savefig(f_bands, bbox_inches="tight"); plt.show(); f_bands

Variance across scenarios (display)

Show the research code
var_df = variance_across_scenarios({sc: res["Y_t"] for sc, res in results.items()})
var_df.head(10)

10) Main — Full Grid (Scenarios × Methods)

We run all scenarios × methods, save summary tables, and print sample heads.

Show the research code
def main():
    seed = 20250821
    rng = set_seed(seed)
    scenarios = ["baseline", "stress", "pandemic"]; methods = ["raw", "naive", "anchored"]
    pi0 = np.array([0.45, 0.40, 0.15, 0.00], dtype=float)
    P_TTC = build_P_TTC(); betas = _build_betas()

    all_results: Dict[str, Dict[str, object]] = {m:{} for m in methods}
    file_registry = []

    # Run experiments
    for m in methods:
        for scn in scenarios:
            res = run_experiment(scn, m, N=N_DEFAULT, T=T_DEFAULT, seed=seed)
            all_results[m][scn] = res
            files = res["files"]
            file_registry.append({"scenario": scn, "method": m, **files})

    # PD bands per method + variance table
    var_tables = []; pd_band_files = []
    for m in methods:
        # Bands (inline + save)
        tgrid = np.arange(0, T_DEFAULT+1)
        fig, ax = plt.subplots(figsize=(8, 4))
        any_key = next(iter(all_results[m])); Y_ttc = all_results[m][any_key]["Y_ttc"]; ax.plot(tgrid, Y_ttc, label="TTC baseline")
        for scn, res in all_results[m].items(): ax.plot(tgrid, res["Y_t"], label=scn)
        ax.set_xlabel("Quarter"); ax.set_ylabel("Cumulative PD (π_t[D])"); ax.set_title(f"PD bands — {m}"); ax.legend()
        f = OUTDIR / f"pd_bands_{m}.png"; fig.savefig(f, bbox_inches="tight"); plt.show()
        pd_band_files.append({"method": m, "pd_bands_figure": str(f)})

        # Variance
        Y_by_scn = {scn: all_results[m][scn]["Y_t"] for scn in scenarios}
        df_var = variance_across_scenarios(Y_by_scn); df_var.insert(0, "method", m); var_tables.append(df_var)

    df_var_all = pd.concat(var_tables, ignore_index=True); f_var = OUTDIR / "variance_Y_across_scenarios.csv"; df_var_all.to_csv(f_var, index=False)

    # Monte Carlo lifetime loss volatility per scenario × method (summary only)
    mc_rows = []
    for m in methods:
        for scn in scenarios:
            base_forecasts = gen_macro_forecasts_and_realised(scn, T=T_DEFAULT, rng=rng)
            # small MC for speed in-page; you can raise n_rep
            def mc_once(n_rep=200, seed_off=0):
                rng_local = np.random.default_rng(seed + zlib.crc32(f"{scn}|{m}|mc".encode()) % 10_000 + seed_off)
                T = len(base_forecasts["M_hat"]); YT = np.zeros(n_rep)
                for r in range(n_rep):
                    gdp_f = base_forecasts["gdp_forecast"]; un_f = base_forecasts["unemp_forecast"]
                    gdp_r = gdp_f + rng_local.normal(0.0, 0.2, size=T); un_r = un_f + rng_local.normal(0.0, 0.2, size=T)
                    if scn.lower().startswith("pandemic") and T >= 4: gdp_r[3] += 2.5; un_r[3] -= 0.8
                    M_hat = macro_index_from_gdp_unemp(gdp_f, un_f); M_real = macro_index_from_gdp_unemp(gdp_r, un_r)
                    if m == "raw": M_eff = M_real
                    elif m == "naive": M_eff = kalman_naive(M_hat, rho=0.90, Q=None, R=0.25)
                    elif m == "anchored": M_eff = kalman_anchored(M_hat, T_anchor=T, rho=0.90, Q=None, R=0.25, sigma_star2_pre=0.25)
                    else: raise ValueError("Unknown method")
                    P_ts = [pit_overlay(P_TTC, float(M_eff[t]), _build_betas()) for t in range(T)]
                    pi_ts = propagate_distribution(pi0, P_ts); YT[r] = pi_ts[-1, IDX["D"]]
                return {"mean": float(YT.mean()), "std": float(YT.std(ddof=0))}
            mc = mc_once(n_rep=200)
            mc_rows.append({"scenario": scn, "method": m, "YT_mean": mc["mean"], "YT_std": mc["std"]})
    df_mc = pd.DataFrame(mc_rows); f_mc = OUTDIR / "lifetime_loss_volatility_mc.csv"; df_mc.to_csv(f_mc, index=False)

    # Summary file registry
    for item in pd_band_files:
        file_registry.append({"scenario":"all","method":item["method"],"macro_paths":"","transition_matrices":"","pd_term_structures":"","macro_figure":"", "pd_bands_figure":item["pd_bands_figure"]})
    df_registry = pd.DataFrame(file_registry); f_registry = OUTDIR / "file_registry.csv"; df_registry.to_csv(f_registry, index=False)

    return {"registry": str(f_registry), "variance_table": str(f_var), "mc_table": str(f_mc),
            "example_macro": all_results["naive"]["baseline"]["files"]["macro_paths"],
            "example_pd": all_results["anchored"]["pandemic"]["files"]["pd_term_structures"]}

paths = main()

print("Saved files (selected):")
for k, v in paths.items(): print(f" - {k}: {v}")

print("\nExample heads:")
df_macro = pd.read_csv(paths["example_macro"]); df_pd = pd.read_csv(paths["example_pd"])
print("\nmacro_paths (baseline × naive) head:"); print(df_macro.head(6))
print("\npd_term_structures (pandemic × anchored) head:"); print(df_pd.head(6))

11) Appendix Figures — MC Distributions & Summaries

We ensure per-scenario MC sample CSVs exist, then create four multi-panel figures. Every figure is shown and saved.

Show the research code
def _stable_offset_key(*parts: str, mod: int = 10_000) -> int:
    key = "|".join(parts).encode(); return zlib.crc32(key) % mod

def _ensure_mc_samples_csvs(
    outdir: Path,
    scenarios = ("baseline", "stress", "pandemic"),
    methods = ("raw", "naive", "anchored"),
    n_rep: int = 200,
    seed_base: int = 2025,
    T: int = T_DEFAULT,
    pi0: np.ndarray = np.array([0.45, 0.40, 0.15, 0.00], dtype=float),
):
    outdir.mkdir(parents=True, exist_ok=True)
    P_TTC_local = build_P_TTC(); betas = _build_betas()
    for scn in scenarios:
        f_samples = outdir / f"mc_samples_YT_{scn}.csv"
        if f_samples.exists(): continue
        rng_base = set_seed(seed_base + _stable_offset_key("base", scn))
        base_forecasts = gen_macro_forecasts_and_realised(scn, T=T, rng=rng_base)
        data: Dict[str, np.ndarray] = {}
        for m in methods:
            mc = []; rng_mc = np.random.default_rng(seed_base + _stable_offset_key("mc", scn, m))
            for _ in range(n_rep):
                gdp_f = base_forecasts["gdp_forecast"]; un_f = base_forecasts["unemp_forecast"]
                gdp_r = gdp_f + rng_mc.normal(0.0, 0.2, size=T); un_r = un_f + rng_mc.normal(0.0, 0.2, size=T)
                if scn.lower().startswith("pandemic") and T >= 4: gdp_r[3] += 2.5; un_r[3] -= 0.8
                M_hat = macro_index_from_gdp_unemp(gdp_f, un_f); M_real = macro_index_from_gdp_unemp(gdp_r, un_r)
                if m == "raw": M_eff = M_real
                elif m == "naive": M_eff = kalman_naive(M_hat, rho=0.90, Q=None, R=0.25)
                elif m == "anchored": M_eff = kalman_anchored(M_hat, T_anchor=T, rho=0.90, Q=None, R=0.25, sigma_star2_pre=0.25)
                else: raise ValueError("Unknown method")
                P_ts = [pit_overlay(P_TTC_local, float(M_eff[t]), betas) for t in range(T)]
                pi_ts = propagate_distribution(pi0, P_ts)
                mc.append(pi_ts[-1, IDX["D"]])
            data[m] = np.array(mc, dtype=float)
        pd.DataFrame(data).to_csv(f_samples, index=False)

# Ensure samples exist
_ensure_mc_samples_csvs(OUTDIR)

11.1 Notched boxplots

Show the research code
scenarios = ["baseline", "stress", "pandemic"]; methods = ["raw", "naive", "anchored"]
fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True)
for ax, scn in zip(axes, scenarios):
    df = pd.read_csv(OUTDIR / f"mc_samples_YT_{scn}.csv")
    ax.boxplot([df[m] for m in methods], labels=methods, notch=True, showfliers=False)
    ax.set_title(scn.capitalize()); ax.set_xlabel("Method")
axes[0].set_ylabel("Lifetime PD at T")
fig.suptitle("Notched boxplots — Lifetime PD distributions (200 MC reps)", y=1.02)
f_box = OUTDIR / "combined_notched_boxplots_all.png"; fig.savefig(f_box, bbox_inches="tight"); plt.show(); f_box

11.2 Violin plots

Show the research code
fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True)
for ax, scn in zip(axes, scenarios):
    df = pd.read_csv(OUTDIR / f"mc_samples_YT_{scn}.csv")
    ax.violinplot([df[m] for m in methods], showmeans=True, showmedians=True)
    ax.set_xticks(range(1, len(methods)+1)); ax.set_xticklabels(methods)
    ax.set_title(scn.capitalize()); ax.set_xlabel("Method")
axes[0].set_ylabel("Lifetime PD at T")
fig.suptitle("Violin plots — Lifetime PD distributions (200 MC reps)", y=1.02)
f_viol = OUTDIR / "combined_violins_all.png"; fig.savefig(f_viol, bbox_inches="tight"); plt.show(); f_viol

11.3 Jittered box+dot (deterministic jitter)

Show the research code
rng_jitter = np.random.default_rng(12345)
fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True)
for ax, scn in zip(axes, scenarios):
    df = pd.read_csv(OUTDIR / f"mc_samples_YT_{scn}.csv")
    ax.boxplot([df[m] for m in methods], labels=methods, showfliers=False)
    for i, m in enumerate(methods, start=1):
        y = df[m].values; x = rng_jitter.normal(i, 0.06, size=len(y))
        ax.plot(x, y, linestyle="", marker="o", alpha=0.35, markersize=3)
    ax.set_title(scn.capitalize()); ax.set_xlabel("Method")
axes[0].set_ylabel("Lifetime PD at T")
fig.suptitle("Jittered box+dot — Lifetime PD distributions (200 MC reps)", y=1.02)
f_boxdot = OUTDIR / "combined_boxdot_all.png"; fig.savefig(f_boxdot, bbox_inches="tight"); plt.show(); f_boxdot

11.4 Mean ± std bars from summary table

Show the research code
df_mc = pd.read_csv(OUTDIR / "lifetime_loss_volatility_mc.csv")
fig, axes = plt.subplots(1, 3, figsize=(15, 4), sharey=True)
for ax, scn in zip(axes, scenarios):
    sub = df_mc[df_mc["scenario"] == scn].copy()
    order = ["raw", "naive", "anchored"]
    sub["method"] = pd.Categorical(sub["method"], categories=order, ordered=True)
    sub = sub.sort_values("method")
    x = np.arange(len(order))
    ax.bar(x, sub["YT_mean"].values, yerr=sub["YT_std"].values, capsize=4)
    ax.set_xticks(x); ax.set_xticklabels(order)
    ax.set_title(scn.capitalize()); ax.set_xlabel("Method")
axes[0].set_ylabel("Lifetime PD at T (mean ± std)")
fig.suptitle("Lifetime PD (200 MC reps) — mean with volatility", y=1.02)
f_bars = OUTDIR / "combined_mc_lifetime_pd_bars_all.png"; fig.savefig(f_bars, bbox_inches="tight"); plt.show(); f_bars

11.5 Summary of saved figures

Show the research code
print("Saved appendix figures:")
print(" - Notched boxplots :", f_box)
print(" - Violin plots     :", f_viol)
print(" - Jittered box+dot :", f_boxdot)
print(" - Mean±Std bars    :", f_bars)

Original manuscript reconstruction

The following open section reconstructs the original manuscript’s formal argument, proofs, and simulation discussion. The source LaTeX remains preserved verbatim under source/original-manuscript/.

Introduction

Forward–looking estimation of lifetime probabilities of default (PDs) is a core requirement of IFRS 9 (Board 2014) and the Current Expected Credit Loss (CECL) framework (Board 2016). In practice, lifetime PDs are obtained by propagating a point–in–time (PIT) transition matrix over multiple quarterly horizons, starting from the current rating distribution (Banking Supervision 2000; Tasche 2019). Transition probabilities are conditioned on macroeconomic variables and calibrated to scenario forecasts provided by risk or economic research teams (Bank 2017). Since forecasts are typically available only over limited horizons (e.g. five years), models assume a neutral long–run environment beyond this window and let the PD term structure converge towards through–the–cycle (TTC) behaviour (Authority 2017; Banking Supervision 2015).

Macroeconomic drivers such as GDP growth or unemployment are inherently stochastic and subject to significant forecast error (Croushore and Stark 2001). Scenario projections provide plausible narratives but cannot anticipate shocks or structural breaks (Reifschneider and Tulip 2017). The COVID–19 crisis illustrates this challenge: forecasts projected a protracted downturn, while realised conditions recovered much faster (Economic Co-operation and Development 2020; Fund 2021). When such forecasts are fed into transition matrices, their errors embed directly in the PD propagation.

The result is that long–horizon PDs inherit the full uncertainty of the scenario. Forecast errors are recycled quarter after quarter, producing volatility and drift in the term structure. This effect is most pronounced during stress, when forecasts deviate strongly from realised macro paths. The outcome is excessive volatility in lifetime PDs, complicating capital planning and undermining the interpretability of credit loss estimates (European Banking Authority 2020; Bank for International Settlements 2021).

Several methods attempt to mitigate this issue by smoothing macro inputs. Scenario averaging (Borio et al. 2014; Authority 2017), moving averages (Orphanides and Norden 2002; Hamilton 1994), and exponential smoothing (Kreinin and Sidelnikova 2001) reduce short–term noise but do not address the dynamic propagation of forecast error. In parallel, credit transition modelling has drawn on latent factor structures (Jarrow et al. 1997; Duffie and Singleton 1999; Lando 2004) and Bayesian smoothing of transition matrices (Kavvathas 2001; Christensen et al. 2004). These methods improve local estimation but do not resolve the long–horizon instability of PD propagation under forecast uncertainty.

This paper makes two contributions. First, it establishes a rigorous instability result for multi–period propagation of PIT transition matrices under forecast uncertainty, showing that conventional schemes do not converge. Second, it introduces an anchored Kalman filter that incorporates a neutral long–run macro state into the update step. Anchoring prevents forecast error recycling and yields asymptotically stochastic stability: the estimation error remains mean–square bounded, and the PD term structure converges in probability to a meaningful TTC limit. The framework therefore links control–theoretic recursive estimation (Kalman 1960; Maybeck 1979; Simon 2006) with regulatory credit risk modelling under IFRS 9 and CECL.

The remainder of the paper is organised as follows. Section 2 formulates lifetime PD estimation in a Markovian state–space framework and demonstrates instability under standard macro–driven propagation. Section 3 introduces a Kalman filtering representation of the macroeconomic state and shows that naïve filtering does not eliminate error recycling. Section 4 develops the anchored Kalman filter, derives its error dynamics, and proves asymptotic stochastic stability. Section 5 reports simulation results on a synthetic corporate portfolio. Section 6 concludes and outlines directions for future research.

Problem Formulation

We now formulate the lifetime PD estimation problem as a discrete–time state–space system. The starting point is a TTC transition matrix describing long–run rating migrations. A macroeconomic driver is then introduced to generate PIT matrices that condition on forecasted scenarios. Propagating the rating distribution forward with these PIT matrices produces a lifetime PD term structure.

While this setup reflects current industry and regulatory practice, it embeds macroeconomic forecast errors into every propagation step. Section [sec:instable] shows that such errors accumulate and prevent convergence, leading to unstable lifetime PD estimates. This motivates the filtering approach developed in subsequent sections.

TTC Transition Matrix

The TTC transition matrix summarises long–run average rating behaviour. It is typically estimated from historical observations using the cohort, or counting, method. This is the most widely used estimator in both academia and practice (Nickell et al. 2000; Lando and Skødeberg 2002). Alternative approaches include maximum likelihood and continuous–time Markov generators (Kavvathas 2001; Israel et al. 2001; Bluhm et al. 2016), as well as Bayesian or smoothing techniques that provide confidence intervals and regularisation (Christensen et al. 2004). For the purpose of this paper we adopt the cohort method as baseline.

Formally, let X_t \in \{1,\dots,K\} denote the discrete rating of an obligor at quarter t, where K includes the default state. The TTC matrix P^{\mathrm{TTC}} \in \mathbb{R}^{K \times K} is row–stochastic with entries

p^{\mathrm{TTC}}_{ij}
=\frac{N_{ij}}{\sum_{j=1}^{K} N_{ij}},
\qquad
i,j \in \{1,\dots,K\},

where N_{ij} is the number of obligors that migrated from state i to j during the sample period. By construction, p^{\mathrm{TTC}}_{ij} \ge 0 and \sum_{j=1}^{K} p^{\mathrm{TTC}}_{ij} = 1 for all i.

PIT Adjustment and Macroeconomic Driver

The TTC matrix reflects long–run average behaviour but does not incorporate current or expected macroeconomic conditions. To obtain a PIT transition matrix, a macroeconomic adjustment is applied to P^{\mathrm{TTC}}. This “macro overlay” links transition intensities to observable covariates such as GDP growth or unemployment (Bangia et al. 2002; Pesaran et al. 2006; Duffie et al. 2007). Regulatory guidance under Basel and IFRS 9 frameworks also recognises the need for PIT calibration and the use of macroeconomic adjustments (Banking Supervision 2005; Authority 2017).

Formally, let (M_t)_{t\ge0} denote the stochastic macroeconomic state (e.g. GDP growth, unemployment) and \widehat M_t its forecast at time t. The PIT transition matrix at quarter t is defined by

P_t \;=\; G\!\left(P^{\mathrm{TTC}},\, \widehat M_t\right),

where G: \mathbb{R}^{K \times K} \times \mathbb{R} \to \mathbb{R}^{K \times K} maps the TTC matrix and macro state into migration probabilities. A common specification is a logit–style overlay,

p_{ij,t} \;\propto\; p^{\mathrm{TTC}}_{ij}\,\exp\!\big(\beta_{ij} \widehat M_t\big),
\qquad i,j \in \{1,\dots,K\},

followed by row normalisation. The sensitivity coefficients \beta_{ij} are estimated from historical rating transitions and macro variables, typically via multinomial logit or proportional hazard regressions (Bangia et al. 2002; Pesaran et al. 2006). Depending on data availability, the estimation may be conducted at the matrix level or directly on obligor–level events. The resulting coefficients quantify how strongly each migration probability responds to the macro factor and are used to adjust the TTC matrix forward under the forecasted macro path.

Lifetime Propagation and State–Space Representation

Starting from an initial rating distribution \pi_0 \in \mathbb{R}^{1\times K}, the portfolio evolves quarter by quarter under the PIT transition matrices. At horizon t+1,

\pi_{t+1} \;=\; \pi_t P_t,
\qquad t=0,1,\dots,T-1,

where P_t = G\!\left(P^{\mathrm{TTC}},\, \widehat M_t\right) depends on the macroeconomic forecast \widehat M_t. The sequence (\pi_t)_{t=0}^T is therefore a stochastic dynamical system driven by the forecasted macro path.

Define Y_t = \pi_t e_K as the probability of default by time t, where e_K denotes the unit vector of the absorbing default state. The lifetime PD term structure is then given by the sequence (Y_t)_{t=1}^T, which inherits its dynamics from the entire forecast path (\widehat M_s)_{s=0}^{t-1}.

Forecast Error and Instability of Lifetime Propagation

In practice, the true macroeconomic state M_t is not observed; only its forecast \widehat M_t is available. Define the forecast error

\delta_t \;=\; \widehat M_t - M_t, \qquad t=0,1,\dots,T-1.

Substituting P_t=G\!\big(P^{\mathrm{TTC}},\,M_t+\delta_t\big) into the rating propagation yields

\pi_{t+1} \;=\; \pi_t \, G\!\big(P^{\mathrm{TTC}},\,M_t+\delta_t\big).

The macro process (M_t) is often modelled as mean–reverting, consistent with standard autoregressive specifications in econometrics (Hamilton 1994; Nelson and Plosser 1982) and equilibrium term–structure models such as Vasicek (Vasicek 1977). Even under such mean–reversion, the repeated injection of \delta_t into P_t can induce substantial variability in (\pi_t) and, consequently, in the lifetime PD sequence (Y_t)_t. Because \delta_t enters the transition matrix at every step, a non–vanishing error sequence (\delta_t) accumulates over the lifetime horizon and prevents convergence to a stable limit.

We formalise this intuition in three parts: an elementary instability lemma, a rigorous non–convergence result, and a deviation bound that quantifies accumulation. Let

\varphi(\pi,m) \;:=\; \pi \, G\!\big(P^{\mathrm{TTC}},\,m\big)\, e_K,

so that Y_t=\varphi(\pi_t,M_t) denotes the error–free default probability at horizon t. Our reasoning is closely related to results on random products of stochastic matrices (Seneta 2006) and to the propagation of forecast error in macroeconomic time series (Hamilton 1994). While sensitivity and non–convergence phenomena are known in stochastic matrix theory and stochastic approximation (Seneta 2006; Mitrophanov 2005; Kushner and Yin 2003), to the best of our knowledge these instability properties have not been established for lifetime PD propagation under macroeconomic forecasting.

Appendix [app:proofs] presents the elementary instability result (Lemma 1). The main implication for lifetime PDs is summarised next.

Proposition 1 (Lifetime PD non–convergence under persistent forecast error). Let P^{\mathrm{TTC}} be row–stochastic and let G(\cdot,m) be row–stochastic and C^1 in m. Assume: (i) uniform, non–degenerate macro sensitivity of Y_t=\varphi(\pi_t,m); (ii) i.i.d. forecast errors \delta_t with \mathbb{P}(|\delta_t|\ge \varepsilon)=p>0; (iii) the error–free path Y_t^\circ=\varphi(\pi_t^\circ,M_t) converges. Then (Y_t) does not converge in probability.

Proof. Sketch. By Borel–Cantelli, |\delta_t|\ge \varepsilon occurs infinitely often with positive probability. Uniform sensitivity implies a fixed minimum deviation from the error–free sequence Y_t^\circ, so the distance to any candidate limit cannot vanish. Full details are given in Appendix [app:proofs]. ◻

A linear accumulation bound for deviations e_t=\|\pi_t-\pi_t^\circ\|_1 is provided in Corollary 1 (Appendix), which quantifies how forecast error adds up over time.

Remark. Remark 1 (On assumptions (A1) and (A3)). Assumption (A1) requires uniform non–degeneracy of the macro sensitivity of default probabilities. It holds, for example, if \partial_m \undefined G exists and \inf_{\pi,m}\big|\partial_m\phi(\pi,m)\big|>0 over the relevant domain. Assumption (A3) is standard when the error–free macro path is bounded or convergent and \undefined G induces an ergodic product of stochastic matrices. If \delta_t\to 0 almost surely, the non–convergence conclusion may fail. Section [sec:anchored_obs] addresses this by replacing raw forecasts with an anchored filter, which enforces \delta_t\to 0 and restores stability.

The results in this subsection show that conventional macro–driven propagation does not yield a stable lifetime PD estimate: forecast errors enter the dynamics at each step, accumulate, and preclude convergence. To restore stability, a filtering mechanism is required that explicitly controls the impact of forecast uncertainty. Section 3 therefore introduces a Kalman filtering representation of the macro state and examines whether such an approach can mitigate the instability identified here.

Kalman Filtering for Lifetime PD Models

This section develops a Kalman filtering framework for lifetime PD propagation. We first recall the essence of the Kalman filter as a recursive state estimator. We then discuss alternative design choices for applying the filter either to the macroeconomic driver or directly to the PD dynamics. A state–space representation of the macro driver is introduced, and the resulting naïve filter is analysed. The analysis shows that while Kalman filtering dampens short–run forecast noise, it does not resolve the structural instability identified in Section [sec:instable].

Essence of the Kalman Filter

The Kalman filter is the canonical recursive estimator for linear stochastic state–space systems (Kalman 1960). Consider latent state dynamics

x_{t+1} = A x_t + w_t, 
\qquad w_t \sim N(0,Q),

and a measurement equation

y_t = H x_t + v_t, 
\qquad v_t \sim N(0,R),

with A the transition matrix, H the observation matrix, and Q,R the noise covariances. The filter recursively updates the posterior mean and covariance of x_t given all past observations, alternating between prediction and update steps. Formally,

\mu_t = \mu_{t|t-1} + K_t \big(y_t - H \mu_{t|t-1}\big),
\qquad
\Sigma_t = (I - K_t H)\Sigma_{t|t-1},

where K_t is the Kalman gain balancing prior uncertainty against measurement noise. The recursion ensures that forecast error is not amplified over time but is damped according to the reliability of new signals.

Since its introduction (Kalman 1960), the filter has been applied widely in navigation, control, and econometrics (Hamilton 1994; Kushner and Yin 2003). In the present context, its relevance lies in treating macroeconomic forecasts as noisy signals of an unobserved “true” macro state. By filtering these signals, transition–matrix dynamics can be driven by smoothed state estimates rather than raw forecasts, a practice increasingly emphasised in regulatory discussions of forward–looking credit risk models (Authority 2017; Banking Supervision 2005).

The Kalman filter can be incorporated into lifetime PD modelling in different ways. One option is to filter the macroeconomic state before it enters the transition matrix. Another is to filter the transition dynamics or PD sequence directly. Between these extremes, hybrid constructions are possible. This subsection compares the alternatives and explains the rationale for adopting macro–state filtering as baseline.

Filtering the Macroeconomic State.

In this design the latent macro driver M_t is modelled explicitly in a linear–Gaussian state–space form. The forecast \widehat M_t is treated as a noisy observation,

\begin{aligned}
\text{State:}& \quad M_{t+1} = A M_t + w_t, \quad w_t\simN(0,Q),\\
\text{Obs.:}& \quad \widehat M_t = H M_t + v_t, \quad v_t\simN(0,R),\\
\text{PD link:}& \quad P_t = \undefined G\!\big(P^{\mathrm{TTC}},\mu_t\big),\quad 
\mu_t \;=\;\mathbb E[M_t|\widehat M_{0:t}].
\end{aligned}

The PIT matrix is thus driven by the filtered estimate \mu_t rather than the raw forecast.

Pros:

  • Preserves the TTC/absorbing structure of the transition matrix by construction.

  • Conceptually transparent: the filter acts on macro noise, not on rating dynamics.

  • Provides a natural entry point for introducing a neutral long–term anchor and proving stability.

Cons:

  • Relies on correct specification of the macro–to–PD map \undefined G.

  • Requires calibration of (A,Q,H,R), often difficult with limited macro–credit data.

Filtering in Transition or PD Space.

Alternatively, the filter may target the migration dynamics directly. Let \theta_t denote latent parameters (e.g. logits or intensities) generating P_t:

\begin{aligned}
\text{State:}& \quad \theta_{t+1}=\theta_t+\eta_t,\quad \eta_t\simN(0,\Sigma_\eta),\\
\text{Obs.:}& \quad C_{i\cdot,t}\sim\mathrm{Multinomial}(n_{i,t},P_{i\cdot,t}),\\
\text{Link:}& \quad P_{i\cdot,t}=\mathrm{softmax}(\theta_{i,t}), \ \text{with absorbing default enforced}.
\end{aligned}

Pros:

  • Targets the transition dynamics directly, independent of \undefined G.

  • Can partially absorb misspecification in the macro overlay by learning from realised migrations.

Cons:

  • Requires nonlinear filtering (EKF, UKF, particles), making analysis delicate.

  • Data scarcity: migration counts are sparse and heteroskedastic, especially in low–default portfolios.

  • Harder to justify for governance: filtering rating dynamics is less transparent than filtering macro inputs.

Hybrid Variants.

Between these designs several mid–ground options exist:

  • Parameter–filter: filter time–varying sensitivities \beta_{ij,t} in P_t=\undefined G(P^{\mathrm{TTC}},\widehat M_t;\beta_t).

  • Generator–filter: work with a continuous–time generator Q_t s.t. P_t=\exp(Q_t\Delta), filtering the constrained parameters of Q_t.

  • Augmented–state: jointly filter (M_t,\theta_t) to capture feedback between macro factors and transitions.

Chosen Design.

This paper adopts the macro–state filter. It preserves the Markov structure, admits a neutral long–run anchor, and enables an explicit asymptotic stability result for the PD term structure, see Section [sec:macroSS]. Section [sec:naiveKF] shows that a direct Kalman application still leaves residual instability, while Section 4 introduces the anchored formulation and establishes stability as the main contribution.

State–Space Model for the Macroeconomic Driver

Let M_t \in \mathbb{R}^r denote the latent macroeconomic state at quarter t and \widehat M_t \in \mathbb{R}^r the forecast available to the modeller. We assume a linear–Gaussian state–space model,

M_{t+1} = A M_t + w_t, 
\qquad w_t \sim N(0,Q),
\widehat M_t = H M_t + v_t, 
\qquad v_t \sim N(0,R),

with A,H \in \mathbb{R}^{r\times r}, process covariance Q\succeq 0, and observation covariance R\succ 0. The Kalman filter delivers the conditional mean \mu_t=\mathbb E[M_t \mid \widehat M_{0:t}] and covariance \Sigma_t=\mathrm{Cov}(M_t \mid \widehat M_{0:t}) via the standard recursion:

\begin{aligned}
\text{Predict:}\quad & \mu_{t|t-1} = A \mu_{t-1}, 
& \Sigma_{t|t-1} = A \Sigma_{t-1} A^\top + Q,\\
\text{Gain:}\quad & K_t = \Sigma_{t|t-1} H^\top \big(H \Sigma_{t|t-1} H^\top + R\big)^{-1},\\
\text{Update:}\quad & \mu_t = \mu_{t|t-1} + K_t\big(\widehat M_t - H \mu_{t|t-1}\big),\\
& \Sigma_t = (I - K_t H)\Sigma_{t|t-1}.
\end{aligned}

The PIT transition matrix at time t is then evaluated at the filtered macro state,

P_t = \undefined G\!\big(P^{\mathrm{TTC}},\, \mu_t\big),

and the portfolio propagates according to \pi_{t+1}=\pi_t P_t. In this setup the Kalman filter acts as a smoothing device: rather than relying on raw forecasts \widehat M_t, the PD dynamics are driven by the filtered estimate \mu_t. The next part examines whether this “naïve” Kalman application is sufficient to stabilise lifetime PDs.

Naïve Kalman Filtering and Residual Instability

We first examine a direct application of the Kalman filter to macroeconomic forecasts. Here the forecast itself is treated as the observation of the latent state. Although this preserves the recursive update form, it means that each observation already embeds the same forecast error as the state being estimated. Forecast error is therefore recycled back into the filter, creating a feedback loop. Over long horizons, this error recycling prevents stochastic stability: estimation variance converges to a strictly positive constant and residual fluctuations remain in the PD term structure. This shortcoming motivates the anchored reformulation in Section 4, where the observation is tied to a neutral long–run macro state.

Formally, let the macro state follow

M_{t+1} = A M_t + w_t,\qquad w_t \sim N(0,Q),

with forecast signal

y_t = \widehat M_t = H M_t + v_t,\qquad v_t \sim N(0,R),

where w_t and v_t are independent, Q\succeq 0, R\succ 0. Let \mu_t be the filtered mean, \Sigma_t the filtered covariance, and e_t=\mu_t-M_t the estimation error. The PIT matrix is evaluated at \mu_t, i.e. P_t=\undefined G(P^{\mathrm{TTC}},\mu_t), with portfolio dynamics \pi_{t+1}=\pi_t P_t.

Standard Kalman algebra gives the error recursion

\begin{equation}
\label{eq:naive_error}
e_{t+1} = \big(I-K_{t+1}H\big)A e_t + \big(I-K_{t+1}H\big)w_t - K_{t+1}v_{t+1},
\end{equation}

with gain K_{t+1}=\Sigma_{t+1|t}H^\top\big(H\Sigma_{t+1|t}H^\top+R\big)^{-1} and predictor covariance \Sigma_{t+1|t}=A\Sigma_t A^\top+Q. The covariance update is the Riccati recursion

\begin{equation}
\label{eq:riccati}
\Sigma_{t+1} = \big(I-K_{t+1}H\big)\Sigma_{t+1|t}.
\end{equation}

Theorem 1 (Residual variability under naive filtering). *Suppose (A,Q^{1/2}) is stabilisable, (A,H) is detectable, Q\succeq 0, and R\succ 0. Then the Kalman error covariance converges to a positive–definite limit \Sigma_\infty\succ 0. If the PD map \varphi(\pi,m) is locally Lipschitz and uniformly sensitive on a compact macro set, then

\limsup_{t\to\infty}\,\mathbb E\big[\,|\,Y_t-\varphi(\pi_t,M_t)\,|\,\big] > 0,

with Y_t=\varphi(\pi_t,\mu_t). In particular, the lifetime PD sequence does not converge in probability.*

Proof. Sketch. Kalman theory implies \Sigma_t\to\Sigma_\infty\succ 0. The non–vanishing state error propagates into the PD sequence through Lipschitz continuity and sensitivity of \varphi, ensuring a strictly positive long–run deviation. Full details are provided in Appendix [app:proofs]. ◻

Remark. Remark 2 (Self–observation pitfall). If one replaces y_t by H\mu_{t-1}, i.e. no new information, the innovation collapses to H(I-A)\mu_{t-1} and the update becomes a deterministic linear map of \mu_{t-1}. This produces a biased estimator and breaks the filter’s error–correction logic. Such degenerate schemes are not considered further.

Residual variability under naive Kalman filtering. (a) Estimation error variance 𝔼∥et∥2 converges to the positive constant tr(Σ∞). (b) Lifetime PD trajectories driven by the filtered macro state fluctuate indefinitely around the neutral limit, preventing asymptotic stability. Naive filtering prevents explosive growth of forecast errors but does not restore convergence.

Implication.

Under the naive scheme, forecast noise survives the filter and perturbs the PIT matrices indefinitely. The lifetime PD term structure therefore fails to stabilise, which motivates the anchored observation design developed in Section 4.

Anchored Filtering and Stability Result

This section presents the proposed anchoring strategy for macroeconomic filtering. We first introduce the idea of neutral anchoring at a conceptual level, then derive a generalised anchored observation model. The resulting error dynamics are analysed and a rigorous proof of asymptotic stochastic stability is provided.

Anchoring Strategies for Macroeconomic Filtering

Section [sec:naiveKF] showed that a naive filter, which reuses forecasts as observations, recycles forecast error and leaves non–vanishing PD variability. To overcome this structural limitation, we introduce anchoring: replace the forecast proxy with a neutral macro state that serves as a stabilising reference. The idea is simple: each update is pulled towards a long–run neutral point, preventing forecast errors from compounding across horizons.

(i) Anchored macro–state filter (neutral proxy)

Anchor: fix a neutral macro state M_\star (e.g. “zero impact”).
Update: \mu_t=\mu_{t|t-1}+K_t\big(M_\star-H\mu_{t|t-1}\big).
Interpretation: equivalent to observing M_\star with vanishing noise; simple and stabilising.
Limitation: calibration and governance are less transparent because the anchor enters implicitly.

(ii) Generalised anchored macro–state filter (stacked observation)

Observation: combine forecast and neutral anchor,

y_t=\begin{bmatrix}\widehat M_t\\ M_\star\end{bmatrix},\qquad 
H_{\mathrm{aug}}=\begin{bmatrix}H\\ I\end{bmatrix},\qquad 
R_{\mathrm{aug}}=\mathrm{diag}\!\big(R,\ \sigma_\star^2 I\big).

Update: \mu_t=\mu_{t|t-1}+K_t\big(y_t-H_{\mathrm{aug}}\mu_{t|t-1}\big).
Control knob: \sigma_\star^2 tunes anchor strength; take \sigma_\star^2>0 within the forecast window and \sigma_\star^2\downarrow 0 beyond T_F.
Benefit: explicit measurement structure, observability via the identity block, and a clean Riccati convergence path for stability proofs.

General formulation.

The neutral proxy in (i) is contained in the stacked model (ii) as the limiting case \sigma_\star^2\downarrow 0. We therefore analyse the general anchored formulation going forward; all stability results apply to the proxy as a special case. Section [sec:stabilityproof] establishes asymptotic stochastic stability of the lifetime PD term structure under this model.

Generalised Anchored Observation Model

To overcome the instability of the naive formulation, we introduce an anchored observation model. The key idea is to represent the macroeconomic state as a deviation from a neutral long–run level M^\ast, interpreted as the TTC equilibrium. Rather than updating relative to past forecasts, the filter evaluates each observation against this anchor, eliminating the accumulation of forecast error across horizons.

Formally, the observation equation is

\begin{equation}
y_t \;=\; H(M_t - M^\ast) + \nu_t, 
\qquad \nu_t \sim N(0,R),
\end{equation}

where H maps deviations from the anchor into the observable space. The state dynamics remain

\begin{equation}
M_{t+1} = A M_t + w_t, 
\qquad w_t \sim N(0,Q),
\end{equation}

with A capturing persistence and Q the innovation covariance. Defining deviations \delta_t := M_t - M^\ast, we obtain

\begin{equation}
\delta_{t+1} = A \delta_t + w_t.
\end{equation}

The Kalman filter applied to this deviation system yields

\begin{align}
\hat{\delta}_{t|t-1} &= A \hat{\delta}_{t-1|t-1}, \\
\hat{\delta}_{t|t}   &= \hat{\delta}_{t|t-1} + K_t \big(y_t - H \hat{\delta}_{t|t-1}\big),
\end{align}

with Kalman gain K_t = P_{t|t-1} H^\top (H P_{t|t-1} H^\top + R)^{-1} and covariance recursion as usual.

By construction, \delta_t is centred at zero under the neutral macro environment. The filter is therefore mean–reverting around the anchor and prevents divergence of lifetime PD forecasts induced by persistent forecast errors. This anchored formulation provides the foundation for the stability analysis in Section [sec:stabilityproof], where asymptotic stochastic stability of the lifetime PD term structure is established.

Error Dynamics and Convergence Intuition

To avoid confusion with the forecast error \delta_t from Section [sec:instable], we denote by

\xi_t := M_t - M^\star

the deviation of the macro state from the neutral anchor M^\star. Under the anchored observation model,

y_t = H \xi_t + \nu_t,\qquad \nu_t\simN(0,R),

with state dynamics

\xi_{t+1} = A \xi_t + w_t,\qquad w_t\simN(0,Q).

Let \hat\xi_{t|t} be the Kalman estimate and e_t:=\hat\xi_{t|t}-\xi_t the estimation error.

Error recursion.

The Kalman update yields

\hat\xi_{t|t-1}=A\hat\xi_{t-1|t-1},\qquad
\hat\xi_{t|t}=\hat\xi_{t|t-1}+K_t(y_t-H\hat\xi_{t|t-1}),

leading to the error dynamic

\begin{equation}
\label{eq:err_deviation}
e_t = (I-K_t H)A e_{t-1} - (I-K_t H)w_{t-1} + K_t \nu_t .
\end{equation}

Thus the mean error evolves under the linear map (I-K_t H)A, with process and measurement noise entering additively.

Stacked formulation.

If both the forecast \widehat M_t and the neutral anchor are observed, the system can be written in stacked form with

y_t^{\mathrm{aug}}=\begin{bmatrix}\widehat M_t\\ M^\star\end{bmatrix},\qquad
H_{\mathrm{aug}}=\begin{bmatrix}H\\ I\end{bmatrix},\qquad
R_{\mathrm{aug}}=\mathrm{diag}(R,\sigma_\star^2 I).

The associated error recursion is

\begin{equation}
\label{eq:err_stacked}
e_{t+1}^{\mathrm{aug}}
= (I-K_{t+1}H_{\mathrm{aug}})A e_t^{\mathrm{aug}}
+ (I-K_{t+1}H_{\mathrm{aug}})w_t - K_{t+1}\eta_{t+1},
\end{equation}

with \eta_t=(v_t,\nu_t)^\top. The identity block in H_{\mathrm{aug}} guarantees detectability and, once the gain stabilises, ensures a strict contraction.

Convergence intuition.

Anchoring changes the innovation reference: instead of tracking past forecasts, each update is pulled back towards M^\star. Large deviations therefore decay over time. The contraction (I-K_t H)A (or (I-K_t H_{\mathrm{aug}})A in the stacked model) implies that the estimation error remains mean–square bounded, and vanishes in the absence of process noise with a tight anchor. By Lipschitz continuity of the PD map \phi, the lifetime PD sequence Y_t=\phi(\pi_t,\hat M_{t|t}) inherits this stability. The next section formalises these statements.

Proof of Asymptotic Stochastic Stability

We analyse the generalised anchored filter with stacked observation. The latent state is M_t, the observation is

y_t^{\mathrm{aug}}=\begin{bmatrix}\widehat M_t\\ M^\star\end{bmatrix},\qquad 
H_{\mathrm{aug}}=\begin{bmatrix}H\\ I\end{bmatrix},\qquad 
R_{\mathrm{aug}}=\mathrm{diag}(R,\sigma_\star^2 I).

The portfolio evolves via \pi_{t+1}=\pi_t P_t with P_t=\undefined G(P^{\mathrm{TTC}},\hat M_{t|t}), and lifetime PDs are given by Y_t=\phi(\pi_t,\hat M_{t|t}).

Assumption 1. A\in\mathbb R^{r\times r}, Q\succeq 0, R\succ 0, \sigma_\star^2>0. The pair (A,Q^{1/2}) is stabilisable and (A,H_{\mathrm{aug}}) is detectable. The PD map \phi(\pi,m) is locally Lipschitz in m near M^\star with constant L>0.

Theorem 2 (Anchored mean–square stability and convergence). *Under Assumption 1, the Kalman error covariance \Sigma_t converges to the unique stabilising solution \Sigma_\infty of the Riccati equation for (A,H_{\mathrm{aug}},Q,R_{\mathrm{aug}}). In particular,

\lim_{t\to\infty}\mathbb E\|e_t\|^2 \;=\; \mathrm{tr}\,\Sigma_\infty \;<\;\infty,

so the estimation error is mean–square bounded and does not recycle forecast noise.*

*If, in addition, there exists a horizon T_F such that Q=0 and \sigma_\star^2=0 for all t\ge T_F (hard neutral anchor beyond the forecast window), then e_t\to 0 exponentially fast. Consequently,

\phi(\pi_t,\hat M_{t|t}) \;\xrightarrow{\;p\;}\; \phi(\pi_t,M^\star),

and the lifetime PD term structure converges in probability to the neutral macro limit.*

Proof. Sketch. Detectability of (A,H_{\mathrm{aug}}) with the identity block guarantees a stabilising Kalman gain, so \Sigma_t\to\Sigma_\infty and the error is mean–square bounded. Under hard anchoring, (I-KH_{\mathrm{aug}})A is a strict contraction, yielding exponential decay of e_t. By Lipschitz continuity of \phi, convergence of e_t implies convergence of PDs. A complete proof is given in Appendix [app:proofs]. ◻

Technical notes.

The assumptions underlying Theorem 2 and their practical justification are discussed further in the Appendix [app:proofs]. In particular, we comment on uniform sensitivity of logit overlays, compactness of the macro domain, and continuous anchoring within the forecast window. These notes clarify the robustness of the stability result while keeping the main exposition focused.

Implementation and Simulation Results

We illustrate the behaviour of lifetime PD propagation under synthetic scenarios. A minimal corporate portfolio is simulated over a five–year forecast horizon (T_F=20 quarters), followed by reversion to TTC dynamics. Three procedures are compared on identical data: (i) direct propagation without filtering, (ii) naive Kalman filtering using forecasts as observations, and (iii) the proposed anchored observation model. Key outputs are lifetime PD term structures, cross–scenario variance, and stability diagnostics.

Synthetic Portfolio and Transition System

We consider a portfolio of N=10{,}000 obligors, initially distributed as

\pi_0 = (0.45,\;0.40,\;0.15,\;0.00), 
\qquad \text{counts } = (4500,\,4000,\,1500,\,0),

across ratings \{A,B,C,D\} with D absorbing (default). The quarterly TTC matrix is

P^{\mathrm{TTC}} =
\begin{bmatrix}
0.975 & 0.022 & 0.002 & 0.001 \\
0.030 & 0.935 & 0.030 & 0.005 \\
0.010 & 0.060 & 0.915 & 0.015 \\
0     & 0     & 0     & 1
\end{bmatrix}.

The macro driver is a composite index

M_t = \tfrac{1}{2}z(g_t)-\tfrac{1}{2}z(u_t),

where g_t is GDP growth, u_t the unemployment rate, and z(\cdot) denotes standardisation. The PIT matrix is constructed via a logit overlay

p_{ij,t} \;\propto\; p^{\mathrm{TTC}}_{ij}\,\exp(\beta_{ij}M_t),
\qquad \sum_j p_{ij,t}=1,\; p_{ij,t}\ge0,

with sensitivities on nearest–neighbour moves and default, e.g. \beta_{A\to B}=2.0, \beta_{A\to D}=3.0, \beta_{B\to D}=2.0, \beta_{C\to D}=1.2, and symmetric upgrades.

Macroeconomic Scenarios

Three stylised scenarios are simulated:

  • Baseline: mild cycle, GDP at 0.5\% per quarter with small oscillations; unemployment flat near 5.5\%. Realisations add Gaussian noise (\sigma_g=0.2\%, \sigma_u=0.2pp).

  • Stress: downturn with slow recovery, mimicking a financial crisis. GDP falls -2.0\% in t=1, recovers linearly; unemployment rises to 7.5\% then recedes. Realised path: deeper trough and slightly higher unemployment peak.

  • Pandemic: abrupt contraction with rapid rebound. GDP -8\% at t=2, +6\% at t=3, then normalisation; unemployment spikes to 9.5\% then falls quickly. Realised path: faster rebound imposed at t=3.

Figures 5–12 display the forecast and realised macro paths. The baseline remains close to equilibrium; the stress scenario shows persistent weakness; the pandemic is characterised by a short, sharp disruption.

Filtering Designs

The macro state follows an AR(1) process

M_{t+1}=\rho M_t+w_t,\quad w_t\simN(0,Q),
\qquad \rho=0.90,\; Q=0.19,

with forecast y_t as a noisy signal.

  • Naive KF: observation y_t=\widehat M_t, with variance R=0.25.

  • Anchored KF: stacked observation y_t^{\mathrm{aug}}=(\widehat M_t,M^\star), R_{\mathrm{aug}}=\mathrm{diag}(0.25,\sigma_\star^2), with \sigma_\star^2=0.25 inside the horizon and \sigma_\star^2=0 afterwards.

This design ensures responsiveness to PIT signals during the forecast window while enforcing TTC convergence beyond.

Parameter summary.

\begin{array}{lcl}
\text{Persistence} & \rho &= 0.90 \\
\text{Process var.} & Q    &= 0.19 \\
\text{Forecast var.} & R   &= 0.25 \\
\text{Anchor var. (in)} & \sigma_\star^2 &= 0.25 \\
\text{Anchor var. (out)} & \sigma_\star^2 &= 0 \\
\text{Horizon} & T_F &= 20 \\
\text{Portfolio size} & N &= 10{,}000
\end{array}

Robustness.

Results are qualitatively unchanged for \rho\in[0.8,0.95]. Higher R increases dispersion under the naive KF but does not destabilise the anchored filter. Smaller \sigma_\star^2 accelerates convergence to M^\star but reduces PIT sensitivity; a decaying schedule \sigma_{\star,t}^2\downarrow0 is a practical compromise.

Composite macro index Mt under baseline (mild cycle), stress (sharp downturn, slow recovery), and pandemic (abrupt shock with rebound). The neutral line at zero indicates TTC conditions.
Quarterly GDP growth forecasts. Baseline oscillates around 0.5%, stress contracts then recovers gradually, pandemic exhibits a sharp fall followed by rebound.
Unemployment forecasts. Baseline remains near 5%, stress rises to a persistent plateau, pandemic spikes abruptly then normalises.
Forecasted macroeconomic scenarios (baseline, stress, pandemic) over a 20–quarter horizon: (a) composite index Mt, (b) GDP growth, and (c) unemployment rate.
GDP growth, baseline scenario. Forecast (blue line) oscillates near trend; realised path (black dots) fluctuates around it.
GDP growth, stress scenario. Forecast (orange) shows contraction then gradual recovery; realised path deviates with deeper trough.
GDP growth, pandemic scenario. Forecast (green) imposes sharp fall and rebound; realised path overshoots during recovery.
Unemployment, baseline scenario. Forecast (blue) stays near 5.5%; realised path shows minor noise.
Unemployment, stress scenario. Forecast (orange) rises towards 7.5% then declines; realised path peaks higher.
Unemployment, pandemic scenario. Forecast (green) spikes to 9.5% then normalises; realised path recovers faster.
Forecasted (coloured lines) and realised (black dots) GDP growth (top row) and unemployment rates (bottom row) for baseline, stress, and pandemic scenarios.

Simulation Results

We compare three specifications: (i) raw forecast input without filtering, (ii) naïve Kalman filter without anchor, and (iii) the proposed anchored Kalman filter. All runs use the synthetic portfolio and calibration described in Sections 5.1–5.2 with N=10{,}000 exposures.

Macroeconomic estimates.

13 illustrates how the two filters treat noisy forecasts. The naïve filter tracks the forecast closely but inherits its persistent bias during shocks, producing oscillatory paths. By contrast, the anchored filter dampens overshooting and converges back to the neutral state after T_F=20 quarters. This stabilisation reduces the root–mean–square error (RMSE) of M_t by 38% relative to the naïve filter. The difference at the macro level sets the stage for contrasting behaviour in PD dynamics.

PD term structures.

14 shows the PIT default rate trajectories implied by the three methods. Raw forecasts generate highly volatile paths that amplify cyclical swings. The naïve filter moderates this volatility but still leaves a persistent drift away from the TTC baseline. Anchoring delivers bounded deviations: PD spreads remain within \pm 20 bps of TTC and revert more quickly after shocks. Thus, the stabilisation observed in the macro index translates directly into smoother PD term structures.

Loss variance.

The quantitative impact is summarised in 1, which reports the average variance of lifetime PD forecasts Y_t. Anchoring reduces variance almost an order of magnitude relative to the naïve filter and by more than a factor of ten relative to the raw PIT overlay. This confirms that the qualitative impressions from Figures 13 and 14 are systematic across horizons and scenarios. Additional loss simulations (not shown) indicate that portfolio loss volatility is nearly halved under the anchored design.

Scenario comparison.

2 highlights regime dependence. In baseline and stress cases, the anchored filter dominates, producing the lowest and most stable PD paths. In the pandemic case, however, the naïve filter sometimes tracks the sharp rebound more closely, illustrating the trade–off between responsiveness and stability. This nuance shows that while anchoring is generally superior, its benefits are strongest when forecast error variance is persistent rather than transitory.

Distributional evidence.

15 examines the distribution of lifetime PDs under 200 Monte Carlo replications. Across all scenarios, the anchored filter compresses dispersion relative to the raw and naïve methods, especially in the stress case where variance would otherwise explode. At the same time, scenario-specific shifts are preserved: stress remains the worst case, pandemic shows sharp but temporary deviations, and baseline hovers near TTC.

Interpretation.

Taken together, the results demonstrate that anchoring improves robustness at two levels: (i) macro signals are stabilised, preventing drift from noisy forecasts; (ii) PD term structures and loss projections become less volatile, yielding more reliable inputs for capital planning. The trade–off is a mild reduction in responsiveness to abrupt reversals, but this is outweighed by the systematic stability gains across horizons and scenarios.

Method Mean variance of Y_t
Anchored 0.000209
Naïve 0.001181
Raw 0.002248

Average variance of lifetime PD forecasts Y_t across scenarios. Anchoring reduces variance almost an order of magnitude relative to the naïve filter.

Scenario Best method Mean Y_T Std. dev. of Y_T
Baseline Anchored 0.1093 1.39\times 10^{-17}
Stress Anchored 0.0900 0.0000
Pandemic Naïve 0.0985 1.39\times 10^{-17}

Best-performing method per scenario ranked by Monte Carlo volatility of terminal lifetime PD Y_T. Anchoring dominates in baseline and stress, while naïve filtering captures the sharp rebound in the pandemic case.

Baseline scenario.
Stress scenario.
Pandemic scenario.
Effect of anchoring in the Kalman filter across scenarios. Grey = forecast, black dots = realised, blue = naïve KF, green = anchored KF. Anchoring stabilises estimates by pulling them towards the long-run neutral level.
Baseline scenario.
Stress scenario.
Pandemic scenario.
Lifetime PD term structures across methods (raw, naïve, anchored) with TTC baseline reference. Anchoring delivers smoother and more stable paths that converge back to TTC more quickly.
Baseline scenario.
Stress scenario.
Pandemic scenario.
Lifetime PD at horizon T under 200 Monte Carlo replications. Bars show mean cumulative PD, error bars show volatility. Anchoring compresses dispersion, particularly under stress.

Reproducible Simulation Results

All code and reproducible notebooks used to generate these results are openly available. The full project is maintained in the lifetime-PD-AKF subfolder of my research repository GitHub – research-lab, with a versioned release archived at GitHub Release v1.0-lifetime-PD-AKF. For long-term reproducibility, the release has also been deposited in Zenodo and assigned the DOI 10.5281/zenodo.17072772, which can be cited directly.

Practical Considerations

Beyond numerical performance, several aspects are critical for practical adoption of the proposed framework in credit risk management.

Calibration choices.

The anchored Kalman filter requires specification of the process variance Q and the anchor variance \sigma_\star^2. In practice these can be calibrated by matching short–horizon forecast errors against realised macro outcomes, and by setting the anchor variance to reflect the institution’s TTC risk appetite. Sensitivity analysis should be performed to ensure that results are robust to moderate changes in these parameters.

Scenario design and governance.

Supervisory expectations under IFRS 9 and CECL require institutions to use multiple forward–looking scenarios. The proposed filter can be applied to each scenario path separately, with scenario weights applied ex post. Anchoring reduces the undue influence of extreme but low–probability trajectories, leading to more stable expected credit loss estimates. Clear governance is required to document parameter settings, anchoring horizon T_F, and rationale for scenario selection.

Model risk and validation.

Anchoring introduces an explicit modelling assumption: that long–run PDs should converge back to TTC levels. This assumption must be validated empirically and challenged by independent model risk teams. Backtesting against realised defaults, especially during volatile periods, is essential. Stress tests should include scenarios where convergence is delayed or where structural breaks alter the long–run mean.

Implementation workflow.

The filter operates on standard inputs already available in most IFRS 9 and CECL platforms: macroeconomic forecasts, transition matrices, and PD term structures. Implementation can therefore be integrated with minimal system changes. A practical workflow is to (i) generate baseline PDs under raw scenarios, (ii) apply the anchored filter to obtain adjusted transition matrices, (iii) propagate lifetime PDs, and (iv) aggregate to expected credit losses. This sequence aligns with existing reporting cycles and committee review processes.

Sensitivity and transparency.

Anchoring inherently reduces volatility, but excessive damping may hide relevant short–term dynamics. Institutions should report both anchored and unanchored results to committees, together with variance decompositions that show the stabilising effect. Such transparency ensures that management understands the trade–off between stability and responsiveness.

Auditor and regulatory reception.

Compared with ad–hoc overlays or heuristic smoothing, the anchored Kalman filter offers a transparent, theoretically grounded mechanism for controlling forecast noise. This clarity matters in supervisory reviews: regulators increasingly require that institutions justify stability adjustments with formal models rather than discretionary overrides. Anchoring provides such a justification. It yields reproducible results, a clear governance trail, and a formal stability proof. As a result, it is easier to defend in model validation, internal audit, and regulatory dialogue than bespoke overlays that lack theoretical backing.

Summary.

The simulation study demonstrates that anchoring yields substantial variance reduction in long–horizon PD forecasts while preserving responsiveness at short horizons. Calibration, governance, and validation considerations show that the method can be integrated into existing IFRS 9 and CECL workflows with limited operational burden. The next section concludes by summarising the main findings and outlining directions for further research.

Conclusion and Future Work

This paper formulated the lifetime probability of default (PD) estimation problem in a state–space framework and demonstrated that macroeconomic forecast uncertainty induces stochastic instability in long–horizon projections. Naïve propagation of forecasted drivers was shown to embed persistent forecast errors, generating unbounded error dynamics that complicate risk management, capital allocation, and committee decisions under IFRS 9 and CECL.

To address this limitation, we introduced an anchoring strategy for macroeconomic filtering and derived a generalised Kalman formulation with provable asymptotic stochastic stability. Theoretical analysis established bounded error dynamics, while simulations on a synthetic corporate portfolio confirmed substantial variance reduction and improved robustness under structural shocks such as financial crises and pandemics. The framework integrates seamlessly with existing transition–matrix models and requires only modest calibration effort, making it suitable for regulatory and industry practice.

Several avenues for future work remain. First, extending the framework to nonlinear state–space dynamics and particle filtering could improve robustness under non–Gaussian shocks (Doucet et al. 2001). Second, empirical validation on large and heterogeneous credit portfolios is needed to assess scalability and sector–specific sensitivity (Durbin and Koopman 2012). Third, the method could be embedded into stress testing frameworks and linked to macroprudential capital planning (Stress Testing Principles 2018). Finally, interactions with climate transition scenarios and long–term sustainability risks (NGFS Climate Scenarios for Central Banks and Supervisors 2020) represent an important direction for future research.

Technical Proofs and Additional Results

*Proofs from Section 2

We first present an intuitive lemma showing that persistent forecast error prevents convergence. We then provide the full proof of Proposition 1, followed by a corollary that quantifies the accumulation of deviations.

Lemma 1 (Instability under non–vanishing forecast error). *Let \pi_{t+1}=\pi_t\,\undefined G(P^{\mathrm{TTC}},M_t+\delta_t) and Y_t=\pi_t\,\undefined G(P^{\mathrm{TTC}},M_t)\,e_K. Assume \undefined G is continuously differentiable in its macro argument and there exists \alpha>0 such that

\big|\pi\,\big(\undefined G(P^{\mathrm{TTC}},m+\delta)-\undefined G(P^{\mathrm{TTC}},m)\big)\,e_K\big| \;\ge\; \alpha\,|\delta|

for all \pi\in\Delta_K, all m in a neighbourhood of the path, and all small |\delta|. If \mathbb P(|\delta_t|>\varepsilon)>0 for some \varepsilon>0 and infinitely often, then (Y_t) does not converge in probability.*

Proof. Proof. Whenever |\delta_t|\ge \varepsilon, the sensitivity bound yields |Y_{t+1}-\phi(\pi_t,M_t)|\ge \alpha\varepsilon, where \phi(\pi,m):=\pi\,\undefined G(P^{\mathrm{TTC}},m)\,e_K. If such events occur infinitely often with positive probability, the distance to any candidate limit cannot vanish in probability. ◻

Proof of Proposition 1.

By (A2) and independence, the events A_t:=\{|\delta_t|\ge\varepsilon\} are i.i.d. with \mathbb P(A_t)=p>0. Hence, by the second Borel–Cantelli lemma, \mathbb P(A_t\ \text{i.o.})=1.

Fix \omega in the full–probability event where A_t occurs infinitely often and where, by (A3), Y^{\circ}_t(\omega)\to Y^\ast. For any such \omega and any t,

\big|Y_{t+1}-Y^{\circ}_{t+1}\big|
=\big|\phi\!\big(\pi_t,M_t+\delta_t\big)-\phi\!\big(\pi_t,M_t\big)\big|
\ \stackrel{(A1)}{\ge}\ \alpha\,|\delta_t|.

On A_t this is at least \alpha\varepsilon. Since Y^{\circ}_t\to Y^\ast, there exists T(\omega) such that for all t\ge T(\omega), \big|Y^{\circ}_{t}-Y^\ast\big|\le \tfrac{1}{3}\alpha\varepsilon. Taking any t\ge T(\omega) with A_t true (there are infinitely many),

\big|Y_{t+1}-Y^\ast\big|
\;\ge\; \big|Y_{t+1}-Y^{\circ}_{t+1}\big|-\big|Y^{\circ}_{t+1}-Y^\ast\big|
\;\ge\; \alpha\varepsilon - \tfrac{1}{3}\alpha\varepsilon
\;=\;\tfrac{2}{3}\alpha\varepsilon.

Thus \limsup_{t\to\infty}\big|Y_{t}-Y^\ast\big|\ \ge\ \tfrac{2}{3}\alpha\varepsilon on a set of probability one. This precludes almost sure convergence and, a fortiori, convergence in probability. 0◻

Corollary 1 (Explicit deviation bound and accumulation). Let \pi_{t+1}=\pi_t\,\undefined G(P^{\mathrm{TTC}},M_t+\delta_t) and \pi^\circ_{t+1}=\pi^\circ_t\,\undefined G(P^{\mathrm{TTC}},M_t), and set e_t:=\|\pi_t-\pi^\circ_t\|_1. Assume:

  • \|\undefined G(P^{\mathrm{TTC}},m+\delta)-\undefined G(P^{\mathrm{TTC}},m)\|_{1\to 1}\le L_G\,|\delta| for all relevant m,\delta.

  • \undefined G(P^{\mathrm{TTC}},m) is row–stochastic for all m.

*Then for all t\ge 1,

e_t \;\le\; e_0 \;+\; L_G \sum_{s=0}^{t-1} |\delta_s|.

Moreover, if there exists \alpha_G>0 with

\|\undefined G(P^{\mathrm{TTC}},m+\delta)-\undefined G(P^{\mathrm{TTC}},m)\|_{1\to 1}\ \ge\ \alpha_G\,|\delta|

for all small |\delta|, then for any t\ge 1 there exists s\in\{0,\dots,t-1\} with

e_{s+1} \;\ge\; \alpha_G\,|\delta_s|.
```*

</div>

<div class="proof">

*Proof.* Write `P_t:=\undefined G(P^{\mathrm{TTC}},M_t+\delta_t)` and `P^\circ_t:=\undefined G(P^{\mathrm{TTC}},M_t)`. Then
``` math
\pi_{t+1}-\pi^\circ_{t+1}
= (\pi_t-\pi^\circ_t)P_t + \pi^\circ_t(P_t-P^\circ_t).

Take the 1–norm and use submultiplicativity together with (B2): \|xP_t\|_1\le \|x\|_1 for any row vector x and row–stochastic P_t. Hence

e_{t+1} \;\le\; e_t + \|\pi^\circ_t\|_1\,\|P_t-P^\circ_t\|_{1\to 1}
\;\le\; e_t + L_G\,|\delta_t|,

since \|\pi^\circ_t\|_1=1 and by (B1). Iteration yields the upper bound.

For the lower bound, pick s\in\{0,\dots,t-1\} maximising \|P_s-P^\circ_s\|_{1\to 1}. Then

e_{s+1} \;\ge\; \|\pi^\circ_s(P_s-P^\circ_s)\|_1 - \|e_s P_s\|_1
\;\ge\; \|P_s-P^\circ_s\|_{1\to 1} - e_s.

If e_s\le \tfrac{1}{2}\alpha_G|\delta_s|, the claim follows. Otherwise e_s>\tfrac{1}{2}\alpha_G|\delta_s|, which itself provides a valid lower bound at time s. ◻

*Proofs from Section 3

We collect here the technical results underlying Theorem 1 on residual variability under naive Kalman filtering.

Step 1. Persistent macro estimation error.

Lemma 2. *Consider the linear–Gaussian state–space model with stabilisable (A,Q^{1/2}) and detectable (A,H). Then the Kalman error covariance \Sigma_t converges to the unique stabilising solution \Sigma_\infty\succeq 0 of the algebraic Riccati equation. If Q\succeq 0 and R\succ 0 are non–degenerate, then \Sigma_\infty\neq 0. In particular,

\lim_{t\to\infty}\mathbb E\|M_t-\mu_t\|^2 \;=\; \mathrm{tr}\,\Sigma_\infty \;>\; 0.
```*

</div>

<div class="proof">

*Proof.* Classical Riccati theory implies `\Sigma_t\to\Sigma_\infty` under the stated conditions. Non–degenerate `Q,R` imply that process or measurement noise persists, hence `\Sigma_\infty\neq 0`. The mean–square error therefore converges to `\mathrm{tr}\,\Sigma_\infty>0`. ◻

</div>

#### Step 2. Transmission to PD sequence.

<div id="prop:naiveInstab" class="proposition">

**Proposition 2**. *Let `Y_t=\phi(\pi_t,\mu_t)` with `\mu_t` the naive Kalman estimate of the macro state `M_t`. Assume Lemma <a href="#lem:naive" data-reference-type="ref" data-reference="lem:naive">2</a>, and suppose `\phi` is locally Lipschitz and uniformly sensitive on a compact macro domain. Then there exists `c>0` such that
``` math
\limsup_{t\to\infty}\,\mathbb E\big|\,Y_t-\phi(\pi_t,M_t)\,\big| \;\ge\; c \;>\; 0,

so (Y_t) does not converge in probability.*

Proof. Proof. By Lemma 2, e_t=\mu_t-M_t converges in distribution to a zero–mean Gaussian Z\simN(0,\Sigma_\infty) with \Sigma_\infty\succ0. Uniform sensitivity implies |Y_t-\phi(\pi_t,M_t)|\ge c\|e_t\| for some c>0 on a compact domain. Taking expectations and limits yields \limsup_{t\to\infty}\mathbb E|Y_t-\phi(\pi_t,M_t)| \ge c\,\mathbb E\|Z\|>0. Hence Y_t does not converge in probability. ◻

Step 3. Verification for logit overlays.

Lemma 3 (Uniform sensitivity for logit overlays). *Consider the exponential/logit specification

p_{ij}(m)=\frac{p^{\mathrm{TTC}}_{ij}\exp(\beta_{ij} m)}
{\sum_{\ell}p^{\mathrm{TTC}}_{i\ell}\exp(\beta_{i\ell} m)},\qquad 
\phi(\pi,m)=\sum_i \pi_i p_{iK}(m).

If (i) p^{\mathrm{TTC}}_{iK}>0 for all i, and (ii) \beta_{iK}-\sum_j \beta_{ij}p_{ij}(m)\ge \delta_i>0 on a compact macro interval, then there exists c>0 such that

|\phi(\pi,m+d)-\phi(\pi,m)| \;\ge\; c\,|d|

for all \pi\in\Delta_K, m in the interval, and small |d|.*

Proof. Proof. Differentiate p_{iK}(m) with respect to m to obtain \partial_m p_{iK}(m)=p_{iK}(m)(\beta_{iK}-\sum_j\beta_{ij}p_{ij}(m)). Assumptions (i)–(ii) imply \partial_m \phi(\pi,m)\ge c_0>0 uniformly. Apply the mean value theorem to obtain the finite difference bound. ◻

Step 4. Conclusion.

Lemmas 2 and 3 together imply Proposition 2, which establishes Theorem 1.

Remark. Remark 3 (Sensitivity in logit overlays). In exponential or logit adjustments, p_{ij}(m)\propto p^{\mathrm{TTC}}_{ij}\exp(\beta_{ij}m), the derivative m\mapsto \partial_m\phi(\pi,m) is bounded away from zero on compact sets provided all p^{\mathrm{TTC}}_{ij}>0. This validates the uniform sensitivity assumption used in Proposition 2.

Remark. Remark 4 (Compactness of the macro domain). For autoregressive macro processes M_{t+1}=A M_t+w_t with |A|<1 and bounded or light–tailed shocks w_t, the distribution of M_t is stationary and mean–reverting. Hence the path remains with high probability in a compact interval \undefined I\subset\mathbb R. This justifies the compactness assumption in Lemma 3; see (Hamilton 1994; Tsay 2010).

*Proofs from Section 4

Proof. Proof of Theorem 2. The proof is structured into 3 clear steps: 1) boundedness, 2) exponential convergence, and 3) transfer to PDs convergence.

Step 1: Mean–square boundedness. Since H_{\mathrm{aug}} contains the identity block, the pair (A,H_{\mathrm{aug}}) is detectable. Together with stabilisability of (A,Q^{1/2}) and R_{\mathrm{aug}}\succ0, classical Kalman filter theory implies that the Riccati recursion converges to the unique stabilising solution \Sigma_\infty\succeq0. Hence \sup_t \mathbb E\|e_t\|^2<\infty and \mathbb E\|e_t\|^2\to \mathrm{tr}\,\Sigma_\infty.

Step 2: Exponential convergence under hard anchoring. If Q=0 and \sigma_\star^2=0 for all t\ge T_F, then beyond T_F the error recursion reduces to

e_{t+1} = (I-K_t H_{\mathrm{aug}})A e_t .

Detectability ensures convergence of K_t to a stabilising gain K_\star such that (I-K_\star H_{\mathrm{aug}})A is Schur. Uniform exponential stability of the time–varying system follows, implying e_t\to 0 exponentially.

Step 3: Transfer to PD convergence. By local Lipschitz continuity of \phi in m near M^\star,

\big|\,\phi(\pi_t,\hat M_{t|t})-\phi(\pi_t,M^\star)\,\big|
\;\le\; L\,\|e_t\|.

Since e_t\to0 exponentially, the right–hand side vanishes in probability. Thus the lifetime PD sequence converges in probability to the neutral macro limit. ◻

Remark. Remark 5 (Continuous anchoring within the forecast window). *If Q\succeq 0 and \sigma_\star^2>0 remain constant, then e_t converges in distribution to a Gaussian with covariance \Sigma_\infty. The induced PD deviations satisfy

\limsup_{t\to\infty}\mathbb E\big|\,\phi(\pi_t,\hat M_{t|t})-\phi(\pi_t,M^\star)\,\big| 
\;\le\; L\,\mathbb E\|Z\|, \qquad Z\simN(0,\Sigma_\infty).

This yields mean–square boundedness and practical stability of the PD term structure. Exact convergence is obtained when Q and \sigma_\star^2 are reduced after T_F.*

The preceding theorem establishes stability for the generalised anchored (stacked) model. For completeness, we also record the parallel result for the simpler anchored deviation formulation introduced in Section [sec:errordyn].

Corollary 2 (Anchored deviation model: stability and convergence). *Let \xi_t := M_t - M^\star with dynamics

\xi_{t+1}=A\xi_t+w_t,\qquad w_t\simN(0,Q),

and observation

y_t=H\xi_t+\nu_t,\qquad \nu_t\simN(0,R).

Assume (A,Q^{1/2}) stabilisable and (A,H) detectable. Let \hat\xi_{t|t} be the Kalman estimate, e_t=\hat\xi_{t|t}-\xi_t, and evaluate the PIT matrix at M^\star+\hat\xi_{t|t}. If \phi(\pi,m) is locally Lipschitz in m near M^\star with constant L>0, then:*

  1. (Mean–square boundedness) \Sigma_t\to\Sigma_\infty\succeq0 and \lim_{t\to\infty}\mathbb E\|e_t\|^2=\mathrm{tr}\,\Sigma_\infty<\infty.

  2. (Exponential convergence under hard anchoring) If Q=0 and R=0 for all t\ge T_F, then e_t\to 0 exponentially and math \phi(\pi_t,M^\star+\hat\xi_{t|t})\;\xrightarrow{\;p\;}\;\phi(\pi_t,M^\star).

Proof. Proof. Standard Riccati theory gives mean–square boundedness. If Q=0 and R=0 beyond T_F, then e_{t+1}=(I-K_t H)A e_t with K_t\to K_\star and (I-K_\star H)A Schur, so e_t\to0 exponentially. Lipschitz continuity of \phi yields the PD convergence. ◻

Authority, European Banking. 2017. Report on Results from the EBA Impact Assessment of IFRS 9. https://www.eba.europa.eu/.
Bangia, Anil, Francis X. Diebold, Andreas Kronimus, Christian Schagen, and Til Schuermann. 2002. “Ratings Migration and the Business Cycle, with Applications to Credit Portfolio Stress Testing.” Journal of Banking & Finance 26 (2-3): 445–74. https://doi.org/10.1016/S0378-4266(01)00225-X.
Bank, European Central. 2017. Guidance to Banks on Non-Performing Loans – Annex: Expected Credit Losses. https://www.ecb.europa.eu/.
Bank for International Settlements. 2021. Basel Committee on Banking Supervision: Credit Risk Modelling Practices and Applications of Expected Credit Loss Accounting Requirements. BIS. https://www.bis.org/bcbs/publ/d522.pdf.
Banking Supervision, Basel Committee on. 2000. Principles for the Management of Credit Risk. https://www.bis.org/publ/bcbs75.htm.
Banking Supervision, Basel Committee on. 2005. Studies on the Validation of Internal Rating Systems. No. 14. Basel Committee on Banking Supervision. https://www.bis.org/publ/bcbs_wp14.pdf.
Banking Supervision, Basel Committee on. 2015. Guidance on Credit Risk and Accounting for Expected Credit Losses. https://www.bis.org/.
Bluhm, Christian, Ludger Overbeck, and Christoph Wagner. 2016. Introduction to Credit Risk Modeling. 2nd ed. Chapman & Hall/CRC.
Board, Financial Accounting Standards. 2016. Accounting Standards Update No. 2016-13: Financial Instruments—Credit Losses (Topic 326). https://www.fasb.org/.
Board, International Accounting Standards. 2014. IFRS 9 Financial Instruments. https://www.ifrs.org/.
Borio, Claudio, Mathias Drehmann, and Kostas Tsatsaronis. 2014. “Stress Testing and Macroprudential Supervision: A Going-Concern Approach.” Journal of Financial Stability 12: 1–16.
Christensen, Jens H. E., Ernst B. Hansen, and David Lando. 2004. “Confidence Sets for Continuous-Time Rating Transition Matrices.” Journal of Banking & Finance 28 (11): 2575–602. https://doi.org/10.1016/j.jbankfin.2003.09.012.
Croushore, Dean, and Tom Stark. 2001. “A Real-Time Data Set for Macroeconomists.” Journal of Econometrics 105 (1): 111–30.
Doucet, Arnaud, Nando de Freitas, and Neil Gordon, eds. 2001. Sequential Monte Carlo Methods in Practice. Springer. https://doi.org/10.1007/978-1-4757-3437-9.
Duffie, Darrell, Leandro Saita, and Ke Wang. 2007. “Multi-Period Corporate Default Prediction with Stochastic Covariates.” Journal of Financial Economics 83 (3): 635–65. https://doi.org/10.1016/j.jfineco.2005.10.011.
Duffie, Darrell, and Kenneth J. Singleton. 1999. “Modeling Term Structures of Defaultable Bonds.” Review of Financial Studies 12 (4): 687–720. https://doi.org/10.1093/rfs/12.4.687.
Durbin, James, and Siem Jan Koopman. 2012. Time Series Analysis by State Space Methods. 2nd ed. Oxford University Press. https://doi.org/10.1093/acprof:oso/9780199641178.001.0001.
Economic Co-operation, Organisation for, and Development. 2020. OECD Economic Outlook, June 2020. https://www.oecd.org/economic-outlook/.
European Banking Authority. 2020. EBA Report on the Implementation of IFRS 9 by EU Institutions. European Banking Authority. https://www.eba.europa.eu/eba-publishes-report-implementation-ifrs-9.
Fund, International Monetary. 2021. World Economic Outlook: Recovery During a Pandemic. https://www.imf.org/en/Publications/WEO.
Hamilton, James D. 1994. Time Series Analysis. Princeton University Press.
Israel, Robert B., Jeffrey S. Rosenthal, and Jason Z. Wei. 2001. “Finding Generators for Markov Chains via Empirical Transition Matrices, with Applications to Credit Ratings.” Mathematical Finance 11 (2): 245–65. https://doi.org/10.1111/1467-9965.00114.
Jarrow, Robert A., David Lando, and Stuart M. Turnbull. 1997. “A Markov Model for the Term Structure of Credit Risk Spreads.” Review of Financial Studies 10 (2): 481–523. https://doi.org/10.1093/rfs/10.2.481.
Kalman, R. E. 1960. “A New Approach to Linear Filtering and Prediction Problems.” Transactions of the ASME–Journal of Basic Engineering 82 (1): 35–45.
Kavvathas, Dennis. 2001. “Estimating Credit Transition Matrices.” PhD thesis, University of Chicago.
Kreinin, Alexander, and Marina Sidelnikova. 2001. “Regularization Algorithms for Transition Matrices.” Algorithmic Finance 1 (1): 1–20.
Kushner, Harold J., and G. George Yin. 2003. Stochastic Approximation and Recursive Algorithms and Applications. 2nd ed. Vol. 35. Applications of Mathematics. Springer. https://doi.org/10.1007/978-1-4612-0053-1.
Lando, David. 2004. Credit Risk Modeling: Theory and Applications. Princeton University Press.
Lando, David, and Torben M. Skødeberg. 2002. “Analyzing Rating Transitions and Rating Drift with Continuous Observations.” Journal of Banking & Finance 26 (2-3): 423–44. https://doi.org/10.1016/S0378-4266(01)00224-8.
Maybeck, Peter S. 1979. Stochastic Models, Estimation and Control. Academic Press.
Mitrophanov, Alexander Y. 2005. “Sensitivity and Convergence of Uniformly Ergodic Markov Chains.” Annals of Applied Probability 15 (1A): 367–79. https://doi.org/10.1214/105051604000000661.
Nelson, Charles R., and Charles I. Plosser. 1982. “Trends and Random Walks in Macroeconomic Time Series: Some Evidence and Implications.” Journal of Monetary Economics 10 (2): 139–62. https://doi.org/10.1016/0304-3932(82)90012-5.
NGFS Climate Scenarios for Central Banks and Supervisors. 2020. Network for Greening the Financial System (NGFS). https://www.ngfs.net/en/ngfs-climate-scenarios-central-banks-and-supervisors.
Nickell, Pamela, William Perraudin, and Simone Varotto. 2000. “Stability of Rating Transitions.” Journal of Banking & Finance 24 (1-2): 203–27. https://doi.org/10.1016/S0378-4266(99)00057-7.
Orphanides, Athanasios, and Simon van Norden. 2002. “The Unreliability of Output-Gap Estimates in Real Time.” Review of Economics and Statistics 84 (4): 569–83.
Pesaran, M. Hashem, Til Schuermann, Björn-Jakob Treutler, and Scott M. Weiner. 2006. “Macroeconomic Dynamics and Credit Risk: A Global Perspective.” Journal of Money, Credit and Banking 38 (5): 1211–61. https://doi.org/10.1353/mcb.2006.0069.
Reifschneider, David, and Peter Tulip. 2017. “Gauging the Uncertainty of the Economic Outlook.” American Economic Review 107 (5): 173–77.
Seneta, Eugene. 2006. Non-Negative Matrices and Markov Chains. Revised. Springer Series in Statistics. Springer. https://doi.org/10.1007/0-387-32792-4.
Simon, Dan. 2006. Optimal State Estimation: Kalman, h Infinity, and Nonlinear Approaches. Wiley.
Stress Testing Principles. 2018. Basel Committee on Banking Supervision. https://www.bis.org/bcbs/publ/d450.pdf.
Tasche, Dirk. 2019. “Forward-Looking Credit Risk Modelling.” In Encyclopedia of Quantitative Finance. Wiley.
Tsay, Ruey S. 2010. Analysis of Financial Time Series. 3rd ed. Wiley.
Vasicek, Oldrich. 1977. “An Equilibrium Characterization of the Term Structure.” Journal of Financial Economics 5 (2): 177–88. https://doi.org/10.1016/0304-405X(77)90016-2.