Build a recombining Cox–Ross–Rubinstein tree for the stock, evaluate the European payoff at every terminal node, and roll those payoffs back using discounted risk-neutral expectations. Compare the resulting price at several step counts with Black–Scholes using the same spot, strike, maturity, rate and volatility.
Roll the payoff backwards through a recombining grid
For step length , set , , , and . The identity makes the discounted stock a martingale under these probabilities. With up moves after steps, the stock is . A European call has . Its backward recursion is
A single vector is enough for the option values because each backward step only needs the next vector. The straightforward method costs time and memory. If is outside , inspect the input assumptions and step size before rolling back.
At an interior node you already know both successor option values because you work from maturity backwards. Multiply each by its risk-neutral weight, add them and discount that single-period expectation. The index identifies the number of up moves so the down successor uses the same and the up successor uses . The node arrangement is why one rolling array is enough. If you update that array in place, go from low to high so both entries on the right-hand side still belong to the next time level.
Why the first refinements can cross the benchmark
Take , , , and . With one step, and . The call pays about in the up state and zero in the down state, giving a price near . With two steps the price is about , on the other side of the Black–Scholes value of . At 100 steps the price is about . Watching just the first two rows might suggest deterioration, but the run at many step counts tells a different story.
A pricer and its convergence check
import numpy as np
from scipy.stats import norm
def crr_call(spot, strike, maturity, rate, vol, steps):
h = maturity / steps
u = np.exp(vol * np.sqrt(h))
d = 1 / u
gross = np.exp(rate * h)
p = (gross - d) / (u - d)
if not 0 < p < 1:
raise ValueError("Tree needs down < gross return < up.")
ups = np.arange(steps + 1)
values = np.maximum(spot * u**ups * d**(steps - ups) - strike, 0)
for _ in range(steps):
values = ((1 - p) * values[:-1] + p * values[1:]) / gross
return values[0]
benchmark = 100 * (norm.cdf(0.35) - np.exp(-0.05) * norm.cdf(0.15))
for steps in (1, 2, 10, 25, 100, 400):
price = crr_call(100, 100, 1, 0.05, 0.20, steps)
print(steps, round(price, 5), round(price - benchmark, 5))Compare the continuous-model reference in the options pricer before diagnosing a discrepancy in the tree.