---
title: "Chapter 30. Model Risk, Calibration, and Financial AI"
---
::: {.chapter-objectives}
## Learning Goals
By the end of this chapter, students should be able to:
- explain **model risk** as the risk that a financial model is wrong, misused, poorly calibrated, or unstable outside its assumptions;
- distinguish **estimation**, **calibration**, **validation**, **backtesting**, and **stress testing**;
- formulate calibration as a weighted inverse problem;
- diagnose identifiability, ill-conditioning, overfitting, extrapolation risk, and data leakage;
- explain parameter uncertainty and model uncertainty using clean mathematical language;
- use no-arbitrage restrictions as model-validation constraints;
- understand how machine learning can be used for pricing, hedging, risk forecasting, and calibration surrogates;
- describe the mathematical risks of AI-assisted financial modeling, including hallucinated formulas, hidden assumptions, regime shift, and false confidence;
- write Python code to calibrate simple models, test robustness, and audit numerical outputs.
:::
::: {.hero-box}
Mathematical finance is not only about deriving elegant formulas. It is also about knowing **when formulas fail**.
A model is a map
$$
M_\theta : \text{inputs} \longrightarrow \text{prices, hedges, risks, or decisions},
$$
where $\theta$ is a vector of parameters. Calibration chooses parameters so that the model approximately matches observed market quantities:
$$
\widehat \theta
=
\arg\min_{\theta\in \Theta}
\sum_{i=1}^n
w_i\left(Y_i^{\mathrm{mkt}}-M_i(\theta)\right)^2.
$$
This chapter studies the mathematics behind that optimization problem. We emphasize a central lesson: **a calibrated model is not necessarily a correct model**.
:::
## 30.1 Why End with Model Risk?
The earlier chapters built a sequence of increasingly sophisticated models:
- one-period no-arbitrage models;
- binomial trees;
- martingale pricing;
- Brownian motion and Itô calculus;
- Black--Scholes;
- stochastic volatility;
- jump models;
- interest-rate models;
- portfolio models;
- risk measures;
- credit-risk models.
Each model is useful because it reduces a complicated market problem to a mathematical structure. But every reduction loses information. The Black--Scholes model assumes continuous trading, constant volatility, no transaction costs, no jumps, and frictionless markets. A mean--variance portfolio model assumes that covariance is a sufficiently informative description of risk. A credit-intensity model assumes that default can be described by a stochastic intensity.
::: {.definition}
## Model risk
**Model risk** is the risk of financial loss, poor decision-making, or misleading measurement caused by errors in model specification, calibration, implementation, interpretation, or use.
:::
Model risk has several mathematical sources:
1. **Specification error:** the true data-generating mechanism is not in the model family.
2. **Parameter uncertainty:** even if the model family is reasonable, the parameter estimate is uncertain.
3. **Numerical error:** the model is correct, but the computed output is inaccurate.
4. **Implementation error:** the formula is coded incorrectly.
5. **Data error:** the inputs are stale, noisy, biased, or inconsistent.
6. **Extrapolation error:** the model is used outside the region where it was calibrated.
7. **Optimization error:** calibration finds a local or unstable minimum.
8. **Governance error:** users interpret model output as truth rather than as conditional output under assumptions.
::: {.remark}
## The mathematical habit
Whenever a model produces a number, ask:
$$
\text{number} = \text{output of which function, under which assumptions, with which inputs?}
$$
This question is more important than the number itself.
:::
## 30.2 Models as Mathematical Maps
A model can be described as a parameterized map
$$
M_\theta:X\to Y,
$$
where:
- $X$ is an input space;
- $Y$ is an output space;
- $\theta\in \Theta$ is a parameter vector;
- $M_\theta(x)$ is the model output for input $x$.
For option pricing, $x$ may contain spot price, strike, maturity, interest rate, dividend yield, and volatility. For risk management, $x$ may contain positions, historical returns, risk factors, and horizon. For credit risk, $x$ may contain exposure, recovery, default probability, and dependence parameters.
::: {.example}
## Black--Scholes as a map
For a European call option, the Black--Scholes model is the map
$$
M_\sigma(S_0,K,T,r)=S_0N(d_1)-Ke^{-rT}N(d_2),
$$
where
$$
d_1=
\frac{\log(S_0/K)+(r+\frac12\sigma^2)T}{\sigma\sqrt T},
\qquad
d_2=d_1-\sigma\sqrt T.
$$
If $S_0$, $K$, $T$, and $r$ are observed and the market call price is $C^{\mathrm{mkt}}$, then implied volatility is a parameter $\widehat\sigma$ satisfying approximately
$$
M_{\widehat\sigma}(S_0,K,T,r)=C^{\mathrm{mkt}}.
$$
:::
::: {.definition}
## Model output error
Given observed quantities $Y_i^{\mathrm{mkt}}$ and model quantities $M_i(\theta)$, the residual is
$$
r_i(\theta)=Y_i^{\mathrm{mkt}}-M_i(\theta).
$$
A small residual means the model fits the chosen observations. It does not prove that the model is true.
:::
## 30.3 Estimation, Calibration, Validation, and Backtesting
Students often use the words estimation and calibration interchangeably. They are related but not identical.
::: {.definition}
## Estimation
**Estimation** usually means choosing parameters from historical data under a statistical model. A typical estimator is
$$
\widehat\theta=\arg\max_{\theta\in\Theta} L(\theta;X_1,\ldots,X_n),
$$
where $L$ is a likelihood function.
:::
::: {.definition}
## Calibration
**Calibration** usually means choosing parameters so that the model matches observed market prices or curves. A typical calibration problem is
$$
\widehat\theta
=
\arg\min_{\theta\in\Theta}
\sum_{i=1}^n w_i
\left(Y_i^{\mathrm{mkt}}-M_i(\theta)\right)^2.
$$
:::
::: {.definition}
## Validation
**Validation** is the process of checking whether the model is appropriate for its intended use. Validation includes checking assumptions, input quality, implementation, calibration stability, out-of-sample performance, and sensitivity to stress scenarios.
:::
::: {.definition}
## Backtesting
**Backtesting** compares model forecasts with realized outcomes. For a sequence of predicted losses or risk measures, backtesting studies whether realized data are statistically consistent with model predictions.
:::
The distinction matters because different tasks use different probability measures. Historical estimation usually takes place under the physical measure $\mathbb P$. Derivative pricing often takes place under a risk-neutral measure $\mathbb Q$. Confusing these two is a common source of model misuse.
## 30.4 Calibration as an Inverse Problem
A forward problem computes model outputs from parameters:
$$
\theta \longmapsto M(\theta).
$$
A calibration problem tries to invert this map from observed outputs:
$$
Y^{\mathrm{mkt}}\approx M(\theta).
$$
This is an inverse problem. Inverse problems are often ill-conditioned: a small change in observed data may produce a large change in calibrated parameters.
::: {.definition}
## Weighted least-squares calibration
Let $Y^{\mathrm{mkt}}\in \mathbb R^n$ be observed market quantities and let $M(\theta)\in \mathbb R^n$ be model outputs. Given weights $w_i>0$, define
$$
J(\theta)=
\sum_{i=1}^n w_i\left(Y_i^{\mathrm{mkt}}-M_i(\theta)\right)^2.
$$
A calibrated parameter is any minimizer
$$
\widehat\theta\in\arg\min_{\theta\in\Theta}J(\theta).
$$
:::
Weights encode priorities. For example, one may put more weight on liquid instruments and less weight on stale or illiquid prices. A common statistical choice is
$$
w_i=\frac{1}{\operatorname{Var}(\varepsilon_i)},
$$
when the observation error satisfies
$$
Y_i^{\mathrm{mkt}}=M_i(\theta_0)+\varepsilon_i.
$$
::: {.remark}
## Calibration objective is a modeling choice
Choosing squared price errors, squared implied-volatility errors, relative errors, bid--ask normalized errors, or likelihood-based errors can produce different calibrated parameters. The objective function is part of the model.
:::
## 30.5 First-Order Conditions and Sensitivity
Suppose $M:\mathbb R^p\to\mathbb R^n$ is differentiable and
$$
J(\theta)=\|Y-M(\theta)\|_W^2
=(Y-M(\theta))^T W(Y-M(\theta)),
$$
where $W$ is a positive definite diagonal matrix. Let
$$
r(\theta)=Y-M(\theta),
$$
and let $D M(\theta)$ be the $n\times p$ Jacobian matrix. Then
$$
\nabla J(\theta)
= -2 D M(\theta)^T W r(\theta).
$$
At an interior optimum $\widehat\theta$,
$$
D M(\widehat\theta)^T W
\left(Y-M(\widehat\theta)\right)=0.
$$
This equation says that the weighted residual is orthogonal to the tangent space of model outputs at the calibrated point.
::: {.theorem}
## Local linear calibration sensitivity
Assume the model is locally approximated by
$$
M(\theta+\Delta\theta)
\approx M(\theta)+A\Delta\theta,
$$
where $A=D M(\theta)$. Then the local least-squares parameter change produced by a data perturbation $\Delta Y$ is approximately
$$
\Delta\theta
\approx (A^TWA)^{-1}A^TW\Delta Y,
$$
provided $A^TWA$ is invertible.
:::
This formula explains why calibration can be unstable. If $A^TWA$ is nearly singular, then small market noise can produce large parameter changes.
::: {.definition}
## Identifiability
A parameter $\theta$ is **identifiable** from observations $Y$ if different parameter values produce distinguishable model outputs. Locally, identifiability requires the Jacobian $D M(\theta)$ to have full column rank.
:::
## 30.6 Interactive: Calibration to a Volatility Smile
The following interactive demo shows a simple model-risk problem. Suppose market prices imply a volatility smile, but the model tries to fit the entire smile with one constant Black--Scholes volatility. The calibrated constant volatility may minimize squared error, but the residual pattern reveals misspecification.
::: {.interactive-card}
### Interactive demo: fitting one volatility to a smile
Move the smile-curvature slider. The dots represent a synthetic implied-volatility smile. The horizontal line is the best constant-volatility fit. The residual plot shows the remaining systematic pattern.
<div class="mf-control-row">
<label for="ch30-smile-curvature">Smile curvature:</label>
<input id="ch30-smile-curvature" type="range" min="0" max="0.45" step="0.01" value="0.20">
<span id="ch30-smile-curvature-value" class="mf-output">0.20</span>
</div>
<div id="ch30-smile-fit" class="plotly-demo"></div>
<div id="ch30-smile-residuals" class="plotly-demo"></div>
:::
```{=html}
<script>
window.mfPlotlyReady(function() {
var slider = document.getElementById("ch30-smile-curvature");
var value = document.getElementById("ch30-smile-curvature-value");
var fitDiv = document.getElementById("ch30-smile-fit");
var resDiv = document.getElementById("ch30-smile-residuals");
if (!slider || !fitDiv || !resDiv) return;
function linspace(a, b, n) {
var arr = [];
for (var i = 0; i < n; i++) arr.push(a + (b - a) * i / (n - 1));
return arr;
}
function update() {
var curvature = Number(slider.value);
value.textContent = curvature.toFixed(2);
var strikes = linspace(70, 130, 25);
var vols = strikes.map(function(k) {
var m = Math.log(k / 100);
return 0.18 + curvature * m * m + 0.04 * Math.max(-m, 0);
});
var meanVol = vols.reduce(function(a,b) { return a + b; }, 0) / vols.length;
var fitted = strikes.map(function() { return meanVol; });
var residuals = vols.map(function(v) { return v - meanVol; });
Plotly.react(fitDiv, [
{x: strikes, y: vols, mode: "markers+lines", name: "market implied vol"},
{x: strikes, y: fitted, mode: "lines", name: "best constant vol"}
], {
title: "Calibrating a Constant-Volatility Model to a Smile",
xaxis: {title: "strike K"},
yaxis: {title: "implied volatility", tickformat: ".1%"},
margin: {l: 60, r: 20, t: 55, b: 55}
}, {responsive: true});
Plotly.react(resDiv, [
{x: strikes, y: residuals, type: "bar", name: "residual"}
], {
title: "Residual Pattern: Evidence of Model Misspecification",
xaxis: {title: "strike K"},
yaxis: {title: "market vol - fitted vol", tickformat: ".1%"},
margin: {l: 60, r: 20, t: 55, b: 55}
}, {responsive: true});
}
slider.addEventListener("input", update);
update();
});
</script>
```
A good model diagnostic does not only report the loss value $J(\widehat\theta)$. It also examines the structure of the residuals. Random-looking residuals may indicate noise. Systematic residuals often indicate misspecification.
## 30.7 Worked Example: Implied Volatility as Calibration
Suppose a European call has observed market price $C^{\mathrm{mkt}}$. Under Black--Scholes, the model price is
$$
C^{\mathrm{BS}}(\sigma)=S_0N(d_1)-Ke^{-rT}N(d_2).
$$
The implied volatility $\widehat\sigma$ solves
$$
C^{\mathrm{BS}}(\widehat\sigma)=C^{\mathrm{mkt}}.
$$
Equivalently,
$$
\widehat\sigma
=
\arg\min_{\sigma>0}
\left(C^{\mathrm{mkt}}-C^{\mathrm{BS}}(\sigma)\right)^2.
$$
::: {.remark}
## Implied volatility is not historical volatility
Historical volatility is estimated from realized returns under the physical measure. Implied volatility is extracted from market option prices under a pricing model. It is a model-dependent summary of option prices, not a directly observed physical standard deviation.
:::
```{python}
#| label: ch30-implied-vol-bisection
#| message: false
#| warning: false
import math
def N(x):
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
def bs_call(S0, K, T, r, sigma):
if T <= 0:
return max(S0 - K, 0.0)
d1 = (math.log(S0 / K) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
d2 = d1 - sigma * math.sqrt(T)
return S0 * N(d1) - K * math.exp(-r * T) * N(d2)
def implied_vol_bisection(price, S0, K, T, r, low=1e-6, high=5.0, tol=1e-10):
intrinsic = max(S0 - K * math.exp(-r * T), 0.0)
upper_bound = S0
if not (intrinsic <= price <= upper_bound):
raise ValueError("Price violates simple no-arbitrage bounds.")
for _ in range(200):
mid = 0.5 * (low + high)
model_price = bs_call(S0, K, T, r, mid)
if abs(model_price - price) < tol:
return mid
if model_price < price:
low = mid
else:
high = mid
return 0.5 * (low + high)
S0, K, T, r = 100.0, 105.0, 1.0, 0.04
market_price = 9.25
sigma_hat = implied_vol_bisection(market_price, S0, K, T, r)
print("implied volatility:", round(sigma_hat, 4))
print("model price check:", round(bs_call(S0, K, T, r, sigma_hat), 4))
```
The bisection method works because, for a European call, the Black--Scholes price is increasing in volatility. The derivative with respect to volatility is the vega:
$$
\frac{\partial C}{\partial \sigma}=S_0\sqrt T\,\varphi(d_1)>0
$$
for nondegenerate parameters.
## 30.8 Calibration Is Not Truth
Consider two models $M^{(1)}$ and $M^{(2)}$. It is possible that
$$
\|Y^{\mathrm{mkt}}-M^{(1)}(\widehat\theta_1)\|
<
\|Y^{\mathrm{mkt}}-M^{(2)}(\widehat\theta_2)\|
$$
on today’s calibration instruments, while $M^{(2)}$ gives better hedges or better out-of-sample predictions. Calibration quality depends on the objective and dataset.
Important questions include:
- Which instruments were used for calibration?
- Are they liquid?
- Are bid--ask spreads included?
- Are prices synchronous?
- Are the parameters stable across days?
- Does the model fit prices but fail hedges?
- Is the model used for interpolation or extrapolation?
::: {.definition}
## In-sample and out-of-sample error
The **in-sample error** is the error on data used to fit the model. The **out-of-sample error** is the error on data not used for fitting. Overfitting occurs when a model has small in-sample error but large out-of-sample error.
:::
## 30.9 Interactive: Overfitting and Out-of-Sample Error
The next demo uses polynomial regression as an analogy. A high-degree model can fit training data very well but behave poorly between or beyond observations. In finance, similar behavior appears when too many parameters are calibrated to too few instruments.
::: {.interactive-card}
### Interactive demo: model complexity and overfitting
Move the polynomial degree slider. The training error usually decreases with degree, but the test error can increase.
<div class="mf-control-row">
<label for="ch30-poly-degree">Polynomial degree:</label>
<input id="ch30-poly-degree" type="range" min="1" max="12" step="1" value="3">
<span id="ch30-poly-degree-value" class="mf-output">3</span>
</div>
<div id="ch30-overfit-curve" class="plotly-demo"></div>
<div id="ch30-overfit-error" class="plotly-demo"></div>
:::
```{=html}
<script>
window.mfPlotlyReady(function() {
var slider = document.getElementById("ch30-poly-degree");
var value = document.getElementById("ch30-poly-degree-value");
var curveDiv = document.getElementById("ch30-overfit-curve");
var errorDiv = document.getElementById("ch30-overfit-error");
if (!slider || !curveDiv || !errorDiv) return;
var xTrain = [-0.95,-0.82,-0.71,-0.58,-0.43,-0.31,-0.22,-0.05,0.08,0.19,0.34,0.49,0.62,0.77,0.91];
var yTrain = xTrain.map(function(x, i) {
var noise = [0.12,-0.05,0.03,-0.11,0.08,0.01,-0.04,0.10,-0.08,0.04,-0.02,0.07,-0.07,0.03,-0.09][i];
return Math.sin(2.5 * Math.PI * x) + noise;
});
var xTest = [];
var yTest = [];
for (var i = 0; i < 80; i++) {
var x = -1 + 2 * i / 79;
xTest.push(x);
yTest.push(Math.sin(2.5 * Math.PI * x));
}
function solve(A, b) {
var n = b.length;
var M = A.map(function(row, i) { return row.slice().concat([b[i]]); });
for (var k = 0; k < n; k++) {
var pivot = k;
for (var i = k + 1; i < n; i++) {
if (Math.abs(M[i][k]) > Math.abs(M[pivot][k])) pivot = i;
}
var tmp = M[k]; M[k] = M[pivot]; M[pivot] = tmp;
var div = M[k][k];
if (Math.abs(div) < 1e-12) div = div >= 0 ? 1e-12 : -1e-12;
for (var j = k; j <= n; j++) M[k][j] /= div;
for (var i = 0; i < n; i++) {
if (i === k) continue;
var factor = M[i][k];
for (var j = k; j <= n; j++) M[i][j] -= factor * M[k][j];
}
}
return M.map(function(row) { return row[n]; });
}
function polyFit(x, y, degree) {
var m = degree + 1;
var A = [];
var b = [];
for (var i = 0; i < m; i++) {
A[i] = [];
for (var j = 0; j < m; j++) {
var sum = 0;
for (var k = 0; k < x.length; k++) sum += Math.pow(x[k], i + j);
A[i][j] = sum;
}
var s = 0;
for (var k = 0; k < x.length; k++) s += y[k] * Math.pow(x[k], i);
b[i] = s;
}
return solve(A, b);
}
function predict(coef, x) {
var y = 0;
var power = 1;
for (var i = 0; i < coef.length; i++) {
y += coef[i] * power;
power *= x;
}
return y;
}
function mse(y, yhat) {
var s = 0;
for (var i = 0; i < y.length; i++) s += Math.pow(y[i] - yhat[i], 2);
return s / y.length;
}
function update() {
var degree = Number(slider.value);
value.textContent = degree.toString();
var coef = polyFit(xTrain, yTrain, degree);
var xGrid = [];
var yFit = [];
for (var i = 0; i < 200; i++) {
var x = -1 + 2 * i / 199;
xGrid.push(x);
yFit.push(predict(coef, x));
}
var trainPred = xTrain.map(function(x) { return predict(coef, x); });
var testPred = xTest.map(function(x) { return predict(coef, x); });
var trainMse = mse(yTrain, trainPred);
var testMse = mse(yTest, testPred);
Plotly.react(curveDiv, [
{x: xTrain, y: yTrain, mode: "markers", name: "training data"},
{x: xTest, y: yTest, mode: "lines", name: "true function"},
{x: xGrid, y: yFit, mode: "lines", name: "fitted model"}
], {
title: "Higher Complexity Can Fit Noise",
xaxis: {title: "input"},
yaxis: {title: "output"},
margin: {l: 55, r: 20, t: 55, b: 55}
}, {responsive: true});
Plotly.react(errorDiv, [
{x: ["training MSE", "test MSE"], y: [trainMse, testMse], type: "bar"}
], {
title: "Training Error Versus Test Error",
yaxis: {title: "mean squared error"},
margin: {l: 60, r: 20, t: 55, b: 55}
}, {responsive: true});
}
slider.addEventListener("input", update);
update();
});
</script>
```
The analogy is direct: adding parameters to a volatility surface, credit model, or portfolio optimizer may improve in-sample fit while making the model less stable.
## 30.10 Parameter Uncertainty
A calibrated parameter is itself uncertain. Suppose
$$
Y=M(\theta_0)+\varepsilon,
$$
where $\varepsilon$ is observation noise. Let $\widehat\theta$ solve a least-squares problem. Under a local linear approximation,
$$
\widehat\theta-\theta_0
\approx
(A^TWA)^{-1}A^TW\varepsilon.
$$
If $\operatorname{Cov}(\varepsilon)=\Sigma_\varepsilon$, then
$$
\operatorname{Cov}(\widehat\theta)
\approx
(A^TWA)^{-1}A^TW\Sigma_\varepsilon W A(A^TWA)^{-1}.
$$
When $W=\Sigma_\varepsilon^{-1}$, this simplifies to
$$
\operatorname{Cov}(\widehat\theta)
\approx
(A^T\Sigma_\varepsilon^{-1}A)^{-1}.
$$
::: {.remark}
## Uncertainty propagates
If a risk number is
$$
R=g(\widehat\theta),
$$
then the delta-method approximation gives
$$
\operatorname{Var}(R)
\approx
\nabla g(\theta_0)^T
\operatorname{Cov}(\widehat\theta)
\nabla g(\theta_0).
$$
A single reported risk number without uncertainty is incomplete.
:::
```{python}
#| label: ch30-calibration-sensitivity
#| message: false
#| warning: false
import numpy as np
# Linearized calibration model: Y = A theta + noise
A = np.array([
[1.00, 0.20],
[1.00, 0.50],
[1.00, 0.80],
[1.00, 1.10],
[1.00, 1.40]
])
theta_true = np.array([0.15, 0.04])
noise_sd = 0.005
rng = np.random.default_rng(7339)
n_rep = 5000
estimates = []
for _ in range(n_rep):
y = A @ theta_true + rng.normal(0, noise_sd, size=A.shape[0])
theta_hat = np.linalg.lstsq(A, y, rcond=None)[0]
estimates.append(theta_hat)
estimates = np.array(estimates)
print("true theta:", theta_true)
print("mean estimate:", estimates.mean(axis=0).round(5))
print("std estimate:", estimates.std(axis=0, ddof=1).round(5))
print("condition number of A^T A:", round(np.linalg.cond(A.T @ A), 2))
```
The condition number measures sensitivity. A high condition number means the calibration problem is close to being singular.
## 30.11 No-Arbitrage as a Validation Constraint
Model outputs should satisfy basic no-arbitrage restrictions. For European call prices with fixed maturity and strikes $K$, absence of static arbitrage implies:
1. Call prices are nonnegative:
$$
C(K)\ge 0.
$$
2. Call prices are decreasing in strike:
$$
K_1<K_2 \quad\Longrightarrow\quad C(K_1)\ge C(K_2).
$$
3. Call prices are convex in strike:
$$
C(\lambda K_1+(1-\lambda)K_2)
\le
\lambda C(K_1)+(1-\lambda)C(K_2),
\qquad 0\le \lambda\le 1.
$$
For equally spaced strikes, convexity is checked by second differences:
$$
C(K_{i-1})-2C(K_i)+C(K_{i+1})\ge 0.
$$
::: {.example}
## Butterfly arbitrage
If
$$
C(K_{i-1})-2C(K_i)+C(K_{i+1})<0,
$$
then a butterfly portfolio has negative cost and nonnegative payoff, which is a static arbitrage.
:::
```{python}
#| label: ch30-no-arbitrage-call-check
#| message: false
#| warning: false
import numpy as np
K = np.array([80, 90, 100, 110, 120], dtype=float)
C_good = np.array([22.5, 14.4, 8.1, 4.1, 1.9])
C_bad = np.array([22.5, 14.4, 10.5, 4.1, 1.9])
def call_arbitrage_report(K, C):
decreasing = np.all(np.diff(C) <= 1e-12)
second_diff = C[:-2] - 2 * C[1:-1] + C[2:]
convex = np.all(second_diff >= -1e-12)
return {
"decreasing": bool(decreasing),
"convex": bool(convex),
"second_differences": second_diff.round(4)
}
print("good prices:", call_arbitrage_report(K, C_good))
print("bad prices:", call_arbitrage_report(K, C_bad))
```
Machine-learning models for option prices should also obey these restrictions. A neural network that fits prices but violates monotonicity or convexity may create arbitrage-like outputs.
## 30.12 Stress Testing and Scenario Analysis
A model is calibrated near current market conditions. Risk management asks what happens under extreme but plausible changes.
::: {.definition}
## Stress test
A **stress test** evaluates model outputs under severe scenarios such as large interest-rate shifts, volatility shocks, liquidity shocks, correlation breakdowns, default clusters, or regime changes.
:::
Mathematically, a stress test replaces an input $x$ by a shocked input
$$
x^{\mathrm{stress}}=x+\Delta x
$$
and studies
$$
M_\theta(x^{\mathrm{stress}})-M_\theta(x).
$$
For a first-order approximation,
$$
M_\theta(x+\Delta x)-M_\theta(x)
\approx
D_xM_\theta(x)\Delta x.
$$
For nonlinear portfolios, second-order terms may be essential:
$$
M_\theta(x+\Delta x)-M_\theta(x)
\approx
D_xM_\theta(x)\Delta x
+rac12 \Delta x^T D_x^2M_\theta(x)\Delta x.
$$
The first derivative corresponds to delta or duration. The second derivative corresponds to gamma or convexity.
```{python}
#| label: ch30-stress-test-option
#| message: false
#| warning: false
import math
def N_scalar(x):
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
def bs_call_scalar(S0, K, T, r, sigma):
d1 = (math.log(S0 / K) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
d2 = d1 - sigma * math.sqrt(T)
return S0 * N_scalar(d1) - K * math.exp(-r * T) * N_scalar(d2)
base = {"S0": 100, "K": 100, "T": 1, "r": 0.04, "sigma": 0.20}
base_price = bs_call_scalar(**base)
scenarios = [
{"name": "spot down 10%", "S0": 90, "sigma": 0.20},
{"name": "vol up to 35%", "S0": 100, "sigma": 0.35},
{"name": "spot down and vol up", "S0": 90, "sigma": 0.35},
]
for sc in scenarios:
params = base.copy()
params.update({"S0": sc["S0"], "sigma": sc["sigma"]})
shocked_price = bs_call_scalar(**params)
print(sc["name"], "price change:", round(shocked_price - base_price, 4))
```
Stress testing is not a substitute for probabilistic modeling. It answers a different question: not what is likely, but what the model says under specified adverse scenarios.
## 30.13 Machine Learning in Mathematical Finance
Machine learning enters finance in several roles:
1. **Prediction:** forecasting returns, volatility, liquidity, default, or order flow.
2. **Pricing approximation:** approximating expensive pricing maps.
3. **Calibration surrogate:** learning a fast inverse map from prices to parameters.
4. **Hedging policy approximation:** learning dynamic hedging strategies under frictions.
5. **Risk forecasting:** estimating VaR, Expected Shortfall, or stress loss.
6. **Text and alternative data:** extracting signals from filings, news, transcripts, or reports.
7. **Anomaly detection:** finding unusual trades, quotes, or model outputs.
::: {.definition}
## ML pricing surrogate
A **pricing surrogate** is a learned function $\widehat M$ trained to approximate a slower pricing model $M$:
$$
\widehat M(x,\theta)\approx M_\theta(x).
$$
The surrogate is useful only if its approximation error is controlled in the region where it is used.
:::
::: {.definition}
## ML calibration surrogate
A **calibration surrogate** is a learned approximate inverse map
$$
\widehat G:Y^{\mathrm{mkt}}\mapsto \widehat\theta.
$$
It should be tested for stability, out-of-sample accuracy, and no-arbitrage consistency.
:::
Machine learning does not remove model risk. It often moves the risk from explicit formulas to data, architecture, loss functions, training procedures, and deployment choices.
## 30.14 Train/Test Splits and Data Leakage
In finance, time ordering matters. A random train/test split may leak future information into the training set. For time-series data, validation should respect chronology.
::: {.definition}
## Data leakage
**Data leakage** occurs when information unavailable at prediction time enters the training or evaluation process. Leakage produces overly optimistic performance estimates.
:::
Common forms of leakage include:
- using future returns to construct current features;
- normalizing data using the full sample before splitting;
- tuning hyperparameters repeatedly on the test set;
- using revised macroeconomic data instead of real-time vintages;
- using survivorship-biased asset universes;
- ignoring transaction costs and liquidity constraints.
```{python}
#| label: ch30-train-test-time-series
#| message: false
#| warning: false
import numpy as np
rng = np.random.default_rng(7243)
n = 300
x = rng.normal(size=n)
# A synthetic target with time-varying coefficient
beta = np.where(np.arange(n) < 180, 0.4, -0.2)
y = beta * x + rng.normal(scale=0.5, size=n)
split = 200
X_train = np.column_stack([np.ones(split), x[:split]])
y_train = y[:split]
X_test = np.column_stack([np.ones(n - split), x[split:]])
y_test = y[split:]
theta_hat = np.linalg.lstsq(X_train, y_train, rcond=None)[0]
train_mse = np.mean((X_train @ theta_hat - y_train)**2)
test_mse = np.mean((X_test @ theta_hat - y_test)**2)
print("fitted intercept and slope:", theta_hat.round(4))
print("train MSE:", round(train_mse, 4))
print("test MSE:", round(test_mse, 4))
```
The change in the true coefficient creates regime shift. A model that looked reasonable in the training period may fail later.
## 30.15 Reinforcement Learning and Trading
Reinforcement learning can be used to model sequential decision problems such as execution, inventory management, market making, or hedging with transaction costs. The mathematical object is usually a Markov decision process:
$$
(S_t,A_t,R_{t+1},S_{t+1}).
$$
A policy $\pi$ maps states to actions. The value function is
$$
V^\pi(s)
=
\mathbb E^\pi\left[
\sum_{t=0}^{T}\gamma^t R_{t+1}
\mid S_0=s
\right].
$$
The optimal value satisfies the Bellman equation
$$
V^*(s)=\max_a
\left\{
r(s,a)+\gamma\sum_{s'}P(s'\mid s,a)V^*(s')
\right\}.
$$
In finance, reinforcement learning is especially vulnerable to model risk because the environment is nonstationary, actions affect market outcomes, transaction costs matter, and historical simulation may not represent future execution conditions.
::: {.remark}
## RL warning
A reinforcement-learning strategy that performs well in a simulated market may fail in a real market if the simulator omits liquidity, impact, latency, constraints, or regime changes.
:::
## 30.16 AI-Assisted Mathematical Finance
Large language models and AI coding tools can help students and practitioners:
- explain formulas in plain language;
- generate prototype code;
- check dimensions and units;
- summarize assumptions;
- produce test cases;
- audit documentation;
- compare alternative modeling choices.
But they introduce new risks:
- hallucinated formulas;
- incorrect sign conventions;
- missing discount factors;
- confusion between $\mathbb P$ and $\mathbb Q$;
- hidden code bugs;
- overconfident explanations;
- privacy and data-governance violations;
- inability to guarantee regulatory correctness.
::: {.ai-box}
## AI-assisted audit prompt: model assumptions
Use an AI assistant to audit a pricing model with the following prompt:
> I am using the following model in mathematical finance. List every mathematical assumption, every market assumption, and every numerical assumption. For each assumption, explain what can go wrong if it fails. Then suggest three tests I can run in Python to detect possible misuse.
A strong answer should mention the probability measure, information structure, admissible strategies, discounting convention, calibration data, numerical method, and intended use.
:::
::: {.ai-box}
## AI-assisted code-review prompt
Use an AI assistant to review code with the following prompt:
> Check this option-pricing or risk-management code for mathematical errors. Pay special attention to discounting, sign conventions, units of volatility and time, physical versus risk-neutral drift, boundary cases, vectorization errors, and no-arbitrage violations. Give corrected code only after explaining the issue.
:::
::: {.warning}
## AI is not a validation authority
AI tools can support model development, but they do not replace mathematical proof, independent implementation checks, domain expertise, governance review, or empirical validation.
:::
## 30.17 Model Governance as Mathematical Documentation
A model should be accompanied by documentation that answers:
1. What is the model purpose?
2. What are the state variables?
3. What is the probability measure?
4. What are the assumptions?
5. What parameters are estimated from history?
6. What parameters are calibrated to market prices?
7. What instruments are used for calibration?
8. What numerical method is used?
9. What tests verify the implementation?
10. What are the limitations?
11. What is the approved use case?
12. What are the prohibited uses?
::: {.definition}
## Model inventory entry
A **model inventory entry** is a structured record of a model’s purpose, owner, mathematical specification, inputs, outputs, assumptions, validation status, version, and approved use.
:::
For students, writing such documentation is an excellent way to learn mathematical finance. It forces precise thinking.
## 30.18 Python Example: Calibrating a Constant Volatility to Multiple Options
The following example creates synthetic option prices from a volatility smile and fits one constant volatility. The residuals reveal that the model is misspecified.
```{python}
#| label: ch30-constant-vol-calibration
#| message: false
#| warning: false
import numpy as np
import math
S0, T, r = 100.0, 1.0, 0.03
strikes = np.linspace(75, 125, 11)
def N_vec(x):
erf_vec = np.vectorize(math.erf)
return 0.5 * (1.0 + erf_vec(x / np.sqrt(2.0)))
def bs_call_vec(S0, K, T, r, sigma):
K = np.asarray(K, dtype=float)
d1 = (np.log(S0 / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
return S0 * N_vec(d1) - K * np.exp(-r * T) * N_vec(d2)
# Synthetic market smile in implied volatility
market_vols = 0.18 + 0.25 * (np.log(strikes / S0))**2 + 0.03 * np.maximum(-np.log(strikes / S0), 0)
market_prices = np.array([bs_call_vec(S0, [K], T, r, vol)[0] for K, vol in zip(strikes, market_vols)])
sigma_grid = np.linspace(0.05, 0.60, 2000)
losses = []
for sigma in sigma_grid:
model_prices = bs_call_vec(S0, strikes, T, r, sigma)
losses.append(np.mean((market_prices - model_prices)**2))
best_sigma = sigma_grid[int(np.argmin(losses))]
model_prices = bs_call_vec(S0, strikes, T, r, best_sigma)
residuals = market_prices - model_prices
print("best constant volatility:", round(best_sigma, 4))
print("root mean squared price error:", round(np.sqrt(np.mean(residuals**2)), 4))
print("residuals by strike:")
for K, res in zip(strikes, residuals):
print(f"K={K:6.1f}, residual={res: .4f}")
```
The fit may look acceptable by one metric but still display systematic residuals. This is one reason volatility surfaces are modeled rather than a single constant volatility.
## 30.19 Python Example: Bootstrap Parameter Uncertainty
Bootstrap methods approximate the sampling variability of calibrated parameters by repeatedly resampling or perturbing data.
```{python}
#| label: ch30-bootstrap-parameter-uncertainty
#| message: false
#| warning: false
import numpy as np
rng = np.random.default_rng(5110)
x = np.linspace(0.0, 1.0, 20)
true_theta = np.array([0.20, -0.05])
y_clean = true_theta[0] + true_theta[1] * x
noise_sd = 0.01
y_obs = y_clean + rng.normal(0, noise_sd, size=x.size)
X = np.column_stack([np.ones_like(x), x])
theta_hat = np.linalg.lstsq(X, y_obs, rcond=None)[0]
residuals = y_obs - X @ theta_hat
B = 2000
boot = []
for _ in range(B):
sampled_resid = rng.choice(residuals, size=residuals.size, replace=True)
y_boot = X @ theta_hat + sampled_resid
boot.append(np.linalg.lstsq(X, y_boot, rcond=None)[0])
boot = np.array(boot)
lower = np.percentile(boot, 2.5, axis=0)
upper = np.percentile(boot, 97.5, axis=0)
print("theta_hat:", theta_hat.round(5))
print("95% bootstrap interval for intercept:", (float(round(lower[0], 5)), float(round(upper[0], 5))))
print("95% bootstrap interval for slope:", (float(round(lower[1], 5)), float(round(upper[1], 5))))
```
The bootstrap interval communicates that calibrated parameters are estimates, not constants handed down by the market.
## 30.20 Python Example: A Simple Surrogate Model
The next example approximates Black--Scholes prices using polynomial regression in moneyness and volatility. This is not recommended as a production pricing method. Its purpose is to show the structure of a surrogate model.
```{python}
#| label: ch30-simple-surrogate-model
#| message: false
#| warning: false
import numpy as np
import math
rng = np.random.default_rng(6241)
def N_float(x):
return 0.5 * (1.0 + math.erf(x / math.sqrt(2.0)))
def bs_call_float(S0, K, T, r, sigma):
d1 = (math.log(S0 / K) + (r + 0.5 * sigma**2) * T) / (sigma * math.sqrt(T))
d2 = d1 - sigma * math.sqrt(T)
return S0 * N_float(d1) - K * math.exp(-r * T) * N_float(d2)
def features(m, sigma):
return np.array([1.0, m, sigma, m**2, m * sigma, sigma**2, m**3, sigma**3])
n_train = 800
S0, T, r = 100.0, 1.0, 0.03
m_train = rng.uniform(-0.25, 0.25, size=n_train) # log-moneyness log(S/K)
sig_train = rng.uniform(0.10, 0.50, size=n_train)
X_train = np.vstack([features(m, s) for m, s in zip(m_train, sig_train)])
y_train = np.array([
bs_call_float(S0, S0 / math.exp(m), T, r, s)
for m, s in zip(m_train, sig_train)
])
coef = np.linalg.lstsq(X_train, y_train, rcond=None)[0]
n_test = 200
m_test = rng.uniform(-0.25, 0.25, size=n_test)
sig_test = rng.uniform(0.10, 0.50, size=n_test)
X_test = np.vstack([features(m, s) for m, s in zip(m_test, sig_test)])
y_test = np.array([
bs_call_float(S0, S0 / math.exp(m), T, r, s)
for m, s in zip(m_test, sig_test)
])
y_pred = X_test @ coef
print("test RMSE:", round(np.sqrt(np.mean((y_test - y_pred)**2)), 4))
print("largest absolute error:", round(np.max(np.abs(y_test - y_pred)), 4))
```
A surrogate must be tested not only on random points, but also near boundaries and under the exact input distribution expected in use.
## 30.21 Common Mistakes
1. **Reporting calibrated parameters without uncertainty.**
2. **Using the same dataset for fitting and evaluation.**
3. **Confusing low calibration error with good hedging performance.**
4. **Ignoring bid--ask spreads.**
5. **Ignoring parameter constraints.**
6. **Using physical drift in a risk-neutral pricing formula.**
7. **Using annual volatility with daily time units.**
8. **Fitting a flexible model without checking arbitrage restrictions.**
9. **Using machine-learning predictions without time-aware validation.**
10. **Treating AI-generated code as correct without independent tests.**
## 30.22 AI Component: A Model-Risk Checklist
::: {.ai-box}
## AI-assisted model-risk checklist
Ask an AI assistant to produce a checklist for any model in this book. Then verify the checklist yourself.
A useful prompt is:
> Create a model-risk checklist for this model. Organize the checklist into assumptions, inputs, parameters, calibration, numerical method, validation tests, stress tests, limitations, and prohibited uses. Include formulas where appropriate. Then list mistakes a student is likely to make.
A weak answer will only describe the model. A strong answer will identify what can fail and how to test it.
:::
::: {.ai-box}
## AI-assisted theorem audit
For a theorem such as the Fundamental Theorem of Asset Pricing, Girsanov theorem, Black--Scholes PDE, or Snell-envelope recursion, use:
> State every assumption required for this theorem. For each assumption, give a counterexample or failure mode if the assumption is removed. Then explain how the assumption appears in the proof.
This prompt helps students connect assumptions to proof structure.
:::
::: {.ai-box}
## AI-assisted numerical audit
For Python code, use:
> Write unit tests for this function. Include boundary cases, monotonicity checks, no-arbitrage checks, dimensional checks, and comparisons with known special cases.
For example, a Black--Scholes call-pricing function should satisfy monotonicity in $S_0$ and $\sigma$, put--call parity when paired with a put function, and convergence to intrinsic value as $T\to 0$.
:::
## 30.23 Chapter Summary
This chapter completes the book by changing the question from
$$
\text{What does the model imply?}
$$
to
$$
\text{Should we trust the model for this purpose?}
$$
The main lessons are:
- A model is a parameterized mathematical map.
- Calibration is an inverse problem, often ill-conditioned.
- Small residuals do not prove that a model is correct.
- Parameter uncertainty propagates to prices, hedges, and risk measures.
- No-arbitrage restrictions provide powerful validation checks.
- Out-of-sample testing is essential.
- Stress testing asks different questions from probabilistic forecasting.
- Machine learning can accelerate pricing and risk modeling, but it introduces its own model risks.
- AI tools are useful assistants for explanation, coding, and auditing, but they are not substitutes for mathematical validation.
The broader message of mathematical finance is therefore:
$$
\text{A formula is useful only together with its assumptions, domain, and validation tests.}
$$
## 30.24 Exercises
### Conceptual Exercises
1. Explain the difference between estimation and calibration. Give one example of each from mathematical finance.
2. Why does small calibration error not imply that a model is correct?
3. List five sources of model risk in the Black--Scholes model.
4. Explain why a model calibrated under $\mathbb Q$ may not forecast returns under $\mathbb P$.
5. Give an example of data leakage in a financial machine-learning problem.
### Mathematical Exercises
6. Let $J(\theta)=\|Y-M(\theta)\|_W^2$. Derive the first-order condition for an interior minimizer.
7. In the local linear model $Y=A\theta+\varepsilon$, derive the least-squares estimator and its covariance matrix when $\varepsilon$ has covariance $\sigma^2I$.
8. Prove that European call prices must be decreasing in strike under no arbitrage.
9. Prove that European call prices must be convex in strike under no arbitrage.
10. Suppose a model output is $R=g(\widehat\theta)$. Use the delta method to derive an approximation for $\operatorname{Var}(R)$.
### Computational Exercises
11. Generate synthetic Black--Scholes option prices with constant volatility. Add noise and recover volatility by calibration.
12. Generate synthetic option prices from a volatility smile. Fit a constant volatility and plot residuals by strike.
13. Implement a no-arbitrage checker for call prices across strikes.
14. Simulate a regime shift and compare random train/test splitting with chronological train/test splitting.
15. Build a simple pricing surrogate for Black--Scholes calls. Test it near the edge of the training region.
### AI-Assisted Exercises
16. Ask an AI assistant to explain why calibration is an inverse problem. Improve the explanation by adding a Jacobian and condition-number discussion.
17. Ask an AI assistant to audit a Black--Scholes implementation. Check whether it catches time-unit, volatility-unit, and discounting mistakes.
18. Ask an AI assistant to generate tests for a VaR function. Add at least three tests the AI missed.
19. Ask an AI assistant to compare historical volatility and implied volatility. Identify any measure-confusion in the answer.
20. Ask an AI assistant to write a model-risk checklist for one chapter of this book. Revise the checklist into a rigorous mathematical document.