Brownian Motion

Brownian Motion

Recipes for simulating the classical Wiener process in both continuous and discrete time.

Simulating Brownian Motion

Continuous-time Recipe for Brownian Motion

Intuition

The goal is to construct a process \( X_t \) such that $$ \mathbb{E}[X_t] = 0, \quad \text{Cov}(X_s, X_t) = K(s, t) $$ For Brownian motion, the covariance kernel is $$ K(s, t) = \min(s, t) $$ The Karhunen–Loève expansion gives $$ W_t = \sqrt{2} \sum_{k=1}^\infty Z_k \frac{\sin\left((k-\frac{1}{2})\pi t\right)}{(k-\frac{1}{2})\pi} $$ where \( Z_k \sim \mathcal{N}(0, 1) \) are independent.

General Recipe

  1. Define the process and covariance kernel \( K(s, t) = \min(s, t) \).
  2. Set up and solve the integral eigenvalue problem: $$ \int_0^1 K(s, t) e_k(s) ds = \lambda_k e_k(t) $$
  3. Obtain eigenfunctions and eigenvalues: $$ e_k(t) = \sqrt{2} \sin\left((k-\frac{1}{2})\pi t\right), \quad \lambda_k = \frac{1}{((k-\frac{1}{2})\pi)^2} $$
  4. Construct the Karhunen–Loève expansion for simulation.
  5. Discretize the expansion for practical computation.

Python Implementation

import numpy as np

def brownian_motion_ctr(steps=100, n_terms=500, paths=1):
    t = np.linspace(0, 1, steps)
    Bt = np.zeros((paths, steps))
    for p in range(paths):
        Z = np.random.normal(0, 1, n_terms)
        k = np.arange(1, n_terms + 1)
        for i, ti in enumerate(t):
            sin_terms = np.sin((k - 0.5) * np.pi * ti) / ((k - 0.5) * np.pi)
            Bt[p, i] = np.sqrt(2) * np.dot(Z, sin_terms)
    return Bt

Summary: This approach yields sample paths of Brownian motion with the correct covariance structure, as guaranteed by the Karhunen–Loève theorem.

Discrete-time Recipe for Brownian Motion

Intuition

In discrete time, we want to construct a vector \( X \) such that $$ \mathbb{E}[X] = 0, \quad \text{Cov}(X) = \Gamma $$ where \( \Gamma_{ij} = \min(t_i, t_j) \) is the covariance matrix for Brownian motion.

General Recipe

  1. Generate the desired covariance matrix \( \Gamma \).
  2. Decompose \( \Gamma = Q \Lambda Q^T \) (eigendecomposition).
  3. Generate a Gaussian vector \( Z \sim N(0, I_n) \).
  4. Set \( X = Q \Lambda^{1/2} Z \).

Python Implementation

import numpy as np

def brownian_motion_dtr(steps=100, paths=1, T=1):
    t = np.linspace(0, T, steps)
    Gamma = np.zeros((steps, steps))
    for i in range(steps):
        for j in range(steps):
            Gamma[i, j] = min(t[i], t[j])
    eigenvals, eigenvecs = np.linalg.eigh(Gamma)
    Z = np.random.normal(0, 1, (steps, paths))
    Wt = eigenvecs @ np.diag(np.sqrt(eigenvals)) @ Z
    return Wt.T

Summary: This approach produces Brownian motion sample paths with the correct covariance structure by diagonalizing the covariance matrix and scaling standard normal draws.

Simulation

Sample Paths

Covariance

Covariance Error

Theoretical Covariance

Discretized Equation

$$ W_t = \\sqrt{2} \\sum_{k=1}^N Z_k \\frac{\\sin\\left((k-\\frac{1}{2})\\pi t\\right)}{(k-\\frac{1}{2})\\pi} $$

Relevant Articles

Recipes for simulating stochastic processes

This note discusses the necessary steps to simulate a stochastic process with a desired covariance structure. In this context, I outline the Karhunen–Loéve theorem and provide intuition and general recipes to decompose a stochastic process by establishing the integral eigenvalue problem (continuous-time) or, equivalently, the covariance matrix diagonalization problem (discrete-time). I then apply these general recipes to a Brownian motion and Brownian bridge to numerically simulate paths.

View on SSRN
Suggested Citation:
Paolucci, Roman, Recipes for simulating stochastic processes (June 30, 2025). Available at SSRN: https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5332011