Skip to content

Pricing European options with a binomial tree

Course exercise 165 · Part III

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.