σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 8 · Stochastic models & machine learning›Topic 8.2·~55 minAdvanced

Geometric Brownian motion

Skill positionModule 8 of 11 · Topic 2 of 10
8.2 Geometric Brownian motion
—Lab score— / 100Total XP—Streak—

Prices change in percent

Arithmetic Brownian motion adds the same dollar noise whether a stock costs 5 or 500, and it can go below zero. A stock behaves differently: a 1% move is a 1% move at any price. Geometric Brownian motion puts the randomness in the percentage change: dS = mu * S * dt + sigma * S * dW. It is the model of Black–Scholes. mu is the expected return per year and sigma the volatility, the quantity quoted in the market.

import math

def gbm_step(s, mu, sigma, dt, z):
    return s * math.exp((mu - 0.5 * sigma ** 2) * dt + sigma * math.sqrt(dt) * z)

print(round(gbm_step(100.0, 0.08, 0.2, 1 / 252, 0.0), 4))     # a calm day: only the (mu - sigma^2/2) drift acts
print(round(gbm_step(100.0, 0.08, 0.2, 1 / 252, 1.0), 4))     # a one-sigma up day
print(round(gbm_step(100.0, 0.08, 0.2, 1 / 252, -50.0), 4))   # an absurd fall: still positive

The exact solution, and the minus one half sigma squared

Solving the equation gives S(t) = S(0) * exp((mu - sigma**2 / 2) * t + sigma * W(t)). Why the strange - sigma**2 / 2? Because the exponential is convex: by Jensen's inequality, noise raises the average of exp(noise), and subtracting sigma**2 / 2 exactly cancels it, so the *expected* price grows at rate mu. Typical *paths* grow more slowly, at mu - sigma**2 / 2, which is why the median stock price is below the mean. The next topic, Itô's lemma, derives this term.

import numpy as np

def gbm_path(s0, mu, sigma, dt, z):
    log_moves = (mu - 0.5 * sigma ** 2) * dt + sigma * np.sqrt(dt) * z
    return s0 * np.exp(np.concatenate([[0.0], np.cumsum(log_moves)]))

rng = np.random.default_rng(1)
z = rng.standard_normal(252)
path = gbm_path(100.0, 0.08, 0.2, 1 / 252, z)
print(path[:4].round(3), round(float(path[-1]), 3))

Many paths: the mean and the median

Simulate thousands of one-year paths and look at where they end. The *average* final price is close to S0 * exp(mu * T), the formula for the expected price. The *median* is lower, S0 * exp((mu - sigma**2/2) * T), and with a high volatility the gap is large: most paths finish below the average, pulled up by a few spectacular ones. This right-skew, a lognormal distribution, is the shape of stock prices over long horizons.

import numpy as np

rng = np.random.default_rng(5)
s0, mu, sigma, T, steps, paths = 100.0, 0.08, 0.4, 1.0, 252, 20000
dt = T / steps
z = rng.standard_normal((paths, steps))
final = s0 * np.exp(np.sum((mu - 0.5 * sigma ** 2) * dt + sigma * np.sqrt(dt) * z, axis=1))

print(round(float(final.mean()), 1), round(s0 * np.exp(mu * T), 1))                      # close to each other: the expected price
print(round(float(np.median(final)), 1), round(s0 * np.exp((mu - 0.5 * sigma ** 2) * T), 1))   # the median is lower
print(round(float(np.mean(final < s0)), 3))                                              # the share of paths that lose money

Estimating mu and sigma

From prices sampled every dt years, the log returns r = diff(log(prices)) are independent normals with mean (mu - sigma**2/2) * dt and standard deviation sigma * sqrt(dt). So sigma = std(r) / sqrt(dt) and mu = mean(r) / dt + sigma**2 / 2. The volatility estimate is good with a few hundred points; the drift is notoriously noisy, because its standard error shrinks only with the total *time*, not with the number of observations.

import numpy as np

def estimate_gbm(prices, dt):
    r = np.diff(np.log(prices))
    sigma = r.std(ddof=1) / np.sqrt(dt)
    return r.mean() / dt + 0.5 * sigma ** 2, sigma

rng = np.random.default_rng(8)
dt = 1 / 252
for years in (1, 10, 100):
    n = int(years * 252)
    log_moves = (0.10 - 0.5 * 0.25 ** 2) * dt + 0.25 * np.sqrt(dt) * rng.standard_normal(n)
    prices = 100 * np.exp(np.concatenate([[0.0], np.cumsum(log_moves)]))
    mu, sigma = estimate_gbm(prices, dt)
    print(years, "years:", round(float(mu), 3), round(float(sigma), 3))           # true mu 0.10 and sigma 0.25
GBM is a good start, not the truthReal prices have fat tails, jumps and volatility that moves (the GARCH topic), none of which GBM contains. Its virtue is that everything is explicit: prices are lognormal, the expected price has a formula, and option prices follow. Models that fix its flaws (jumps, stochastic volatility) are built on top of it.
  • GBM: S(t) = S(0) * exp((mu - sigma**2 / 2) * t + sigma * W(t)); percentage changes are random and prices stay positive.
  • The - sigma**2 / 2 cancels the convexity of exp: the expected price is S0 * exp(mu * t), but the median path grows at mu - sigma**2 / 2.
  • Estimate from log returns: sigma = std(r) / sqrt(dt), mu = mean(r) / dt + sigma**2 / 2; volatility is estimated well, drift poorly.

Next up: Itô's lemma, the chain rule that explains where the - sigma**2 / 2 comes from.

Lab · what's graded

  • Take one exact step with gbm_step25 pts
  • Simulate a price path with gbm_path25 pts
  • Average over all futures with gbm_expected25 pts
  • Read the parameters back with estimate_gbm25 pts
Explorer · lab-103-1
▾ lab-103-1
…
bash · in-browser
learner@quantcraft:~/lab-103-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-103-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub