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)
rng2) 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_TTCVisualise & 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()
fpath4) Macroeconomic Scenarios & Index
We define three stylised macro forecasts and simulate realisations by adding noise. We also build a macro index .
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(); fpath5) PIT Overlay (Logit Tilt)
We tilt TTC with a matrix using , 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(); fpath6) Kalman Filters (Naïve vs Anchored)
We compare a naïve KF to an anchored KF with a second observation (anchor at 0), soft before 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(); fpath7) Distribution Propagation & PD Series
We propagate and extract default mass .
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_bandsVariance 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_box11.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_viol11.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_boxdot11.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_bars11.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}inP_t=\undefined G(P^{\mathrm{TTC}},\widehat M_t;\beta_t).Generator–filter: work with a continuous–time generator
Q_ts.t.P_t=\exp(Q_t\Delta), filtering the constrained parameters ofQ_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.
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 near5.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\%int=1, recovers linearly; unemployment rises to7.5\%then recedes. Realised path: deeper trough and slightly higher unemployment peak.Pandemic: abrupt contraction with rapid rebound. GDP
-8\%att=2,+6\%att=3, then normalisation; unemployment spikes to9.5\%then falls quickly. Realised path: faster rebound imposed att=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 varianceR=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.25inside the horizon and\sigma_\star^2=0afterwards.
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.
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.
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 relevantm,\delta.\undefined G(P^{\mathrm{TTC}},m)is row–stochastic for allm.
*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:*
(Mean–square boundedness)
\Sigma_t\to\Sigma_\infty\succeq0and\lim_{t\to\infty}\mathbb E\|e_t\|^2=\mathrm{tr}\,\Sigma_\infty<\infty.(Exponential convergence under hard anchoring) If
Q=0andR=0for allt\ge T_F, thene_t\to 0exponentially andmath \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. ◻