Binomial Tree Models
Numerical Methods in Quantitative Finance
Li Hao
[email protected]Numerical Methods in QF (QF5204) Binomial Tree Models 1 / 60
Pricing Financial Derivatives
Flow or Vanilla options that can be priced analytically
Swap / Forward / Futures
▶ Linear products, priced by ”Law of One Price”
European option - an option that may only be exercised on expiry
date, priced by Black-Scholes analytic formulas
Numerical Methods in QF (QF5204) Binomial Tree Models 2 / 60
Pricing Financial Derivatives
Exotic options require numerical pricer
American option - option that allows exercise any time prior to the
expiry date
Barrier option - if the spot price touches the pre-defined barrier in a
given time window, the option holder obtains (Knock-In Option) or
loses (Knock-Out Option) an underlying payoff
▶ the underlying payoff can be a European option,
▶ or just a constant rebate (touch option)
Asian option - an option where the payoff is determined by the
average underlying price over some pre-set period of time
▶ average of price has lower volatility, therefore a cheaper option for
hedging purpose,
▶ less sensitive to price manipulation on the expiry date
Target redemption forward, Bermudan callable structure, etc.
Numerical Methods in QF (QF5204) Binomial Tree Models 3 / 60
Bionmial tree option pricing model
A numerical method for the valuation of options.
The model uses a discrete-time model of varying price over time of
the underlying financial instrument.
First proposed by Cox, Ross and Rubinstein in 1979 − the CRR
binomial tree
Numerical Methods in QF (QF5204) Binomial Tree Models 4 / 60
One-Step Binomial Tree
p Su
S0
1−p Sd
t=0 t=T
A single time step from 0 to T
Two traded instruments in the market: stock S and a zero coupon
bond with continuous yield r (risk free interest rate)
Two possible states at time T: up and down
What’s the price of the call and put option at time 0?
Numerical Methods in QF (QF5204) Binomial Tree Models 5 / 60
Price by Replication
Consider an option with final payoff V (ST , T ), we want to solve for it’s
present value V (S0 , 0):
Construct a portfolio of δ shares of the stock and V (S0 , 0) − δS0
units of bond that grows at constant compounding rate r .
At time T, we would like the portfolio to have value V (ST , T ) no
matter whether S0 goes to the up state or the down state, so the
below equations should hold
(
δSu + e rT (V0 − δS0 ) = Vu
(1)
δSd + e rT (V0 − δS0 ) = Vd
Numerical Methods in QF (QF5204) Binomial Tree Models 6 / 60
Solution and Risk Neutral Probabilities
We know Su , Sd , Vu and Vd in the equations, so two unknowns δ, V0 and
two equations we can solve
Vu −Vd
δ = Su −Sd
rT S0 e rT − Sd
−rT Su − S0 e (2)
V 0 = e Vd + Vu
S −S S −S
| u {z d } | u {z d }
qd qu
We call qu and qd risk neutral probabilities, and the option’s price is
expectation of VT under the risk neutral probability measure (Q-measure):
V0 = e −rT EQ [VT ] (3)
Numerical Methods in QF (QF5204) Binomial Tree Models 7 / 60
Why Binomial Model?
Pros:
Overly simplified, but surprisingly general after extensions
More final states can be included with multiple steps
Handle many payoffs and option types – the only assumption of the
payoff V we make is that it depends on the terminal value of ST
Handle American options naturally
Easy to implement
Cons:
Difficult to handle path-dependent options
Numerical Methods in QF (QF5204) Binomial Tree Models 8 / 60
How Easy Is The Implementation
Inputs are:
Current value of the underlying stock S0
Risk free interest rate r
Up state Su and down state Sd
Option type: Call or Put for Now
Option strike K and time to maturity T
Output: option price
Numerical Methods in QF (QF5204) Binomial Tree Models 9 / 60
One Step Binomial Tree Implementation
1 from enum import Enum
2 import math
3 class PayoffType(str, Enum):
4 Call = ’Call’
5 Put = ’Put’
6
7 def oneStepBinomial(S:float, r:float, u:float, d:float, optType:PayoffType, K:
float, T:float) -> float:
8 p = (math.exp(r * T) - d) / (u-d)
9 if optType == PayoffType.Call:
10 return math.exp(-r*T) * (p*max(S*u-K, 0) + (1-p) * max(S*d-K, 0))
1 oneStepBinomial(S=100, r=0.01, u=1.2, d=0.8, optType=PayoffType.Call, K=105, T=1.0)
2 7.798504987524955
Numerical Methods in QF (QF5204) Binomial Tree Models 10 / 60
Let’s recap the input of our simple one step binomial model:
Option type, strike K and time to maturity T ,
Current value of the underlying stock S0 , risk free interest rate r
Up state Su and down state Sd
— HOW can we possibly know Su and Sd in reality?
But does it mean our binomial tree model is impractical? No, it is in fact
a discretized version of Black-Scholes model
Numerical Methods in QF (QF5204) Binomial Tree Models 11 / 60
Black-Scholes Model
The Black-Scholes model says, under the risk neutral measure, the
non-dividend paying stock price follows a log-normal process:
dSt
= rdt + σdWt (4)
St
where
σ is the volatility
Wt is a standard Brownian motion
r is the risk free interest rate
This is much closer to reality than the one step binomial model!
Numerical Methods in QF (QF5204) Binomial Tree Models 12 / 60
Black-Scholes Solution of the Stock Price
Applying Ito’s lemma we can get the diffusion of d ln St :
1 1 1
d ln St = dSt − 2 dSt2 = (r − σ 2 )dt + σdWt
St 2St 2
So ln St is a drifted Brownian motion
Z t Z t
1 2
ln St = ln S0 + (r − σ )ds + σdW (5)
0 2 0
1
= ln S0 + (r − σ 2 )t + σWt (6)
2
And the solution of SDE (4) is
1
St = S0 exp (r − σ 2 )t + σWt (7)
2
Numerical Methods in QF (QF5204) Binomial Tree Models 13 / 60
Black-Scholes Formula
The present value (t = 0) of a call option with strike at K and expiry at T
is
C (S0 , K , T ) = e −rT EQ [(ST − K )+ ] = S0 N(d+ ) − Ke −rT N(d− ) (8)
where
r is the risk-free rate
ln SK0 + (r ± 12 σ 2 )T
d± = √
σ T
N(·) is the standard cumulative normal function
And the price of a put option is
P(S0 , K , T ) = e −rT EQ [(K − ST )+ ] = Ke −rT N(−d− ) − S0 N(−d+ ) (9)
Numerical Methods in QF (QF5204) Binomial Tree Models 14 / 60
Black-Scholes Formula Implementation
Input:
option type, strike, current stock price, volatility, and interest rate
1 def cnorm(x):
2 return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0
3 def bsPrice(S, r, vol, payoffType, K, T):
4 fwd = S * math.exp(r * T)
5 stdev = vol * math.sqrt(T)
6 d1 = math.log(fwd / K) / stdev + stdev / 2
7 d2 = d1 - stdev
8 if payoffType == PayoffType.Call:
9 return math.exp(-r * T) * (fwd * cnorm(d1) - cnorm(d2) * K)
10 elif payoffType == PayoffType.Put:
11 return math.exp(-r * T) * (K * cnorm(-d2) - cnorm(-d1) * fwd)
12 else:
13 raise Exception("not supported payoff type", payoffType)
14 # test ---
15 S, r, vol, K, T, u, d = 100, 0.01, 0.2, 105, 1.0, 1.2, 0.8
16 print("blackPrice: ", bsPrice(S, r, vol, T, K, PayoffType.Call))
17 print("oneStepTree: ", oneStepBinomial(S, r, u, d, PayoffType.Call, K, T))
Do they agree on the price? Why not?
Numerical Methods in QF (QF5204) Binomial Tree Models 15 / 60
Where is the Gap?
The inputs for the option are the same, how about the market inputs?
One step binomial tree model:
▶ current stock price S0 ,
▶ risk free interest rate r ,
▶ up state Su and down state Sd
Black-Schole model:
▶ current stock price S0 ,
▶ risk free interest rate r ,
▶ volatility σ
The two models are making different assumption on the distribution of the
stock price at T .
Numerical Methods in QF (QF5204) Binomial Tree Models 16 / 60
Possible to Build the Bridge?
Distributions of ST
Binomial tree: two state discrete distribution.
S0 e rT −Sd
▶ Risk neutral probability of Su : p = Su −Sd
▶ Risk neutral probability of Sd : 1 − p
▶ Mean of St : S0 e rT
▶ Variance of St : pSu2 + (1 − p)Sd2 − S02 e 2rT
Black-Scholes: continuous log-normal distribution
1 2 )t+σW
St = S0 e (r − 2 σ t
(10)
▶ Mean of St : S0 e rT
2
▶ Variance of St : S02 e 2rt e σ t − S02 e 2rt
They have the same mean, so can we match the variance?
Numerical Methods in QF (QF5204) Binomial Tree Models 17 / 60
Matching the Variance
Stock price is always positive, we can rewrite Su = uS0 , Sd = dS0 with
u > 0, d > 0
The variance of the binomial tree becomes
S02 (pu 2 + (1 − p)d 2 ) − S02 e 2rt (11)
To match the variance with Black-Sholes’s, we need to satisfy:
2t e rT − d
e 2rt+σ = pu 2 + (1 − p)d 2 , p= (12)
u−d
One equation and two unknowns — Let us impose another constraint
1
d = : CRR binomial tree
u
Numerical Methods in QF (QF5204) Binomial Tree Models 18 / 60
Equation (26) now becomes
2T e rT u 2 − u + 1/u − e rT (1/u)2
e 2rT +σ = = e rT (u + 1/u) − 1 (13)
u − 1/u
2T
e rT u 2 − (e 2rT +σ + 1)u + e rT = 0 (14)
rT +σ 2 T
u 2 − (e + e −rT )u + 1 = 0 (15)
Solving the quadratic equation (14) we have
√
b ± b2 − 4 2
u= , where b = e rT +σ T + e −rT (16)
2
The two solutions correspond to u and d, and we expect u > 1 since its
the up state, so
√
b + b2 − 4
u= (17)
√2
b − b2 − 4 1
d= = (18)
2 u
Numerical Methods in QF (QF5204) Binomial Tree Models 19 / 60
Now It Is Consistent
Now we can derive u and d from σ, making the input about the market
the same for both pricers
1 def oneStepBinomial2(S, r, vol, optType, K, T):
2 b = math.exp(vol * vol * T+r*T) + math.exp(-r * T)
3 u = (b + math.sqrt(b*b - 4)) / 2
4 d = 1/u
5 p = (math.exp(r * T) - d) / (u-d)
6 if optType == PayoffType.Call:
7 return math.exp(-r * T) * (p * max(S * u - K, 0) + (1-p) * max(S * d -
K, 0))
8 # test ---
9 S,r,vol,K,T,u,d = 100, 0.01, 0.2, 105, 1.0, 1.2, 0.8
10 print("blackPrice: \t", bsPrice(S, r, vol, PayoffType.Call, K, T))
11 print("oneStepTree1: \t", oneStepBinomial(S, r, u, d, PayoffType.Call, K, T))
12 print("oneStepTree2: \t", oneStepBinomial2(S, r, vol, PayoffType.Call, K, T))
Numerical Methods in QF (QF5204) Binomial Tree Models 20 / 60
Bringing It Closer
Now we have first moment and second moment matched, have we
closed the gap between the two pricers now? Sort of, but we are still
subject to discretization errors – our one step tree is very coarse
Natural way to extend our model is to make more steps and more
states
p S2,0 = uuS0
S1,0 = uS0
p
S0 1−p S2,1 = udS0
1−p S1,1 = dS0 p
1−p S2,2 = ddS0
t=0 t = 12 T t=T
Numerical Methods in QF (QF5204) Binomial Tree Models 21 / 60
Multi-Step Binomial Tree
Fortunately, our tree is recombining: the two intermediate states
collapse to the same final state because ud = du. Otherwise the
number of nodes will grow exponentially and will soon become
unmanageable.
The probability associated with each final state:
P(S2,0 ) = p 2 , P(S2,1 ) = 2p(1 − p), P(S2,2 ) = (1 − p)2
p S2,0 = uuS0
S1,0 = uS0
p
S0 1−p S2,1 = udS0
1−p S1,1 = dS0 p
1−p S2,2 = ddS0
t=0 t = 12 T t=T
Numerical Methods in QF (QF5204) Binomial Tree Models 22 / 60
Multi-Step Binomial Tree
Adding one time step will give us one more final state
N i
The probability of the state SN,i is P(SN+1,i ) = p (1 − p)N−i
i
And the probability density function of ST converges to a log-normal
distribution:
0.15 0.15
0.1 0.1
P(ST )
0−2 5 · 10−2
0 0
1 2 3 4 −1 −0.5 0 0.5 1 1.5
1
ST (S0 = 1, N = 30, p = 0.5, u = 1.05, v = u) ln ST (S0 = 1, N = 30, p = 0.5, u = 1.05, v = u1 )
So, we can match as close as we want the Black-Scholes distribution
by matching the first and second moments, and increasing the
number of time steps
Numerical Methods in QF (QF5204) Binomial Tree Models 23 / 60
N-Step Binomial Tree — First Moment
p E[ST |S2,0 ] = u 2 S0
p E[ST |S1,0 ] = uS0 (pu + (1 − p)d)
E[ST |S0,0 ] 1−p E[ST |S2,1 ] = udS0
1−p p
E[ST |S1,0 ] = dS0 (pu + (1 − p)d)
1−p E[ST |S2,2 ] = d 2 S0
t=0 t = 21 T t=T
For two step model
E[ST |S0 ] = S0 (pu + (1 − p)d)2 (19)
By induction for N step model
E[ST |S0 ] = S0 × (pu + (1 − p)d)N (20)
Numerical Methods in QF (QF5204) Binomial Tree Models 24 / 60
N-Step Binomial Tree — Second Moment
p E [ST2 |S2,0 ] = u 4 S02
p E[ST2 |S1,0 ] = u 2 S02 (pu 2 + (1 − p)d 2 )
E[ST2 |S0,0 ] 1−p E [ST2 |S2,1 ] = (udS0 )2
1−p p
E[ST2 |S1,0 ] = d 2 S02 (pu 2 + (1 − p)d 2 )
1−p E [ST2 |S2,2 ] = d 4 S02
t=0 t = 12 T t=T
For two step model
E[ST2 |S0 ] = S02 (pu 2 + (1 − p)d 2 )2 (21)
By induction for N step model
E[ST2 |S0 ] = S02 × (pu 2 + (1 − p)d 2 )N (22)
Numerical Methods in QF (QF5204) Binomial Tree Models 25 / 60
N-Step Binomial Tree — Matching With Black-Scholes
T
We split the N step in equal space, so each time step is ∆t =
N
To match the mean we just need the probability p to be risk neutral:
S0 e r ∆t − dS0 e r ∆t − d
p= = (23)
uS0 − dS0 u−d
then
E[St |S0 ] = S0 (pu + (1 − p)d)N = S0 e r ∆t N = S0 e rT (24)
Similarly, we just need to replace T by ∆t in our one step binomial
model so as to match the second moment:
2
p
e (2r +σ )∆t + 1 + (e (2r +σ2 )∆t + 1)2 − 4e 2r ∆t 1
u= r ∆
, d = (25)
2e t u
Numerical Methods in QF (QF5204) Binomial Tree Models 26 / 60
N-Step Binomial Tree Implementation
1 def crrBinomial(S, r, vol, payoffType, K, T, n):
2 t = T / n
3 b = math.exp(vol * vol * t+r*t) + math.exp(-r * t)
4 u = (b + math.sqrt(b*b - 4)) / 2
5 p = (math.exp(r * t) - (1/u)) / (u - 1/u)
6 # set up the last time slice, there are n+1 nodes at the last time slice
7 payoffDict = {
8 PayoffType.Call: lambda s: max(s-K, 0),
9 PayoffType.Put: lambda s: max(K-s, 0),
10 }
11 vs = [payoffDict[payoffType]( S * u**(n-i-i)) for i in range(n+1)]
12 # iterate backward
13 for i in range(n-1, -1, -1):
14 # calculate the value of each node at time slide i, there are i nodes
15 for j in range(i+1):
16 vs[j] = math.exp(-r * t) * (vs[j] * p + vs[j+1] * (1-p))
17 return vs[0]
18 # test ---
19 S, r, vol, K, T = 100, 0.01, 0.2, 105, 1.0
20 print("blackPrice: \t", bsPrice(S, r, vol, T, K, PayoffType.Call))
21 print("crrNStepTree: \t", crrBinomial(S, r, vol, PayoffType.Call, K, T, 300))
Numerical Methods in QF (QF5204) Binomial Tree Models 27 / 60
Difference between CRR tree and BS analytic
1 import matplotlib.pyplot as plt
2 n = 300
3 S, r, vol, K, T = 100, 0.01, 0.2, 105, 1.0
4 bsPrc = bsPrice(S, r, vol, PayoffType.Call, K, T)
5 crrErrs = [(crrBinomial(S,r,vol,PayoffType.Call,K,T,i) - bsPrc) for i in range(1, n)]
6 plt.plot(range(1, n), crrErrs, label = "CRR - BSAnalytic")
7 plt.xlabel(’number of tree steps’)
8 plt.legend()
9 plt.savefig(’../figs/crrError.eps’, format=’eps’)
2.00
CRR - BSAnalytic
1.75
1.50
1.25
1.00
0.75
0.50
0.25
0.00
0 50 100 150 200 250 300
number of tree steps
Numerical Methods in QF (QF5204) Binomial Tree Models 28 / 60
Easier to look at error on log scale
1 import matplotlib.pyplot as plt
2 n = 300
3 S, r, vol, K, T = 100, 0.01, 0.2, 105, 1.0
4 bsPrc = bsPrice(S, r, vol, PayoffType.Call, K, T)
5 crrErrs = [abs(crrBinomial(S,r,vol,PayoffType.Call,K,T,i) - bsPrc) for i in range(1, n)] # abs error here
6 plt.plot(range(1, n), crrErrs, label = "CRR - BSAnalytic")
7 plt.xlabel(’number of tree steps’)
8 plt.yscale(’log’) # plot on log scale
9 plt.legend()
10 plt.savefig(’../figs/crrLogError.eps’, format=’eps’)
CRR - BSAnalytic
100
10−1
10−2
10−3
10−4
0 50 100 150 200 250 300
number of tree steps
Numerical Methods in QF (QF5204) Binomial Tree Models 29 / 60
What’s the Next Step
European call/put and digital options are not all — we have plenty of
other products we would like to trade
▶ European option with generic payoff
▶ American options
▶ Barrier options
▶ And maybe more — you never know how the industry evolves but you
should be ready for changes
Not all of them have analytic formulae, but our binomial tree pricer
can handle much more
We would like the tree pricer to be implemented elegantly so that it is
▶ easy to maintain
▶ easy to extend
Numerical Methods in QF (QF5204) Binomial Tree Models 30 / 60
American Option
At any point in time, or any node of the tree, we know the
continuation value of the product — through calculating the
conditional expectation
American product allows the option holder to exercise the option any
time
This translates to the choice to make at any node of the tree:
continue holding the option or take the intrinsic value
▶ Continue holding the option — the option is worth its conditional
expectation
▶ Exercise now — the option is worth its intrinsic value
▶ Optimal exercise strategy is to exercise when intrinsic value is worth
more than the continuation value — taking the max
Numerical Methods in QF (QF5204) Binomial Tree Models 31 / 60
American Binomial Tree Pricer — A Trivial Extension
1
2 def crrBinomialAmer(S, r, vol, payoffType, K, T, n):
3 ... # set up tree
4 ... # set up last time slice payoff
5 # iterate backward
6 for i in range(n-1, -1, -1):
7 # calculate the value of each node at time slide i, there are i nodes
8 for j in range(i+1):
9 # here we deal with american early exercise:
10 continuation = math.exp(-r * t) * (vs[j] * p + vs[j+1] * (1-p))
11 vs[j] = max(continuation, payoffDict[payoffType](S * u**(i-j-j)))
12 return vs[0]
Only change to the european pricer crrBinomial():
At each iteration, max is taken between
▶ continuation value — not exercise, and
▶ payoff value — exercise immediately
Numerical Methods in QF (QF5204) Binomial Tree Models 32 / 60
Problems?
Trivial extension is OK for experimenting the algorithms
Not serious code for production usage
▶ Copy-and-paste should be avoided in general
▶ Both “‘crrBinomial“‘ and “‘crrBinomialAmer“‘ do not leave too much
room for payoff extension — what if I want to price a digital option or
call spread?
Numerical Methods in QF (QF5204) Binomial Tree Models 33 / 60
First Step Extension
Encapsulate arguments that belong to an trade-able instrument
For European style options:
▶ expiry T
▶ strike K
▶ payoffType
For American style options:
▶ everything of an European option
▶ early exercise feature
So let us define classes for the trading instruments
Numerical Methods in QF (QF5204) Binomial Tree Models 34 / 60
European option class:
1 class EuropeanOption():
2 def __init__(self, expiry, strike, payoffType):
3 self.expiry = expiry
4 self.strike = strike
5 self.payoffType = payoffType
6 def payoff(self, S):
7 if self.payoffType == PayoffType.Call:
8 return max(S - self.strike, 0)
9 elif self.payoffType == PayoffType.Put:
10 return max(self.strike - S, 0)
11 else:
12 raise Exception("payoffType not supported: ", self.payoffType)
American option class:
1 class AmericanOption():
2 def __init__(self, expiry, strike, payoffType):
3 self.expiry = expiry
4 self.strike = strike
5 self.payoffType = payoffType
6 def payoff(self, S):
7 if self.payoffType == PayoffType.Call:
8 return max(S - self.strike, 0)
9 elif self.payoffType == PayoffType.Put:
10 return max(self.strike - S, 0)
11 else:
12 raise Exception("payoffType not supported: ", self.payoffType)
Numerical Methods in QF (QF5204) Binomial Tree Models 35 / 60
In this way we lift out the code that deals with payoff from our binomial
tree pricer, so that it is more orthogonal to the trade being priced:
1 def crrBinomial(S, r, vol, trade, n):
2 t = trade.expiry / n
3 b = math.exp(vol * vol * t+r*t) + math.exp(-r * t)
4 u = (b + math.sqrt(b*b - 4)) / 2
5 p = (math.exp(r * t) - (1/u)) / (u - 1/u)
6 # d = 1 / u
7 # set up the last time slice, there are n+1 nodes at the last time slice
8 vs = [trade.payoff( S * u**(n-i-i)) for i in range(n+1)]
9 # iterate backward
10 for i in range(n-1, -1, -1):
11 # calculate the value of each node at time slide i, there are i nodes
12 for j in range(i+1):
13 vs[j] = math.exp(-r * t) * (vs[j] * p + vs[j+1] * (1-p))
14 return vs[0]
The only requirement from crrBinomial to trade is the expiry
and payoff function, everything else is generic
Numerical Methods in QF (QF5204) Binomial Tree Models 36 / 60
Dealing with American Option
In order not to “‘copy paste“‘, our first attempt is to add a boolean flag
“‘isAmer“‘ to the signature of “‘crrBinomial“‘
1 def crrBinomial(S, r, vol, trade, isAmer, n):
2 ...
3 for i in range(n-1, -1, -1):
4 # calculate the value of each node at time slide i, there are i nodes
5 for j in range(i+1):
6 vs[j] = math.exp(-r * t) * (vs[j] * p + vs[j+1] * (1-p))
7 if isAmer:
8 vs[j] = max(vs[j], trade.payoff(S * u**(i-j-j)))
9 return vs[0]
It does avoid “‘copy paste“‘, but...
It puts back attributes that belong to the tradeable to the pricing
model, consider these two function calls
1 crrBinomial(S = 100, r = 0.01, vol = 0.2, trade = EuropeanOption(expiry=1, strike=105, payoffType =
PayoffType.Call), isAmer = True, n = 1000)
2 crrBinomial(S = 100, r = 0.01, vol = 0.2, trade = AmericanOption(expiry=1, strike=105, payoffType =
PayoffType.Call), isAmer = False, n = 1000)
without looking at the code of “‘crrBinomial“‘ it’s hard to tell which
one is pricing an American option and which one is European option.
Numerical Methods in QF (QF5204) Binomial Tree Models 37 / 60
Second Attempt
What if we require a function isAmer() -> bool from trade
1 def crrBinomial(S, r, vol, trade, n):
2 ...
3 for i in range(n-1, -1, -1):
4 # calculate the value of each node at time slide i, there are i nodes
5 for j in range(i+1):
6 vs[j] = math.exp(-r * t) * (vs[j] * p + vs[j+1] * (1-p))
7 if trade.isAmer():
8 vs[j] = max(vs[j], trade.payoff(S * u**(i-j-j)))
9 return vs[0]
Better that the attribute of the trade is provided by the trade, but
the pricer is restricted to American trade and non-American trade
Exercise strategy should be associated with the product, not the pricer
Numerical Methods in QF (QF5204) Binomial Tree Models 38 / 60
Look At Our Tree Algorithm Again
Algorithm 1
1: Set up the tree and parameters
2: Initialize the last time slice with final payoff
3: for k = N − 1 to 0 do
4: for i = 0 to k do
5: Calculate the continuation value (discounted expectation)
6: Given the information of the tree node, calculate the option value
at Node(k, i)
7: end for
8: end for
Step 5 belongs to the tree pricer
Step 6 is fully determined by the product:
▶ Input: stock price S, continuation value V , current time t
▶ Output: current option value
Numerical Methods in QF (QF5204) Binomial Tree Models 39 / 60
Pricing American Products With Tree
It makes sense to encapsulate step 6 in the tree algorithm into the
trade-able classes
class AmericanOption():
def __init__(self, expiry, strike, payoffType):
self.expiry = expiry
self.strike = strike
self.payoffType = payoffType
def payoff(self, S):
if self.payoffType == PayoffType.Call:
return max(S - self.strike, 0)
elif self.payoffType == PayoffType.Put:
return max(self.strike - S, 0)
else:
raise Exception("payoffType not supported: ", self.payoffType)
# step 6 in tree algo, exercise logic
def valueAtNode(self, t, S, continuation):
return max(self.payoff(S), continuation)
Numerical Methods in QF (QF5204) Binomial Tree Models 40 / 60
For European option, valueAtNode just pass on the continuation
value
class EuropeanOption():
def __init__(self, expiry, strike, payoffType):
self.expiry = expiry
self.strike = strike
self.payoffType = payoffType
def payoff(self, S):
if self.payoffType == PayoffType.Call:
return max(S - self.strike, 0)
elif self.payoffType == PayoffType.Put:
return max(self.strike - S, 0)
else:
raise Exception("payoffType not supported: ", self.payoffType)
def valueAtNode(self, t, S, continuation):
return continuation
Numerical Methods in QF (QF5204) Binomial Tree Models 41 / 60
Pricing American Products With Tree
The tree pricer now becomes
1 def crrBinomialG(S, r, vol, trade, n):
2 t = trade.expiry / n
3 b = math.exp(vol * vol * t+r*t) + math.exp(-r * t)
4 u = (b + math.sqrt(b*b - 4)) / 2
5 p = (math.exp(r * t) - (1/u)) / (u - 1/u)
6 # d = 1 / u
7 # set up the last time slice, there are n+1 nodes at the last time slice
8 vs = [trade.payoff( S * u**(n-i-i)) for i in range(n+1)]
9 # iterate backward
10 for i in range(n-1, -1, -1):
11 # calculate the value of each node at time slide i, there are i nodes
12 for j in range(i+1):
13 nodeS = S * u**(i-j-j)
14 continuation = math.exp(-r * t) * (vs[j] * p + vs[j+1] * (1-p))
15 vs[j] = trade.valueAtNode(t*i, nodeS, continuation)
16 return vs[0]
There is no more ambiguity pricing European and American options:
1 euroPrc.append(crrBinomialG(S, r, vol, EuropeanOption(expiry, strike, PayoffType.Call), 300))
2 amerPrc.append(crrBinomialG(S, r, vol, AmericanOption(expiry, strike, PayoffType.Call), 300))
Numerical Methods in QF (QF5204) Binomial Tree Models 42 / 60
Testing Binomial Tree Pricer with American Option
1 euroPrc, amerPrc = [],[]
2 S, r, vol = 100, 0.05, 0.2
3 ks = range(50, 150)
4 for k in ks:
5 euroPrc.append(crrBinomialG(S, r, vol, EuropeanOption(1, float(k), PayoffType.Call), 300))
6 amerPrc.append(crrBinomialG(S, r, vol, AmericanOption(1, float(k), PayoffType.Call), 300))
7 plt.plot(ks, euroPrc, ’r’, label=’euroCall’)
8 plt.plot(ks, amerPrc, ’g’, label=’amerCall’)
9 euroPrc, amerPrc = [], []
10 for k in ks:
11 euroPrc.append(crrBinomialG(S, r, vol, EuropeanOption(1, float(k), PayoffType.Put), 300))
12 amerPrc.append(crrBinomialG(S, r, vol, AmericanOption(1, float(k), PayoffType.Put), 300))
13 plt.plot(ks, euroPrc, ’r’, label=’euroPut’)
14 plt.plot(ks, amerPrc, ’g’, label=’amerPut’)
15 plt.legend()
16 plt.show()
50 euroCall
amerCall
euroPut
40 amerPut
30
20
10
0
60 80 100 120 140
Numerical Methods in QF (QF5204) Binomial Tree Models 43 / 60
Generalize the Payoff Function
Now the CRR binomial tree pricer is generic, and if we want to price
European / American exercise style option with any payoff function, we
just need to create a tradeable
1 class EuropeanPayoff():
2 def __init__(self, expiry, payoffFun):
3 self.expiry = expiry
4 self.payoffFun = payoffFun
5 def payoff(self, S):
6 return self.payoffFun(S)
7 def valueAtNode(self, t, S, continuation):
8 return continuation
9
10 class AmericanPayoff():
11 def __init__(self, expiry, payoffFun):
12 self.expiry = expiry
13 self.payoffFun = payoffFun
14 def payoff(self, S):
15 return self.payoffFun(S)
16 def valueAtNode(self, t, S, continuation):
17 return max(self.payoff(S), continuation)
Numerical Methods in QF (QF5204) Binomial Tree Models 44 / 60
Example: Pricing a Call Spread
1 S, r, vol = 100, 0.05, 0.2
2 callSpread = lambda S: min(max(S-90, 0), 10)
3 plt.plot(range(80, 120), [callSpread(i) for i in range(80, 120)] )
4 plt.show()
5 print("Euro callspread: ", crrBinomialG(S, r, vol, EuropeanPayoff(1, callSpread), 300))
6 print("Amer callspread: ", crrBinomialG(S, r, vol, AmericanPayoff(1, callSpread), 300))
10
0
80 85 90 95 100 105 110 115 120
Euro callspread: 6.259190489574921
Amer callspread: 10.0
Practice: plot a spot ladder of Euro/Amer call spread prices, explain what you see.
Numerical Methods in QF (QF5204) Binomial Tree Models 45 / 60
Pricing Barrier Options
Now let us extend the CRR binomial pricer to Barrier Options
A barrier option defines
▶ one or two barrier levels (up or/and down) and
▶ a pre-defined time window (normally from now to option expiry) ¡br¿
such that if the spot price touches the barrier in the given time
window, the option holder obtain (Knock-In) or lose (Knock-Out) a
underlying payoff
The underlying payoff can be
▶ a European option
▶ or just a constant rebate (normally called touch option)
Numerical Methods in QF (QF5204) Binomial Tree Models 46 / 60
Pricing Barrier Options
Knock-Out (KO) barrier option can be priced naturally with tree
▶ At each point of the tree, if the spot price triggers the KO, then the
option is worth 0, otherwise it is worth its continuation value
▶ The continuation value (discounted future expectation) does not
contribute to the nodes triggering the KO
Knock-In (KI) barrier option is not so natural for pricing with tree:
▶ Not-yet knocked in does not mean the value is 0
▶ Requires an auxiliary variable to price with tree
▶ KIKO parity: KI + KO = Vanilla static replication
We consider knock-out barrier option for now
Numerical Methods in QF (QF5204) Binomial Tree Models 47 / 60
Barrier Option Class
1 class BarrierOption():
2 def __init__(self, downBarrier, upBarrier, barrierStart, barrierEnd,
underlyingOption):
3 self.underlyingOption = underlyingOption
4 self.barrierStart = barrierStart
5 self.barrierEnd = barrierEnd
6 self.downBarrier = downBarrier
7 self.upBarrier = upBarrier
8 self.expiry = underlyingOption.expiry
9 def payoff(self, S):
10 return self.underlyingOption.payoff(S)
11 def valueAtNode(self, t, S, continuation):
12 if t > self.barrierStart and t < self.barrierEnd:
13 if self.upBarrier != None and S > self.upBarrier:
14 return 0
15 elif self.downBarrier != None and S < self.downBarrier:
16 return 0
17 return continuation
Now we can enjoy the fruit of our generic CRR biniomial pricer — no
change needed to the pricer
Numerical Methods in QF (QF5204) Binomial Tree Models 48 / 60
Testing Barrier Options — Up Barriers
1 # varying up barrier
2 S, r, vol, K = 100, 0.05, 0.2, 105
3 eurOpt = EuropeanOption(1, k, PayoffType.Put)
4 euroPrc = crrBinomialG(S, r, vol, eurOpt, 300)
5 barrierPrc, ks = [], range(50, 150)
6 for barrierLevel in ks:
7 prc = crrBinomialG(S, r, vol, BarrierOption(barrierStart = 0, barrierEnd = 1.0, downBarrier = None,
upBarrier = barrierLevel, underlyingOption = eurOpt), n = 300)
8 barrierPrc.append(prc)
9 plt.hlines(euroPrc, ks[0], ks[-1], label = ’euroPrc’)
10 plt.plot(ks, barrierPrc, ’g’, label=’barrierPrc’)
11 plt.xlabel(’up barrier level’); plt.legend(); plt.savefig(’../figs/upKO.eps’, format=’eps’)
8 barrierPrc
7 euroPrc
0
60 80 100 120 140
up barrier level
Numerical Methods in QF (QF5204) Binomial Tree Models 49 / 60
Testing Barrier Options — Down Barriers
1 # varying down barrier
2 S, r, vol, K = 100, 0.05, 0.2, 105
3 eurOpt = EuropeanOption(1, k, PayoffType.Put)
4 euroPrc = crrBinomialG(S, r, vol, eurOpt, 300)
5 barrierPrc, ks = [], range(30, 130)
6 for barrierLevel in ks:
7 prc = crrBinomialG(S, r, vol, BarrierOption(barrierStart = 0, barrierEnd = 1.0, downBarrier =
barrierLevel, upBarrier = None, underlyingOption = eurOpt), n = 300)
8 barrierPrc.append(prc)
9 plt.hlines(euroPrc, ks[0], ks[-1], label = ’euroPrc’)
10 plt.plot(ks, barrierPrc, ’g’, label=’barrierPrc’)
11 plt.xlabel(’down barrier level’); plt.legend(); plt.savefig(’../figs/downKO.eps’, format=’eps’)
barrierPrc
40
euroPrc
30
20
10
0
40 60 80 100 120
down barrier level
Numerical Methods in QF (QF5204) Binomial Tree Models 50 / 60
Testing Barrier Options — Window Barriers
1 # varying barrier window, barrier end
2 S, r, vol, k = 100, 0.05, 0.2, 105
3 eurOpt = EuropeanOption(1, k, PayoffType.Put)
4 euroPrc = crrBinomialG(S, r, vol, eurOpt, 300)
5 barrierPrc = []
6 ks = range(0, 100)
7 for t in ks:
8 prc = crrBinomialG(S, r, vol, BarrierOption(barrierStart = 0, barrierEnd = t / 100.0, downBarrier =
80, upBarrier = 150, underlyingOption = eurOpt), n = 300)
9 barrierPrc.append(prc)
10 plt.hlines(euroPrc, ks[0], ks[-1] / 100, label = ’euroPrc’)
11 plt.plot(ks, barrierPrc, ’g’, label=’barrierPrc’)
12 plt.legend(); plt.xlabel(’window end’); plt.savefig(’../figs/winBarrier.eps’, format=’eps’)
8
barrierPrc
euroPrc
7
3
0 20 40 60 80 100
window end
Numerical Methods in QF (QF5204) Binomial Tree Models 51 / 60
Testing Barrier Options — Window Barriers
1 # varying barrier window, barrier start
2 S, r, vol, k = 100, 0.05, 0.2, 105
3 eurOpt = EuropeanOption(1, k, PayoffType.Put)
4 euroPrc = crrBinomialG(S, r, vol, eurOpt, 300)
5 barrierPrc = []
6 ks = range(0, 100)
7 for t in ks:
8 prc = crrBinomialG(S, r, vol, BarrierOption(barrierStart = t, barrierEnd = 1.0, downBarrier = 80,
upBarrier = 150, underlyingOption = eurOpt), n = 300)
9 barrierPrc.append(prc)
10 plt.hlines(euroPrc, ks[0], ks[-1] / 100, label = ’euroPrc’)
11 plt.plot(ks, barrierPrc, ’g’, label=’barrierPrc’)
12 plt.legend(); plt.xlabel(’window start’); plt.savefig(’../figs/winBarrierStart.eps’, format=’eps’)
8
barrierPrc
euroPrc
7
3
0 20 40 60 80 100
window start
Numerical Methods in QF (QF5204) Binomial Tree Models 52 / 60
More On Binomial Tree Models
Recall that we use CRR binomial tree model [1] where the additional
constraint u = d1 was imposed to restrict the solution space when
trying to match the second moment
2 ∆t
e 2r ∆t+σ = pu 2 + (1 − p)d 2 (26)
Recall also that the first moment is matched by setting
e r ∆t − d
p= (27)
u−d
p uS
S
1−p dS
t t +∆t
Numerical Methods in QF (QF5204) Binomial Tree Models 53 / 60
Alternative Binomial Tree Models
Alternative binomial tree models are built by imposing different constriants
Jarrow-Rudd binomial tree
1
▶ Equal probabily variation (not risk neutral): p = 2
▶ Risk neutral variation
Tian binomial model: match the first three moments
...
M. Joshi presented a comparison of 11 different tree modesl: [2]
Numerical Methods in QF (QF5204) Binomial Tree Models 54 / 60
Jarrow-Rudd Models
Jarrow-Rudd Equal Probability Model (JREQ)
p = 12
√
σ2
u = e (r − 2 )∆t+σ ∆t (28)
σ2
√
d = e (r − 2 )∆t−σ ∆t
−d r ∆t
Not risk neutral since e u−d ̸= 12
Jarrow-Rudd Risk Neutral Model (JRRN)
r ∆t −d
p = e u−d
√
σ2
u = e (r − 2 )∆t+σ ∆t (29)
σ2
√
d = e (r − 2 )∆t−σ ∆t
Use the same u and d as JREQ model, but replace the probability by
risk neutral probability.
Numerical Methods in QF (QF5204) Binomial Tree Models 55 / 60
Tian’s Model
Matching the first three moments:
r ∆t
pu + (1 − p)d = e
2
pu 2 + (1 − p)d 2 = e 2r ∆t+σ ∆t (30)
3 2
pu + (1 − p)d 3 = e 3r ∆t+3σ ∆t
This leads to the parameters:
r ∆t −d
p = e u−d
u = 0.5e r ∆t v (v + 1 + √v 2 + 2v − 3)
√ (31)
d = 0.5e r ∆t v (v + 1 − v 2 + 2v − 3)
v = e σ2 ∆t
Note that all these models calibrate model parameters p, u, d to the drift r
and volatility σ, the binomial tree algorithm does not change — another
sign of generalization.
Numerical Methods in QF (QF5204) Binomial Tree Models 56 / 60
Our Generic Binomial Tree Pricer
1 def binomialPricer(S, r, vol, trade, n, calib):
2 t = trade.expiry / n
3 (u, d, p) = calib(r, vol, t)
4 # set up the last time slice, there are n+1 nodes at the last time slice
5 vs = [trade.payoff(S * u ** (n - i) * d ** i) for i in range(n + 1)]
6 # iterate backward
7 for i in range(n - 1, -1, -1):
8 # calculate the value of each node at time slide i, there are i nodes
9 for j in range(i + 1):
10 nodeS = S * u ** (i - j) * d ** j
11 continuation = math.exp(-r * t) * (vs[j] * p + vs[j + 1] * (1 - p))
12 vs[j] = trade.valueAtNode(t * i, nodeS, continuation)
13 return vs[0]
The tree price expects u, d, p from the model
Each model is responsible for the calib function:
Numerical Methods in QF (QF5204) Binomial Tree Models 57 / 60
Binommial Tree Models Calib
1 def crrCalib(r, vol, t):
2 b = math.exp(vol * vol * t + r * t) + math.exp(-r * t)
3 u = (b + math.sqrt(b * b - 4)) / 2
4 p = (math.exp(r * t) - (1 / u)) / (u - 1 / u)
5 return (u, 1/u, p)
6
7 def jrrnCalib(r, vol, t):
8 u = math.exp((r - vol * vol / 2) * t + vol * math.sqrt(t))
9 d = math.exp((r - vol * vol / 2) * t - vol * math.sqrt(t))
10 p = (math.exp(r * t) - d) / (u - d)
11 return (u, d, p)
12
13 def jreqCalib(r, vol, t):
14 u = math.exp((r - vol * vol / 2) * t + vol * math.sqrt(t))
15 d = math.exp((r - vol * vol / 2) * t - vol * math.sqrt(t))
16 return (u, d, 1/2)
17
18 def tianCalib(r, vol, t):
19 v = math.exp(vol * vol * t)
20 u = 0.5 * math.exp(r * t) * v * (v + 1 + math.sqrt(v*v + 2*v - 3))
21 d = 0.5 * math.exp(r * t) * v * (v + 1 - math.sqrt(v*v + 2*v - 3))
22 p = (math.exp(r * t) - d) / (u - d)
23 return (u, d, p)
Numerical Methods in QF (QF5204) Binomial Tree Models 58 / 60
Test Binomial Models
1 opt = EuropeanOption(1, 105, PayoffType.Call)
2 S, r, vol, n = 100, 0.01, 0.2, 300
3 bsprc = bsPrice(S, r, vol, opt.payoffType, opt.strike, opt.expiry)
4 crrErrs = [math.log(abs(binomialPricer(S, r, vol, opt, i, crrCalib) - bsprc)) for i in range(1, n)]
5 jrrnErrs = [math.log(abs(binomialPricer(S, r, vol, opt, i, jrrnCalib) - bsprc)) for i in range(1, n)]
6 jreqErrs = [math.log(abs(binomialPricer(S, r, vol, opt, i, jreqCalib) - bsprc)) for i in range(1, n)]
7 tianErrs = [math.log(abs(binomialPricer(S, r, vol, opt, i, tianCalib) - bsprc)) for i in range(1, n)]
8 plt.plot(range(1, n), crrErrs, label = "crr")
9 plt.plot(range(1, n), jrrnErrs, label = "jrrn")
10 plt.plot(range(1, n), jreqErrs, label = "jreq")
11 plt.plot(range(1, n), tianErrs, label = "tian")
12 plt.legend(); plt.savefig(’../figs/btrees.eps’, format=’eps’)
crr
0 jrrn
jreq
−2 tian
−4
−6
−8
−10
0 50 100 150 200 250 300
Numerical Methods in QF (QF5204) Binomial Tree Models 59 / 60
References
John C. Cox, Stephen A. Ross, and Mark Rubinstein.
Option pricing: A simplified approach.
Journal of Financial Economics, 7(3):229–263, 1979.
Mark S. Joshi.
The convergence of binomial trees for pricing the american put.
2007.
Numerical Methods in QF (QF5204) Binomial Tree Models 60 / 60