Why the typical price path loses
Geometric Brownian motion, the model behind the Monte Carlo scene on the home page. A price can grow on average and still end below its start more often than not.
The Monte Carlo panel on the home page draws 64 price paths a year long and stacks where they end into a histogram. Watch it for a few runs and something looks off: the model is set to grow 6% a year, yet about half of the paths end below where they started. Nothing is broken. That is what compounding does to a noisy price.
The model
Geometric Brownian motion says the relative change of a price over a short time is a constant drift plus noise:
Here is the drift, the volatility and the increment of a Brownian motion, a normal draw with variance . Applying Itô’s lemma to gives a closed-form solution,
so is normal and is lognormal. The extra is the whole story.
Mean and median part ways
Take the expectation and the noise term contributes , which exactly cancels the correction:
The mean grows at ; the typical path grows at . The gap, per year, is often called volatility drag. A few lucky paths that end far above the rest pull the mean up, while most paths sit below it. The chance of ending below the starting price is
with the standard normal distribution function.
The figure starts with the home page’s settings, and . The drag is , larger than the drift, so the expected price after a year is about while the median is about , and roughly of paths end below . Push to with and the median falls to about while the mean rises to ; about of paths now lose.
Simulating it
Because the solution is exact, there is no need for a small-step integrator: draw normal increments, add the drift in log space and exponentiate.
import numpy as np
def gbm_paths(s0, mu, sigma, years=1.0, steps=252, n=400, seed=7): rng = np.random.default_rng(seed) dt = years / steps shocks = rng.standard_normal((n, steps)) log_steps = (mu - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * shocks log_paths = np.cumsum(log_steps, axis=1) return s0 * np.exp(np.hstack([np.zeros((n, 1)), log_paths]))
paths = gbm_paths(100, mu=0.06, sigma=0.38, n=100_000)final = paths[:, -1]print(f"mean {final.mean():.1f} median {np.median(final):.1f} " f"below start {(final < 100).mean():.1%}")With a large n the printout lands on the theory: mean near , median near , a little over half the
paths below the start.
Why it matters
Volatility drag is why a portfolio’s average return and its compounded return differ, why leveraged products decay in sideways markets, and why “expected value” can describe almost nobody’s actual outcome. The model is deliberately simple; real prices have fat tails and changing volatility. The gap between mean and median survives those refinements, and it only gets wider as volatility rises.