1. Promise
One-sentence promise
After this lecture, you can assess robustness via parameter sensitivity, subperiod stability, and simple stress tests, and you can convert results into safe-usage and monitoring language.
End tag line
Robust models survive small shocks.
2. Learning objectives
After reading this lecture, you will be able to:
- Run sensitivity analysis on key parameters (horizon, window sizes, thresholds).
- Evaluate stability across subperiods and regimes, not only on average.
- Perform simple stress tests and scenario analysis in a defensible way.
- Summarise robustness evidence in a format a validator or stakeholder accepts.
3. Conceptual core
3.1 Sensitivity to parameters
What it means
Your pipeline has parameters that are not “learned” but chosen:
- horizon ,
- feature windows (e.g., rolling vol window),
- thresholds (e.g., trade if score > 0),
- universe filters (liquidity cutoff),
- regularisation strength .
Robustness asks:
If you change these within a reasonable range, does performance:
- stay similar,
- degrade smoothly,
- or collapse unpredictably?
Interpretation
- Smooth degradation is acceptable.
- Performance cliffs indicate fragility and overfitting to a choice.
Validator sentence
If one “magic number” makes the result, you do not have a result.
3.2 Stability across subperiods
Why it matters
Markets are not stationary. You must show that performance is not concentrated in one lucky interval.
Subperiod views:
- by calendar (years/quarters),
- by volatility regime (low/high),
- by liquidity regime,
- by event buckets (earnings weeks, crisis periods).
Robustness evidence
- fold metrics distribution,
- worst-window performance,
- rolling error,
- performance by regime.
Validator sentence
Average performance is a weak claim. Worst-window is a real claim.
3.3 Stress tests / scenario analysis (simple is fine)
Stress testing is not “predict the crisis perfectly”. Stress testing is:
“How does the model behave under adverse but plausible conditions?”
Simple defensible stress tests:
- Volatility shock: multiply returns noise by 2–3.
- Liquidity shock proxy: add a spread/slippage penalty.
- Missing data shock: drop or mask a block of observations.
- Correlation breakdown: scramble relationships between features and target.
The deliverable is:
- a table: baseline vs stressed metrics,
- a statement: “model is disabled or exposure reduced under these conditions”.
Validator sentence
Stress tests exist to define safe fallback behaviour.
4. Robustness evidence pack (what you deliver)
A robustness section should include:
- Parameter sensitivity table (grid).
- Subperiod stability plot (rolling metric).
- Stress test table (baseline vs stress).
- A short conclusion:
- safe usage envelope,
- triggers and actions.
5. Control lens
Robust control asks: - “What if the plant parameters change?” - “What if noise increases?”
Robust modelling is the same: - parameter drift, - volatility regime shifts, - missing signals.
Finance just adds incentives and execution.
6. Python lab (sensitivity grid + subperiod stability + stress tests)
This lab is fast and produces:
- sensitivity results for horizon and vol window ,
- subperiod RMSE and rolling RMSE,
- three stress tests.
We use a simple baseline model so the focus is on robustness protocol.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
# ---------------------------------------------------------
# 1) Generate a time series with a regime change
# ---------------------------------------------------------
T = 700
t = np.arange(T)
idx = pd.date_range("2024-01-01", periods=T, freq="B")
season = 0.3 * np.sin(2*np.pi*t/5)
sigma = np.ones(T) * 0.7
sigma[T//2:] = 1.5 # volatility shock regime
y = np.zeros(T)
for k in range(1, T):
y[k] = 0.25*y[k-1] + season[k] + sigma[k]*rng.normal()
df0 = pd.DataFrame({"y": y}, index=idx)
# ---------------------------------------------------------
# 2) Feature builder and evaluation (simple OLS baseline)
# ---------------------------------------------------------
def build(df, h=1, W=20):
d = df.copy()
d["r1"] = d["y"].diff()
d["y_target"] = d["r1"].shift(-h) # predict future change
d["lag1"] = d["r1"].shift(1)
d["lag2"] = d["r1"].shift(2)
d[f"vol_{W}"] = d["r1"].rolling(W).std()
d = d.dropna()
return d
def rmse(y_true, y_hat):
return float(np.sqrt(np.mean((y_true - y_hat)**2)))
def fit_predict_ols(train, test, feat_cols):
Xtr = np.column_stack([np.ones(len(train))] + [train[c].values for c in feat_cols])
ytr = train["y_target"].values
beta = np.linalg.lstsq(Xtr, ytr, rcond=None)[0]
Xte = np.column_stack([np.ones(len(test))] + [test[c].values for c in feat_cols])
yhat = Xte @ beta
return yhat
def eval_split(d, split_idx):
train = d.iloc[:split_idx]
test = d.iloc[split_idx:]
feat_cols = [c for c in d.columns if c.startswith("lag") or c.startswith("vol_")]
yhat = fit_predict_ols(train, test, feat_cols)
return rmse(test["y_target"].values, yhat), test.index, test["y_target"].values, yhat
split_idx = len(df0) // 2 # train on first half, test on second half
# ---------------------------------------------------------
# 3) (A) Sensitivity grid: horizon h and vol window W
# ---------------------------------------------------------
Hs = [1, 3, 5]
Ws = [10, 20, 40]
rows = []
for h in Hs:
for W in Ws:
d = build(df0, h=h, W=W)
# adjust split for dropped rows
split = len(d) // 2
score, _, _, _ = eval_split(d, split)
rows.append({"horizon_h": h, "vol_window_W": W, "test_rmse": score})
sens_df = pd.DataFrame(rows).sort_values(["horizon_h", "vol_window_W"])
print("=== Sensitivity grid (test RMSE) ===")
print(sens_df.to_string(index=False))
# ---------------------------------------------------------
# 4) (B) Stability across subperiods: RMSE by quarter + rolling RMSE
# ---------------------------------------------------------
d = build(df0, h=1, W=20)
split = len(d) // 2
score, test_idx, ytrue, yhat = eval_split(d, split)
test = d.iloc[split:].copy()
test["err2"] = (test["y_target"].values - yhat)**2
test["rmse_roll_40"] = test["err2"].rolling(40).mean().apply(np.sqrt)
rmse_by_q = test.groupby(pd.Grouper(freq="Q"))["err2"].mean().apply(np.sqrt)
plt.figure(figsize=(8, 3.8))
plt.plot(test.index, test["rmse_roll_40"], label="rolling RMSE (40)")
plt.title("Stability check: rolling RMSE on test period")
plt.xlabel("date")
plt.ylabel("RMSE")
plt.legend()
plt.tight_layout()
plt.show()
plt.figure(figsize=(7.5, 3.5))
plt.plot(rmse_by_q.index.astype(str), rmse_by_q.values, marker="o")
plt.title("Subperiod stability: RMSE by quarter (test)")
plt.xlabel("quarter")
plt.ylabel("RMSE")
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
plt.show()
print("\nRMSE by quarter (test):")
print(rmse_by_q)
# ---------------------------------------------------------
# 5) (C) Stress tests (simple and defensible)
# Baseline vs stressed metrics
# ---------------------------------------------------------
def stress_volatility(df, factor=2.0):
d = df.copy()
eps = rng.normal(0, 1, len(d))
d["y_stress"] = d["y"].values + (factor - 1.0) * eps # add extra noise
return d.rename(columns={"y_stress": "y"})[["y"]]
def stress_missing_block(df, start=450, end=520):
d = df.copy()
d.iloc[start:end] = np.nan
d = d.ffill() # policy must be explicit
return d
def stress_feature_break(df):
# Break relationship by permuting y (destroys dependence structure)
d = df.copy()
d["y"] = rng.permutation(d["y"].values)
return d
def run_one(df_in, label):
d = build(df_in, h=1, W=20)
split = len(d) // 2
score, _, _, _ = eval_split(d, split)
return {"scenario": label, "test_rmse": score}
base = run_one(df0[["y"]], "baseline")
vols = run_one(stress_volatility(df0[["y"]], factor=2.0), "volatility_shock_x2")
miss = run_one(stress_missing_block(df0[["y"]], start=450, end=520), "missing_block_ffill")
perm = run_one(stress_feature_break(df0[["y"]]), "dependence_break_permute")
stress_df = pd.DataFrame([base, vols, miss, perm])
print("\n=== Stress test table (test RMSE) ===")
print(stress_df.to_string(index=False))7. How to explain robustness results (stakeholder language)
Parameter sensitivity
- “We tested horizon and window choices in a reasonable range.”
- “Performance changes smoothly; no single magic value drives the result.”
- “If a cliff exists, we treat it as fragility and tighten usage scope.”
Subperiod stability
- “We report performance by quarter and rolling windows.”
- “We focus on the worst period, not the average.”
- “If failures cluster in high-vol periods, we restrict usage or adjust controls.”
Stress tests
- “Under volatility shock, error increases as expected.”
- “Under missing data, we define explicit fallback behaviour.”
- “Under dependence break, performance collapses, which confirms the model relies on that structure.”
Validator sentence
Robustness is evidence that your result is not an accident.
8. Summary
- Robustness is sensitivity + stability + stress.
- Sensitivity checks prevent “magic parameter” results.
- Subperiod stability prevents lucky window narratives.
- Stress tests define safe usage and fallback behaviour.
- Simple protocols beat complex stories.