Skip to content

Milstein Convergence Geometric Brownian Motion

Background

Milstein discretization convergence for Geometric Brownian Motion.

Demonstrates weak and strong convergence properties of the Milstein scheme for GBM approximation.

Reference: Oosterlee & Grzelak (2019). Mathematical Modeling and Computation in Finance. World Scientific.


Code

```python

-- coding: utf-8 --

""" Milstein discretization convergence for Geometric Brownian Motion.

Demonstrates weak and strong convergence properties of the Milstein scheme for GBM approximation.

Reference: Oosterlee & Grzelak (2019). Mathematical Modeling and Computation in Finance. World Scientific. """

import numpy as np import matplotlib.pyplot as plt

=============================================================================

1. Path Generation

=============================================================================

def generate_paths_gbm_milstein(num_paths, num_steps, maturity, r, sigma, s0): """ Generate GBM paths using Milstein discretization.

Parameters
----------
num_paths : int
    Number of sample paths.
num_steps : int
    Number of time steps.
maturity : float
    Time to maturity (T).
r : float
    Risk-free rate.
sigma : float
    Volatility.
s0 : float
    Initial stock price.

Returns
-------
paths : dict
    Dictionary with keys 'time', 'S1' (Milstein approx), 'S2' (exact).
"""
z = np.random.normal(0.0, 1.0, (num_paths, num_steps))
w = np.zeros((num_paths, num_steps + 1))

# Approximation (Milstein scheme)
s1 = np.zeros((num_paths, num_steps + 1))
s1[:, 0] = s0

# Exact solution
s2 = np.zeros((num_paths, num_steps + 1))
s2[:, 0] = s0

time = np.zeros(num_steps + 1)

dt = maturity / float(num_steps)
for i in range(num_steps):
    # Ensure samples from normal have mean 0 and variance 1
    if num_paths > 1:
        z[:, i] = (z[:, i] - np.mean(z[:, i])) / np.std(z[:, i])
    w[:, i + 1] = w[:, i] + np.sqrt(dt) * z[:, i]

    # Milstein scheme
    s1[:, i + 1] = (s1[:, i] + r * s1[:, i] * dt + sigma * s1[:, i] * (w[:, i + 1] - w[:, i])
                    + 0.5 * sigma ** 2 * s1[:, i] * ((w[:, i + 1] - w[:, i]) ** 2 - dt))

    # Exact solution
    s2[:, i + 1] = s2[:, i] * np.exp((r - 0.5 * sigma * sigma) * dt + sigma * (w[:, i + 1] - w[:, i]))
    time[i + 1] = time[i] + dt

paths = {"time": time, "S1": s1, "S2": s2}
return paths

=============================================================================

2. Visualization

=============================================================================

def plot_sample_paths(time, s1, s2): """ Plot sample paths from Milstein and exact schemes.

Parameters
----------
time : ndarray
    Time grid.
s1 : ndarray
    Milstein scheme paths.
s2 : ndarray
    Exact scheme paths.
"""
plt.figure(1, figsize=(10, 6))
plt.plot(time, np.transpose(s1), 'k')
plt.plot(time, np.transpose(s2), '--r')
plt.grid()
plt.xlabel("time")
plt.ylabel("S(t)")
plt.tight_layout()

def plot_convergence(dt_vec, error_weak, error_strong): """ Plot weak and strong convergence errors.

Parameters
----------
dt_vec : ndarray
    Vector of time steps.
error_weak : ndarray
    Weak convergence errors.
error_strong : ndarray
    Strong convergence errors.
"""
plt.figure(2, figsize=(10, 6))
plt.plot(dt_vec, error_weak)
plt.plot(dt_vec, error_strong, '--r')
plt.grid()
plt.legend(['weak convergence', 'strong convergence'])
plt.xlabel("dt")
plt.ylabel("error")
plt.tight_layout()

=============================================================================

3. Main

=============================================================================

def main(): """Run convergence analysis for Milstein GBM scheme.""" # ===== Sample Path Visualization ===== num_paths = 25 num_steps = 25 maturity = 1.0 r = 0.06 # Risk-free rate sigma = 0.3 # Volatility s0 = 50.0 # Initial stock price

paths = generate_paths_gbm_milstein(num_paths, num_steps, maturity, r, sigma, s0)
time = paths["time"]
s1 = paths["S1"]
s2 = paths["S2"]

plot_sample_paths(time, s1, s2)

# ===== Convergence Analysis =====
num_steps_vec = range(1, 500, 1)
num_paths = 100
error_weak = np.zeros((len(num_steps_vec), 1))
error_strong = np.zeros((len(num_steps_vec), 1))
dt_vec = np.zeros((len(num_steps_vec), 1))

for idx, num_steps in enumerate(num_steps_vec):
    paths = generate_paths_gbm_milstein(num_paths, num_steps, maturity, r, sigma, s0)
    # Get paths at maturity
    s1_at_t = paths["S1"][:, -1]
    s2_at_t = paths["S2"][:, -1]

    error_weak[idx] = np.abs(np.mean(s1_at_t) - np.mean(s2_at_t))
    error_strong[idx] = np.mean(np.abs(s1_at_t - s2_at_t))
    dt_vec[idx] = maturity / num_steps

print(error_strong)
plot_convergence(dt_vec, error_weak, error_strong)
plt.show()

if name == "main": main() ```

Exercises

Exercise 1. Write the Milstein scheme for GBM and show that the correction term \(\frac{1}{2}\sigma^2 S[(\Delta W)^2 - \Delta t]\) recovers strong order 1.

Solution to Exercise 1

The Milstein scheme: \(S_{i+1} = S_i + rS_i\Delta t + \sigma S_i \Delta W_i + \frac{1}{2}\sigma^2 S_i[(\Delta W_i)^2 - \Delta t]\). This equals \(S_i(1 + r\Delta t + \sigma\Delta W + \frac{\sigma^2}{2}((\Delta W)^2 - \Delta t))\). Expanding the exact \(\exp((r - \sigma^2/2)\Delta t + \sigma\Delta W)\) to second order matches, confirming strong order 1.


Exercise 2. If Euler strong error at \(\Delta t = 0.002\) is \(0.1\) and Milstein is \(0.01\), estimate both at \(\Delta t = 0.0002\).

Solution to Exercise 2

Euler (\(O(\sqrt{\Delta t})\)): \(0.1/\sqrt{10} \approx 0.0316\). Milstein (\(O(\Delta t)\)): \(0.01/10 = 0.001\). At \(\Delta t = 0.0002\), Milstein is about 32 times more accurate.


Exercise 3. For what class of SDEs does the Milstein scheme reduce to the Euler scheme?

Solution to Exercise 3

When \(b^\prime(X) = 0\), i.e., when the diffusion coefficient is constant (additive noise). Examples: Ornstein-Uhlenbeck (\(dX = \kappa(\theta - X)dt + \sigma dW\)) and the Bachelier model. For these, both schemes have strong order 1.


Exercise 4. How does the number of Monte Carlo paths (e.g., 100) affect the reliability of the estimated convergence rate?

Solution to Exercise 4

The standard error of the strong error estimate is \(\text{SE} \approx \sigma_{\text{abs}}/\sqrt{N_{\text{paths}}}\). With 100 paths, \(\text{SE} \approx \hat{\epsilon}/10\), giving 10% relative uncertainty. The convergence slope on a log-log plot has uncertainty \(\pm 0.1\). Using 10,000 paths would reduce SE to 1%, giving a much cleaner convergence plot.