Multi-Step Binomial Trees

The one-step model priced an option over a single jump — realistic for nothing, but the whole point was that it's a Lego brick. Chain many of these one-step bricks together, back to back, and the crude "up or down" market becomes a rich lattice of possible price paths that can approximate any continuous-time model as closely as you like. This is how a desk actually implements pricing for instruments that Black–Scholes can't touch — and, reassuringly, if you chain enough bricks together for a plain European option, you get the Black–Scholes price back out, to as many decimal places as you like.

Chaining one-step nodes into a tree

Split the option's life T into N equal steps of length \Delta t = T/N. At every node of the resulting lattice, the stock still only moves up by a factor u or down by a factor d — the one-step model, over and over. Because u and d are the same at every node, an up-move followed by a down-move lands on exactly the same price as a down-move followed by an up-move (S_0 u d = S_0 d u). The tree recombines: after N steps there are only N+1 distinct terminal prices, not 2^N — the difference between a lattice you can compute on a laptop and one you can't.

Pricing is backward induction, exactly the one-step formula applied again and again. First fill in the terminal payoffs at every leaf — that needs no probability at all, just the payoff function. Then walk backward one layer at a time: at every node, the value is the one-step discounted q-expectation of the two nodes directly ahead of it,

V_{i,j} = e^{-r\Delta t}\Bigl(q\,V_{i+1,j+1} + (1-q)\,V_{i+1,j}\Bigr),

where i indexes the time step and j the number of up-moves so far. Repeat until you reach the root, and the number sitting there is V_0. Every node uses the same q, because u, d and the per-step rate never change across the tree — the one-step recipe just repeats itself N times.

Choosing u and d: the Cox–Ross–Rubinstein tree

The one-step model left u and d as free inputs. To make an N-step tree approximate a particular continuous-time stock — one with volatility \sigmaCox, Ross and Rubinstein (1979) proposed matching the tree's variance per step to the stock's:

Notice the shape of the trick: u and d no longer come from a market view — they're engineered from \sigma alone, purely so the discrete tree mimics the continuous model as N grows.

Watching it converge

Price a European call with S_0 = K = 100, r = 5\%, \sigma = 20\%, T = 1 year, on CRR trees of growing N, and compare against the closed-form Black–Scholes price (10.4506):

// Cox-Ross-Rubinstein binomial price vs. the closed-form Black-Scholes price, // for a European call, as the number of tree steps N grows. function erf(x: number): number { const sign = x < 0 ? -1 : 1; x = Math.abs(x); const a1 = 0.254829592, a2 = -0.284496736, a3 = 1.421413741; const a4 = -1.453152027, a5 = 1.061405429, p = 0.3275911; const t = 1 / (1 + p * x); const y = 1 - (((((a5 * t + a4) * t) + a3) * t + a2) * t + a1) * t * Math.exp(-x * x); return sign * y; } function normCdf(x: number): number { return 0.5 * (1 + erf(x / Math.sqrt(2))); } function bsCall(S0: number, K: number, r: number, sig: number, T: number): number { const d1 = (Math.log(S0 / K) + (r + 0.5 * sig * sig) * T) / (sig * Math.sqrt(T)); const d2 = d1 - sig * Math.sqrt(T); return S0 * normCdf(d1) - K * Math.exp(-r * T) * normCdf(d2); } function crrCall(S0: number, K: number, r: number, sig: number, T: number, N: number): number { const dt = T / N; const u = Math.exp(sig * Math.sqrt(dt)); const d = 1 / u; const disc = Math.exp(-r * dt); const q = (Math.exp(r * dt) - d) / (u - d); const payoffs: number[] = []; for (let i = 0; i <= N; i++) { const ST = S0 * Math.pow(u, N - i) * Math.pow(d, i); payoffs.push(Math.max(ST - K, 0)); } for (let step = N; step > 0; step--) { for (let i = 0; i < step; i++) { payoffs[i] = disc * (q * payoffs[i] + (1 - q) * payoffs[i + 1]); } } return payoffs[0]; } const S0 = 100, K = 100, r = 0.05, sig = 0.2, T = 1; const bs = bsCall(S0, K, r, sig, T); console.log(`Black-Scholes price = ${bs.toFixed(4)}`); for (const N of [1, 2, 5, 10, 20, 50, 100, 200]) { const price = crrCall(S0, K, r, sig, T, N); console.log(`N=${N.toString().padStart(3)} CRR price=${price.toFixed(4)} gap=${(price - bs).toFixed(4)}`); }

The gap column shrinks towards zero, but not smoothly — it flips sign and shrinks in a zig-zag. That wobble, and why it happens, is worth a closer look below. The chart shows the same convergence over a wider range of N, against the Black–Scholes price as a flat reference line.

The wobble is a discretization artifact, not noise — CRR trees are entirely deterministic, so the same N always gives the same price. The culprit is where the strike K lands relative to the tree's terminal nodes. For some N a terminal node sits almost exactly at K, which the discrete tree handles awkwardly (that node's payoff is either counted fully as in-the-money or fully as out-of-the-money, with nothing in between); for other N the nodes straddle K more evenly. As N increases by one, which case you're in flips back and forth, so the error oscillates in sign even as its size shrinks roughly like 1/N. Practitioners route around it in two ways: average the price at N and N+1 steps (the oscillations largely cancel), or use a variant like the Trigeorgis or Jarrow–Rudd tree that centres the nodes around K deliberately.