using Random, CairoMakie
# Set seed for reproducibility
Random.seed!(1234)
# Function to simulate CIR process
function cir_simulation(κ, θ, σ, r₀, Δt, num_steps, num_simulations)
= zeros(num_steps, num_simulations)
interest_rate_paths for j in 1:num_simulations
1, j] = r₀
interest_rate_paths[for i in 2:num_steps
= randn() * sqrt(Δt)
dW # dr = κ * (θ - interest_rate_paths[i-1, j]) * Δt + σ * dW # for Vasicek
= κ * (θ - interest_rate_paths[i-1, j]) * Δt + σ * sqrt(interest_rate_paths[i-1, j]) * dW
dr = max(interest_rate_paths[i-1, j] + dr, 0) # Ensure non-negativity
interest_rate_paths[i, j] end
end
return interest_rate_paths
end
let
# CIR model parameters
= 0.2 # Speed of mean reversion
κ = 0.05 # Long-term mean
θ = 0.1 # Volatility
σ
# Initial short-term interest rate
= 0.03
r₀
# Number of time steps and simulations
= 252
num_steps = 1_000
num_simulations
# Time increment
= 1 / 252
Δt # Run CIR simulation
= cir_simulation(κ, θ, σ, r₀, Δt, num_steps, num_simulations)
cir_paths
# Plot the simulated interest rate paths
= Figure()
f Axis(f[1, 1])
for i in 1:num_simulations
lines!(1:num_steps, cir_paths[:, i])
end
fend
25 Scenario Generation
“Scenarios are the rehearsal of possible futures. They are not predictions, but pathways that help us prepare for the unknown.” — Peter Schwartz
25.1 Chapter Overview
How to generate synthetic data for your model using sub-models, with applications to economic scenario generation and portfolio composition.
25.2 Common Use Cases of Scenario Generators
Scenario generators are widely used in risk management, investment analysis, and regulatory compliance to model potential future outcomes. For forecasting under the real-world (physical) measure P, use real-world (RW) scenarios. For valuation and hedging, use risk‑neutral (RN) scenarios under the martingale measure Q (e.g., with drift adjusted to the short rate minus dividends for equities and to fit the initial curve for interest rates).
25.2.1 Risk management, especially Value at Risk (VaR) & Expected Shortfall (ES).
RW scenario generators are used to simulate market movements to estimate potential portfolio losses. Basel III regulatory capital requirements have adopted these approaches.
25.2.2 Stress Testing & Regulatory Compliance
RW scenario generators can also be used to generate extreme but plausible market conditions to assess resilience, which is required by central banks and financial regulators (e.g., Federal Reserve and ECB).
25.2.3 Portfolio Optimization & Asset Allocation
RW scenario generators are used to simulate thousands of market conditions to determine optimal portfolio allocations which is commonly used in modern portfolio theory (MPT) and Black-Litterman models.
25.2.4 Pension & Insurance Risk Modeling
RW scenario generators can be used to simulate longevity risk, policyholder behavior, and interest rate movements. They are also used for economic capital estimation under uncertain economic scenarios.
25.2.5 Economic & Macro-Financial Forecasting
Central banks and institutions (e.g., IMF, World Bank) use RW scenario generators to predict macroeconomic trends.
25.2.6 Asset Pricing & Hedging
RN scenario generators help value options using stochastic models (e.g., Black-Scholes, Heston model). They can help simulate future stock price movements under different volatility conditions. They can also be used for hedging purposes to test how a portfolio performs under different inflation, interest rate, or commodity price scenarios.
25.2.7 Fixed Income & Interest Rate Modeling
Yield curve modeling uses RN scenarios to value bonds and interest rate derivatives. Swaps, swaptions, and credit default swaps (CDS) also rely on RN pricing. RN scenario generators can also simulate yield curves for bond and fixed-income pricing. Models like Cox-Ingersoll-Ross (CIR) or Hull-White generate future interest rate paths.
25.2.8 Regulatory & Accounting Valuations
IFRS 13 & fair value accounting uses RN models determine the market-consistent value of liabilities. Solvency II for insurers requires valuation of policyholder guarantees using RN scenarios.
25.3 Common Economic Scenario Generation Approaches
Economic scenario generation involves the development of plausible future economic scenarios to assess the potential impact on financial portfolios, investments, or decision-making processes. Various approaches are used to generate economic scenarios, such as adapting underlying stochastic differential equations (SDEs) for Monte Carlo scenario generation techniques.
25.3.1 Interest Rate Models
25.3.1.1 Vasicek and Cox Ingersoll Ross (CIR)
The Vasicek model is a one-factor model commonly used for simulating interest rate scenarios. It describes the dynamics of short-term interest rates using a stochastic differential equation (SDE). In a Monte Carlo simulation, we can use the Vasicek model to generate multiple interest rate paths. The CIR model is an extension of the Vasicek model with non-constant volatility. It is a square‑root diffusion with state‑dependent volatility that helps keep rates positive. It addresses the issue of negative interest rates by ensuring that interest rates remain positive. Vasicek is defined as
\[ dr(t) = \kappa (\theta - r(t)) \, dt + \sigma \, dW(t) \]
where
- \(r(t)\) is the short-term interest rate at time \(t\).
- \(κ\) is the speed of mean reversion, representing how quickly the interest rate reverts to its long-term mean.
- \(θ\) is the long-term mean or equilibrium level of the interest rate.
- \(σ\) is the volatility of the interest rate.
- \(dW(t)\) is a Wiener process or Brownian motion, representing a random shock.
And CIR is defined as
\[ dr(t) = \kappa (\theta - r(t)) \, dt + \sigma \sqrt{r(t)} \, dW(t) \]
where
- \(r(t)\) is the short-term interest rate at time \(t\).
- \(κ\) is the speed of mean reversion, representing how quickly the interest rate reverts to its long-term mean.
- \(θ\) is the long-term mean or equilibrium level of the interest rate.
- \(σ\) is the volatility of the interest rate.
- \(dW(t)\) is a Wiener process or Brownian motion, representing a random shock.
The following code shows a simplified implementation of a CIR model. The specification of \(dr\) can be changed to make it a Vasicek model.
The exact CIR process has a non-central chi-squared distribution for \(r(t)\) in closed form.
\[ \begin{aligned} d &= \frac{4 \kappa \theta}{\sigma^2} \\ c &= \frac{\sigma^2 \left( 1 - e^{-\kappa \Delta t} \right)}{4 \kappa} \\ \lambda &= \frac{4 \kappa e^{-\kappa \Delta t} r_t}{\sigma^2 \left( 1 - e^{-\kappa \Delta t} \right)} \\ r_{t+\Delta t} &\sim c \cdot \chi'^2_{d}(\lambda) \\ \end{aligned} \]
where \(\chi'^2_{d}(\lambda)\) means the non-central chi-square distribution with \(d\) degrees of freedom and a non-centrality parameter \(λ\). The following code shows an exact implementation of a CIR model.
using Distributions
let
# CIR parameters
= 0.5 # mean reversion speed
κ = 0.04 # long-term mean
θ = 0.1 # volatility coefficient
σ = 0.03 # initial rate
r0 = 5.0 # years
T = 500 # time steps
Nsteps = T / Nsteps
dt = 1000 # number of simulated paths
nsim
# Precompute constants
= 4 * κ * θ / (σ * σ)
d
# Time grid
= collect(0:dt:T)
tgrid = zeros(length(tgrid), nsim)
rpaths
# Seed reproducibility
Random.seed!(1234)
for path in 1:nsim
= r0
r 1, path] = r
rpaths[for i in 2:length(tgrid)
# Transition distribution parameters
= (σ * σ * (1 - exp(-κ * dt))) / (4 * κ)
c = (4 * κ * exp(-κ * dt) * r) / (σ * σ * (1 - exp(-κ * dt)))
λ = NoncentralChisq(d, λ)
nc_chisq = c * rand(nc_chisq)
r = r
rpaths[i, path] end
end
# Plot
= Figure(size=(800, 500))
fig = Axis(fig[1, 1], xlabel="Time (years)", ylabel="Short rate", title="Exact CIR Process Simulation (Non-central Chi-square)")
ax for path in 1:nsim
lines!(ax, tgrid, rpaths[:, path])
end
figend
25.3.1.2 Hull-White
The Hull-White model is a one-factor model that extends the Vasicek model by allowing the mean reversion and volatility parameters to be time-dependent. It is commonly used for pricing interest rate derivatives. Brace-Gatarek-Musiela (BGM) Model extends the Hull-White model to incorporate more factors. It is one of the Libor Market Model (LMM) that describes the evolution of forward rates. It allows for the modeling of both the short-rate and the entire yield curve. It is defined as
\[ dr(t) = (\theta(t) - a r(t)) \, dt + \sigma(t) \, dW(t) \]
where
- \(r(t)\) is the short-term interest rate at time \(t\).
- \(θ(t)\) is the long-term mean or equilibrium level of the interest rate.
- \(a\) is the speed of mean reversion.
- \(σ(t)\) is the time-dependent volatility of the interest rate.
- \(dW(t)\) is a Wiener process or Brownian motion, representing a random shock.
using Interpolations, Dierckx, Random, CairoMakie
function hull_white_theta_timevar_sigma(years, prices, σt; a::Float64)
@assert length(years) == length(prices) == length(σt) "times and prices and sigmas must have same length"
# Interpolate logP for smooth derivatives (cubic spline)
= log.(prices)
logP = Spline1D(years, logP, k=3) # cubic spline
interp_logP
# instantaneous forward f(0,t) = -∂_t log P(0,t)
f0(t) = -derivative(interp_logP, t)
df0(t) = -derivative(interp_logP, t, 2)
# Precompute Δs_j (left Riemann widths). assumes years[1] == 0 or handles first width as years[1].
= Vector{Float64}(undef, length(years))
Δs 1] = years[1] # if years[1]==0 this is zero; else left interval from 0
Δs[for j in 2:length(years)
= years[j] - years[j-1]
Δs[j] end
# Compute integral term I(t_i) = 1/2 * ∫_0^{t_i} e^{-2a(t_i - s)} σ(s)^2 ds
= zeros(length(years))
I for i in 1:length(years)
= years[i]
ti = 0.0
s = 0.0
acc # left Riemann: use sigma at s = years[j] for interval [years[j], years[j+1])
for j in 1:i
= years[j]
sj # width Δs[j] approximates length of [sj, sj+1) (or [0, years[1]] for j=1)
= Δs[j]
w if w <= 0
# if grid is pure points (years[1]==0 and Δs[1]==0), skip zero-width
continue
end
# integrand evaluated at left point sj
= exp(-2a * (ti - sj)) * (σt[j]^2)
integrand += integrand * w
acc end
= 0.5 * acc
I[i] end
# theta vector: df0 + a*f0 + I
= [df0(years[i]) + a * f0(years[i]) + I[i] for i in 1:length(years)]
θt
return θt, f0, df0
end
let
# Market curve setup (synthetic)
= collect(0.0:0.1:10.0)
years = 0.02 .+ 0.002 .* years
yields = exp.(-yields .* years)
prices
# Parameters
= 0.1 # mean reversion speed
a = 0.008 .+ 0.004 .* sin.(0.5 .* years) # sinusoidal time-varying sigma
σt = hull_white_theta_timevar_sigma(years, prices, σt; a=a)
θt, f0, df0 = 0.03 # initial short rate
r0 = 1000 # number of simulated paths
nsim
# Preallocate
= zeros(length(years), nsim)
rpaths
# Seed for reproducibility
Random.seed!(1234)
# Euler–Maruyama simulation
for path in 1:nsim
= r0
r 1, path] = r
rpaths[for i in 2:length(years)
= years[i] - years[i-1]
dt = sqrt(dt) * randn()
dW = (θt[i-1] - a * r) * dt
drift = σt[i-1] * dW
diffusion += drift + diffusion
r = r
rpaths[i, path] end
end
# Plot
= Figure(size=(800, 500))
fig = Axis(fig[1, 1], xlabel="Time (years)", ylabel="Short rate", title="Hull–White Simulation with time-dependent θ(t) and σ(t)")
ax for path in 1:nsim
lines!(ax, years, rpaths[:, path])
end
figend
25.3.2 Equity Models
25.3.2.1 Geometric Brownian Motion (GBM)
GBM is a stochastic process commonly used to model the price movement of financial instruments, including stocks. It assumes constant volatility and is characterized by a log-normal distribution. It is defined as
\[ dS(t) = \mu S(t) \, dt + \sigma S(t) \, dW(t) \]
where
- \(S(t)\) is the stock price at time \(t\).
- \(μ\) is the drift coefficient (expected return).
- \(σ\) is the volatility coefficient.
- \(dW(t)\) is a Wiener process or Brownian motion, representing a random shock.
let
# Set seed for reproducibility
Random.seed!(1234)
# GBM parameters
= 0.05 # Drift (expected return)
μ = 0.2 # Volatility
σ
# Initial stock price
= 100.0
S₀
# Number of time steps and simulations
= 252
num_steps = 1_000
num_simulations
# Time increment
= 1 / 252
Δt
# Function to simulate GBM
function gbm_simulation(μ, σ, S₀, Δt, num_steps, num_simulations)
= zeros(num_steps, num_simulations)
stock_price_paths = (μ - 0.5 * σ * σ) * Δt
drift = σ * sqrt(Δt)
vol for j in 1:num_simulations
1, j] = S₀
stock_price_paths[for i in 2:num_steps
= stock_price_paths[i-1, j] * exp(drift + vol * randn())
stock_price_paths[i, j] end
end
return stock_price_paths
end
# Run GBM simulation
= gbm_simulation(μ, σ, S₀, Δt, num_steps, num_simulations)
gbm_paths
# Plot the simulated stock price paths
= Figure()
f Axis(f[1, 1])
for i in 1:num_simulations
lines!(1:num_steps, gbm_paths[:, i])
end
fend
25.3.2.2 Generalized Autoregressive Conditional Heteroskedasticity (GARCH)
GARCH models capture time-varying volatility. They are often used in conjunction with other models to forecast volatility. It is defined as
\[ \sigma^2_t = \omega + \alpha_1 r^2_{t-1} + \beta_1 \sigma^2_{t-1} \]
\[ r_t = \varepsilon_t \sqrt{\sigma^2_t} \]
- \(σ^2_t\) is the conditional variance at time \(t\)
- \(r_t\) is the return at time \(t\)
- \(\varepsilon_t\) is a white noise or innovation process
- \(\omega\), \(\alpha_1\), \(\beta_1\) are model parameters
let
# Set seed for reproducibility
Random.seed!(1234)
# GARCH(1,1) parameters
= 0.01 # Constant term
α₀ = 0.1 # Coefficient for lagged squared returns
α₁ = 0.8 # Coefficient for lagged conditional volatility
β₁ @assert α₁ + β₁ < 1 "Stationarity requires α₁ + β₁ < 1"
# Number of time steps and simulations
= 252
num_steps = 1_000
num_simulations
# Time increment
= 1 / 252
Δt
# Function to simulate GARCH(1,1) volatility
function garch_simulation(α₀, α₁, β₁, num_steps, num_simulations)
= zeros(num_steps, num_simulations)
volatility_paths for j in 1:num_simulations
= randn(num_steps)
ε = zeros(num_steps)
squared_returns for i in 2:num_steps
= α₀ + α₁ * ε[i-1] * ε[i-1] + β₁ * squared_returns[i-1]
squared_returns[i] = sqrt(squared_returns[i])
volatility_paths[i, j] end
end
return volatility_paths
end
# Run GARCH simulation
= garch_simulation(α₀, α₁, β₁, num_steps, num_simulations)
garch_paths
# Plot the simulated volatility paths
= Figure()
f Axis(f[1, 1])
for i in 1:num_simulations
lines!(1:num_steps, garch_paths[:, i])
end
fend
25.3.3 Copulas
Copulas give us a flexible way to model dependence structures between random variables, independently from their marginal distributions. With copulas, we can model each variable’s distribution (marginal) separately, and then “glue” them together with a copula to describe the dependence structure. This is useful when variables have very different distributions (e.g. stock returns vs. interest rates). Besides, copulas can capture nonlinear dependence and tail dependence (how extreme events co-occur), v.s. traditional correlation (e.g. Pearson) only captures linear dependence.
Simulating data using copulas involves generating multivariate samples with specified marginal distributions and a copula structure.
using Copulas
let
Random.seed!(1234)
# 2D Gaussian copula with correlation ρ = 0.8
= 0.8
ρ = [1.0 ρ;
Σ 1.0]
ρ = GaussianCopula(Σ)
Cg = rand(Cg, 10_000) # n × d matrix with U(0,1) margins
U = Figure()
f = Axis(f[1, 1], xlabel="U₁", ylabel="U₂")
ax scatter!(ax, U[1, :], U[2, :], markersize=2, color=:steelblue, alpha=0.4)
fend
Copulas can also be used to infer combined distributions from data samples.
let
= Gamma(2.0, 3.0)
X₁ = Pareto(3.0, 1.0)
X₂ = LogNormal(0.0, 1.0)
X₃ = ClaytonCopula(3, 0.7) # A 3-variate Clayton Copula with θ = 0.7
C = SklarDist(C, (X₁, X₂, X₃)) # The final distribution
D
# Generate a dataset
= rand(D, 1000)
simu # Estimate marginals by MLE
1 = fit(Gamma, simu[:, 1])
d̂2 = fit(Pareto, simu[:, 2])
d̂3 = fit(LogNormal, simu[:, 3])
d̂# Probability integral transform to uniforms
= hcat(cdf.(d̂1, simu[:, 1]), cdf.(d̂2, simu[:, 2]), cdf.(d̂3, simu[:, 3]))
Û # Fit copula on uniforms
= fit(ClaytonCopula, Û)
Ĉ # Compose the estimated Sklar distribution
= SklarDist(Ĉ, (d̂1, d̂2, d̂3))
D̂ end
Copulas.SklarDist{Copulas.ClaytonCopula{3, Float64}, Tuple{Distributions.Gamma{Float64}, Distributions.Pareto{Float64}, Distributions.LogNormal{Float64}}}(
C: Copulas.ClaytonCopula{3, Float64}(
G: Copulas.ClaytonGenerator{Float64}(-0.5)
)
m: (Distributions.Gamma{Float64}(α=1.2297936405142902, θ=0.9789028809154936), Distributions.Pareto{Float64}(α=0.9847322709380394, θ=1.502490671065896), Distributions.LogNormal{Float64}(μ=-0.2623276117827195, σ=1.1226430321293628))
)