Pricing European options with a binomial tree¶
Problem¶
Price a one-year European call and put, at the money (\(S_0 = K = 100\)), with a 5% annual rate and 20% volatility, on a recombining binomial tree with 50 steps. Check the result with put-call parity.
Method¶
The Cox–Ross–Rubinstein tree moves the price up by \(u\) or down by \(d\) at each step of length \(\Delta t = T/N\):
\[
u = e^{\sigma\sqrt{\Delta t}}, \qquad d = \frac{1}{u}, \qquad
q = \frac{e^{r\Delta t} - d}{u - d}, \qquad r = \ln(1 + i)
\]
where \(q\) is the risk-neutral probability of an up move. At maturity the option is worth its payoff at each of the \(N + 1\) nodes; then the value is rolled back one step at a time as the discounted risk-neutral expectation:
\[
V_{j,k} = e^{-r\Delta t}\,\big(q\,V_{j+1,k+1} + (1 - q)\,V_{j+1,k}\big)
\]
The value at the root is the price. Put-call parity, \(P = C - S_0 + K e^{-rT}\), gives an independent check of the put.
Code¶
clear all, clc
%% 1
% Inputs of the problem
T = 1; % time to maturity
K = 100; % strike price
S_0 = 100; % current price
i = 0.05; % risk free rate
sigma = 0.20; % volatility of the underlying
N = 50; % number of time steps
[C,P,pcp] = F_es165(T,K,S_0,i,sigma,N)
function [Call_price,Put_price,PutcallParity]=F_es165(T,K,S_0,i,sigma,N)
% EXERCISE PRICE OF A EUROPEAN OPTION
% INPUTS: T := maturity;
% K := strike price;
% S_0 := current price;
% i := risk free rate;
% sigma := volatility of the underlying;
% N := steps;
% OUTPUTS: Call_price := price of the European Call
% Put_price := price of the European Put
% PutcallParity := check with the Put-Call Parity formula
% CRR parametrization
delta_t = T/N; % length of a single step
r=log(1+i); % continuously compounded interest rate
u = exp(sigma*sqrt(delta_t)); % upstate coefficient
d = 1/u; % downstate coefficient
q = (exp(r*delta_t)-d)/(u-d); % risk neutral probability related to the upstate
NodesCall = NaN(N+1,N+1); % initialize the matrix,
% containing the nodes of the recombining binomial tree
NodesPut = NaN(N+1,N+1);
% payoff at maturity T (i.e., j=N)
for k = 0:N
NodesCall(k+1,N+1) = max(0,S_0*(u^k)*(d^(N-k))-K);
NodesPut(k+1,N+1) = max(0,K-S_0*(u^k)*(d^(N-k)));
end
% Apply a backward procedure to find the value of the option
for j = N-1:-1:0
for k = 0:j
NodesCall(k+1,j+1) = exp(-r*delta_t)*(q*NodesCall(k+2,j+2) + (1-q) *NodesCall(k+1,j+2));
NodesPut(k+1,j+1) = exp(-r*delta_t)*(q*NodesPut(k+2,j+2) + (1-q) *NodesPut(k+1,j+2));
end
end
Call_price = NodesCall(1,1); % the price of a call in t=0
Put_price = NodesPut(1,1); % the price of a put in t=0
% check the price of the put using the put-call-parity relationship
PutcallParity = Call_price-S_0+K*exp(-r*T);
end
import numpy as np
# %% function F_es165
def F_es165(T, K, S_0, i, sigma, N):
# PRICE OF A EUROPEAN OPTION ON A BINOMIAL TREE
# INPUTS: T := maturity; K := strike price; S_0 := current price;
# i := risk free rate; sigma := volatility; N := steps
# OUTPUTS: Call_price, Put_price, PutcallParity (check with put-call parity)
# CRR parametrization
delta_t = T / N # length of a single step
r = np.log(1 + i) # continuously compounded interest rate
u = np.exp(sigma * np.sqrt(delta_t)) # upstate coefficient
d = 1 / u # downstate coefficient
q = (np.exp(r * delta_t) - d) / (u - d) # risk neutral probability of the upstate
NodesCall = np.full((N + 1, N + 1), np.nan) # MATLAB: NaN(N+1, N+1)
NodesPut = np.full((N + 1, N + 1), np.nan)
# payoff at maturity T (column j = N)
# in MATLAB node (k, j) is stored in (k+1, j+1); in Python, counting from 0, it is just [k, j]
for k in range(N + 1): # MATLAB: for k = 0:N
NodesCall[k, N] = max(0, S_0 * u**k * d**(N - k) - K)
NodesPut[k, N] = max(0, K - S_0 * u**k * d**(N - k))
# backward procedure to find the value of the option
for j in range(N - 1, -1, -1): # MATLAB: for j = N-1:-1:0
for k in range(j + 1): # MATLAB: for k = 0:j
NodesCall[k, j] = np.exp(-r * delta_t) * (q * NodesCall[k+1, j+1] + (1 - q) * NodesCall[k, j+1])
NodesPut[k, j] = np.exp(-r * delta_t) * (q * NodesPut[k+1, j+1] + (1 - q) * NodesPut[k, j+1])
Call_price = NodesCall[0, 0] # the price of a call in t=0 (MATLAB: NodesCall(1,1))
Put_price = NodesPut[0, 0]
# check the price of the put using the put-call parity relationship
PutcallParity = Call_price - S_0 + K * np.exp(-r * T)
return Call_price, Put_price, PutcallParity
# %% 1
# Inputs of the problem
T = 1 # time to maturity
K = 100 # strike price
S_0 = 100 # current price
i = 0.05 # risk free rate
sigma = 0.20 # volatility of the underlying
N = 50 # number of time steps
C, P, pcp = F_es165(T, K, S_0, i, sigma, N)
print("C =", C)
print("P =", P)
print("pcp =", pcp)
Results¶
| Binomial tree, 50 steps | Black–Scholes | Put-call parity | |
|---|---|---|---|
| Call | 10.3464 | 10.3863 | |
| Put | 5.5845 | 5.6244 | 5.5845 |
Black–Scholes is computed with the same continuously compounded rate, \(r = \ln 1.05\).
Takeaways¶
- The tree is a discrete approximation of Black–Scholes: with 50 steps it is 4 cents off, and the error shrinks roughly as \(1/N\).
- Put-call parity holds exactly on the tree, whatever the number of steps, because both prices come from the same risk-neutral probabilities.
- The rate convention matters: the annual 5% becomes a continuous \(\ln 1.05 = 4.88\%\) before it enters the tree.
- The backward procedure is what makes trees useful beyond European options: an American option only needs a comparison with the exercise value at each node.