The random walk with the steps shrunk
Imagine a price that moves up or down each second by a random amount. Make the interval shorter and shorter, and shrink each step so the total spread stays reasonable: the limit is Brownian motion W(t), introduced to describe pollen grains in water and adopted by Bachelier in 1900 to model prices. It is defined by three properties: W(0) = 0; increments over non-overlapping intervals are independent; and W(t) - W(s) is normal with mean 0 and variance t - s.
import numpy as np
rng = np.random.default_rng(1)
dt = 0.01
z = rng.standard_normal(5) # standard normal draws
increments = z * np.sqrt(dt) # each step over dt has standard deviation sqrt(dt)
print(np.round(increments, 4))A path is a running sum
To simulate a path on a grid of times 0, dt, 2 dt, ..., draw one standard normal z per step, scale it by sqrt(dt), and add up: W(k * dt) is the sum of the first k increments, np.cumsum. Put a 0 in front because the path starts at 0. The variance of W(t) is t, so the spread of the position grows like the square root of time, the same square-root law as in volatility scaling and VaR.
import numpy as np
def brownian_path(z, dt):
return np.concatenate([[0.0], np.cumsum(z * np.sqrt(dt))])
rng = np.random.default_rng(1)
n, dt = 252, 1 / 252
ends = np.array([brownian_path(rng.standard_normal(n), dt)[-1] for _ in range(4000)])
print(round(float(ends.mean()), 3), round(float(ends.var()), 3)) # about 0 and 1: W(1) is a standard normalA path that is infinitely rough
A Brownian path is continuous but nowhere smooth, and its length is infinite. Its roughness has a precise measure, the quadratic variation, the sum of squared steps. For a smooth curve this sum shrinks to 0 as the grid gets finer; for Brownian motion it stays at the elapsed time t, whatever the grid, because each squared step is about dt and there are t / dt of them. This fact, (dW)^2 = dt, is what makes stochastic calculus different from ordinary calculus, and is the key to the next topics.
import numpy as np
def quadratic_variation(path):
return float(np.sum(np.diff(path) ** 2))
rng = np.random.default_rng(2)
t = 2.0
for steps in (10, 100, 10000):
dt = t / steps
path = np.concatenate([[0.0], np.cumsum(rng.standard_normal(steps) * np.sqrt(dt))])
print(steps, round(quadratic_variation(path), 3)) # settles near 2.0 as the grid gets finer
smooth = np.linspace(0, 1, 10001) * 2
print(round(quadratic_variation(smooth), 5)) # a straight line: close to 0Drift and volatility
A price has a trend and a scale. Arithmetic Brownian motion with drift is X(t) = mu * t + sigma * W(t): mu is the drift (the trend per unit of time), sigma the volatility (the scale of the random part). It is the simplest model of a price change, and it can go negative, which is why prices themselves are modelled as geometric Brownian motion in the next topic. It is still the building block for rates and spreads.
import numpy as np
def drifted_path(z, dt, mu, sigma):
t = np.arange(len(z) + 1) * dt
w = np.concatenate([[0.0], np.cumsum(z * np.sqrt(dt))])
return mu * t + sigma * w
rng = np.random.default_rng(3)
n, dt = 252, 1 / 252
path = drifted_path(rng.standard_normal(n), dt, 0.08, 0.2) # 8% a year drift, 20% volatility
print(round(float(path[-1]), 4), "after one year; the drift alone would give 0.08")
print(np.round(drifted_path(np.zeros(4), 0.25, 0.08, 0.2), 4)) # no noise: just the trend linenp.random.default_rng(seed)), which makes a run reproducible. A single path proves nothing: statistics such as the mean or the variance of W(1) only show up over thousands of paths, and the error of a Monte Carlo estimate shrinks like 1 / sqrt(paths).- Brownian motion starts at 0 and has independent normal increments of variance
dt: simulate withz * sqrt(dt)and a cumulative sum. - The variance of
W(t)ist, and the quadratic variation of a path stays at the elapsed time:(dW)^2 = dt. - Add trend and scale with
X(t) = mu * t + sigma * W(t); one path proves nothing, statistics need many paths.
Next up: geometric Brownian motion, the model in which prices cannot go negative.
Lab · what's graded
- Scale the shocks with
brownian_increments25 pts - Build the path with
brownian_path25 pts - Measure the roughness with
quadratic_variation25 pts - Add a trend with
drifted_path25 pts