Pricing barrier option with Brownian bridge MC simulation

Abstract
In this project, I will model the stock price using the geometric brownian motion. After that, the price of the barrier options will be determine by using the Monte carlo simulation.
Geomatric Brownian motion for Stock modeling
Let us assume that the daily returns of the stock satisfies the follows SDE;
\[ dS(t) = \mu S(t)dt+\sigma S(t)dB(t) \] where \(B(t)\) represents the Brownian motion. Then, the solution of the above SDE is the geometric Brownian motion as follows by Ito’s lemma:
\[ S(t) = S(0)e^{(\mu-\frac{1}{2}\sigma^2)t+\sigma B(t)} \] This means under the real probability measure \(P\), the log return of the stock price during the time \(t\) follows
\[ ln(S(t)/S(0)) \sim \mathcal{N}((\mu - \frac{1}{2} \sigma^2)t, \sigma^2 t) \]
Since the distribution holds for any two interval, we have
\[ ln\left(\frac{S(t)}{S(t-1)}\right) \sim \mathcal{N}((\mu - \frac{1}{2} \sigma^2)\Delta t, \sigma^2 \Delta t) \]
Thus, the parameters in the stock price generator should be
- stock drift: \(\mu\)
- stock vol: \(\sigma\)
- maturity: T
- time step: \(\Delta t\)
- initial stock price: \(S(0)\)
- num of paths to generate
For the result of the function, I choose different column represents the indivisual path of the stock price. For example, the fisrt column indicates the path of the first stock price.
Method 1: Brute Force for
loop
The first way we can attack is to use for loop. It is very straight forward method. First, we will generate the random samples from normal distribution. The total number of sample should be (maturity / dt) * numPath
since we want to generate the entire paths. After that genStock1
uses for loop to calculate the cumulative summation of the log return.
genStock1 <- function(mu, sigma, S_0, maturity, numPath, dt = 1){
totaln <- (maturity / dt) * numPath
V <- matrix(rnorm(totaln,
mean = (mu - 0.5 * sigma^2) * dt,
sd = sigma * sqrt(dt)), ncol = numPath)
result <- matrix(0, nrow = dim(V)[1], ncol = dim(V)[2])
for (i in 1:dim(V)[2]){
tempsum <- 0
for (j in 1:dim(V)[1]){
result[j, i] <- V[j, i] + tempsum
tempsum <- V[j, i] + tempsum
}
}
stock_price <- S_0 * rbind(1, exp(result))
ts(stock_price,
start = 0, end = maturity, deltat = dt,
names = paste0("Stock", 1:numPath))
}
Method 2: Use apply
function
The for
loop in R can be replaced my the apply
function. It is little faster and can be written in a compact code.
genStock2 <- function(mu, sigma, S_0, maturity, numPath, dt = 1){
totaln <- (maturity / dt) * numPath
V <- matrix(rnorm(totaln,
mean = (mu - 0.5 * sigma^2) * dt,
sd = sigma * sqrt(dt)), ncol = numPath)
stock_price <- S_0 * rbind(1, exp(apply(V, 2, cumsum)))
ts(stock_price,
start = 0, end = maturity, deltat = dt,
names = paste0("Stock", 1:numPath))
}
Confirmation
Using the same seed, let us confirm they generate the same path.
set.seed(123)
stock_path1 <- genStock1(0.126, 0.350, 100, maturity = 1, numPath = 5, dt = 1/365)
set.seed(123)
stock_path2 <- genStock2(0.126, 0.350, 100, maturity = 1, numPath = 5, dt = 1/365)
head(stock_path1)
## Stock1 Stock2 Stock3 Stock4 Stock5
## [1,] 100.00000 100.00000 100.0000 100.0000 100.00000
## [2,] 98.99603 99.63407 103.2547 100.8351 98.43947
## [3,] 98.59695 100.34453 104.9537 101.6676 99.78169
## [4,] 101.47100 98.63905 101.3003 103.6428 100.71824
## [5,] 101.62018 100.21755 103.9497 104.5846 101.91342
## [6,] 101.87923 99.39229 103.8614 104.1350 102.72568
head(stock_path2)
## Stock1 Stock2 Stock3 Stock4 Stock5
## [1,] 100.00000 100.00000 100.0000 100.0000 100.00000
## [2,] 98.99603 99.63407 103.2547 100.8351 98.43947
## [3,] 98.59695 100.34453 104.9537 101.6676 99.78169
## [4,] 101.47100 98.63905 101.3003 103.6428 100.71824
## [5,] 101.62018 100.21755 103.9497 104.5846 101.91342
## [6,] 101.87923 99.39229 103.8614 104.1350 102.72568
Speed comparison
Using rbenchmark
package, we can compare the performance of thses two function.
library(rbenchmark)
## Warning: package 'rbenchmark' was built under R version 4.0.3
# reset seed
rm(.Random.seed, envir=globalenv())
benchmark_result <- benchmark(
"genStock1" = {genStock1(0.126, 0.350, 100, maturity = 1, numPath = 1000, dt = 1/365)},
"genStock2" = {genStock2(0.126, 0.350, 100, maturity = 1, numPath = 1000, dt = 1/365)}
)
benchmark_result[,1:6]
## test replications elapsed relative user.self sys.self
## 1 genStock1 100 7.13 1.71 7.01 0.11
## 2 genStock2 100 4.17 1.00 4.14 0.03
As we can see that the genStock2
performs better than the genStock1
and it is more compact code. Thus, I will use the genStock2
to do the Monte Carlo simulation in the later section.
ts
class
The last part of the function makes the stock price result as a ts
class in R, which is the special class for the timeseries oject.
ts(stock_price,
start = 0, end = maturity, deltat = dt,
names = paste0("Stock", 1:numPath))
class(stock_path1)
## [1] "mts" "ts" "matrix"
attributes(stock_path1)
## $dim
## [1] 366 5
##
## $dimnames
## $dimnames[[1]]
## NULL
##
## $dimnames[[2]]
## [1] "Stock1" "Stock2" "Stock3" "Stock4" "Stock5"
##
##
## $tsp
## [1] 0 1 365
##
## $class
## [1] "mts" "ts" "matrix"
To exploit the ts
class, we can just wrap a base plot function to draw the stock path as follows:
R code for Figure
stockPlot <- function(stockPath){
nPath <- attributes(stockPath)$dim[2]
# ploting use base plot function
plot(stockPath,
oma=c(2, 3, 4, 2),
plot.type = "single",
ylab = "Stock Price",
lty = 1, col = 1:nPath)
mytitle <- "Simulation of Stock prices"
mysubtitle <- paste("Number of Stocks:", nPath)
mtext(side=3, line=3, adj=1, cex=1.5, mytitle)
mtext(side=3, line=2, adj=1, cex=1, mysubtitle)
}
stock_pathsample <- genStock2(mu = 0.126, sigma = 0.350, S_0 = 100,
maturity = 1, numPath = 1000, dt = 1/365)
stockPlot(stock_pathsample)
Confirmation of the stock generator using Call option price
In the financial mathematics, the call option price can be written as the present value of the expected payoff under the risk-neutral measure \(Q\).
\[ C_{0}=e^{-r(T)}\mathbb{E}^{Q}\left[\left(S\left(T\right)-K\right)_{+}\right] \]
The Black-Scholes formula tells us that the price of the call option is
\[ {\displaystyle { \begin{aligned} C_{0}&=S(0) \Phi(d_{1})-Ke^{-rT}\Phi(d_{2}),\\ d_{1}&={\frac{\ln \left({\frac {S(0)}{K}}\right)+\left(r+{\frac {1}{2}\sigma ^{2}}\right)T}{\sigma {\sqrt {T}}}},\\ d_{2}&={\frac{\ln \left({\frac {S(0)}{K}}\right)+\left(r-{\frac {1}{2}\sigma ^{2}}\right)T}{\sigma {\sqrt {T}}}}. \end{aligned}} } \] where \(\Phi\) represents the cdf function of the standard Normal distribution.
# Confirm using Call option
price_Call <- function(S_0, K, r, vol, maturity, t = 0){
d_1or2 <- function(S_0, K, r, sigma, maturity, t = 0, option){
if (option == 1) option else option <- option - 3
(log(S_0/K) + (r + option*(sigma^2 / 2)) * (maturity-t) ) /
(sigma * sqrt(maturity-t))
}
d1 <- d_1or2(S_0, K, r, vol, maturity, t, 1)
d2 <- d_1or2(S_0, K, r, vol, maturity, t, 2)
S_0 * pnorm(d1) - K*exp(-r*(maturity - t)) * pnorm(d2)
}
Monte Carlo simulation for call price
By the law of large number, we can calculate the call option by simulating the stock price.
r <- 0.05
Maturity <- 1
stock_path <- genStock2(mu = r, sigma = 0.2,
S_0 = 100, maturity = Maturity,
numPath = 100000, dt = 1)
# filter payoff
stock_path <- stock_path[Maturity +1,]
# PV and take expectation
mean(exp(-r*Maturity)*pmax(stock_path - 100, 0))
## [1] 10.43656
c_0 <- price_Call(S_0 = 100, K = 100, r = 0.05, vol = 0.2, maturity = 1)
Let us figure out the distribtuion of the MC based call option price.
result_mc <- rep(0, 500)
for (i in 1:500){
stock_path <- genStock2(mu = r, sigma = 0.2,
S_0 = 100, maturity = Maturity,
numPath = 10000, dt = 1)
# filter payoff
stock_path <- stock_path[Maturity +1,]
# PV and take expectation
result_mc[i] <- mean(exp(-r*Maturity)*pmax(stock_path - 100, 0))
}
R code for Figure
hist(result_mc, oma=c(2, 3, 3, 2),
breaks = seq(min(result_mc), max(result_mc), length.out = 30),
main = NULL,
xlab = "Call price",
xlim = c(10, 11))
mytitle <- "Empirical Dist. of MC based call prices"
mysubtitle <- paste("Mean:", round(mean(result_mc), 3), ",",
"Std:", round(sd(result_mc), 3))
mtext(side=3, line=3, adj=1, cex=1.5, mytitle)
mtext(side=3, line=2, adj=1, cex=1, mysubtitle)
abline(v = c_0, col = "red")
abline(v = mean(result_mc), col = "blue")
In the figure, the blue bar represents the mean of the emprical distribution of the MC based call price while the red bar represents the B-S formular price.
Barrier option
Barrier option has various types. There are two factors used to classify the barrier options:
- Knock in or out:
in
implies the option will be activated andout
implies the option will be deactivated. - Direction: Stock can knock the barrier up or down
By combining these two concepts; we can itemize the basic barrier options for each Call and Put option. The name of barrier option indicates the condition of activation (in
) or deactivation (out
). For example, Up and in
option will be activated (in)
when the stock knocks up
the barrier, while Down and out
option will be deactivated (out)
when the stock knocks down
the barrier.
R code for Figure
check_barrier <- apply(stock_pathsample, 2, max) > 230
turnoff_paths <- stock_pathsample[,check_barrier]
stockPlot(stock_pathsample)
abline(h = 230)
for(i in 1:dim(turnoff_paths)[2]){
points(turnoff_paths[,i], col = rgb(1,1,1, 1), type = "l")
}
outNumber <- paste("Number of outed stocks:", sum(check_barrier))
mtext(side=3, line=3, adj=1, cex=1, outNumber)
title(main = "Payoff of Up-and-out call option")
In the above figure of simulated stock prices, the horizontal line represents the out barrier at the level of 230. The white outed stock paths indicate the paths that knock up the barrier. For these stocks, the payoff of the up-and-out options will be zero.
Payoff of the barrier options
Let us denote the following notations:
- Stock price at time \(t\): \(S(t)\)
- Strike price: \(K\)
- Barrier Level: \(L\)
- Maturity: \(T\)
Then, we can write the pay off of the following four barreir call options. Note that \(\unicode{x1D7D9}_{[\cdot]}\) represents the indicator function which returns 1 when the condition in the braket is true.
- (Knock) Up and in call barrier, assume \(S(0) < L\). \[ (S(T) - K)_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{max}(S(t)) \ge L \right] \]
- (Knock) Up and out call barrier, assume \(S(0) < L\). \[ (S(T) - K)_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{max}(S(t)) < L \right] \]
- (Knock) Down and in call barrier, asumme \(S(0) > L\). \[ (S(T) - K)_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{min}(S(t)) \leq L \right] \]
- (Knock) Down and out call barrier, assume \(S(0) > L\). \[ (S(T) - K)_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{min}(S(t)) > L \right] \]
One of the thing that the readers notice that the vanilla call option pay-off can be decomposed into the in
and out
barrier options. For example, combining the Up-and-in option and the Up-and-out option, we can make a vanilla option pay-off. This means, the summation of the Up-and-in and out call options should be the same as the price of the vanilla call. This also can be confirm by looking at the fact that the summation of the following indicator functions will be always the same as 1.
\[ \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{max}(S(t)) \ge L \right] + \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{max}(S(t)) < L \right] = 1 \]
In the other hand, the pay off of the four put barreir options will be as follows:
- (Knock) Up and in put barrier, assume \(S(0) < L\). \[ (K - S(T))_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{max}(S(t)) \ge L \right] \]
- (Knock) Up and out put barrier, assume \(S(0) < L\). \[ (K - S(T))_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{max}(S(t)) < L \right] \]
- (Knock) Down and in put barrier, asumme \(S(0) > L\). \[ (K - S(T))_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{min}(S(t)) \leq L \right] \]
- (Knock) Down and out put barrier, assume \(S(0) > L\). \[ (K - S(T))_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{min}(S(t)) > L \right] \]
Theoretical price of up-and-in put barrier
The theoretical price of up or down barrier is little complicated since the pay-off is the function of maximum or minimum of the stock prices during time from \(0\) to \(T\). Among these, I choose price formula of the up-and-out put barrier in this section as a reference value of the MC simulation value.
\[ P_0^{UI} = e^{-rT}\mathbb{E}^{Q}\left[ (K - S(T))_{+} \unicode{x1D7D9}_\left[\underset{0 \leq t \leq T}{max}(S(t)) \ge L \right] \right] \]
In the actuarial scinece field, this expectation can be cleverly calculated using exponential tilting called the Esscher transform.
Note that when the Brownian motion \(B(t)\) with drift \(\mu\) and \(\sigma\) and \(M(T)\) represents the maximum of the Brownian motions during time from \(0\) to \(T\), it satisfies the following equation by the reflection principle.
\[ \begin{align} P_{UO}(x,m;\mu,\sigma, T) &= P\left(B(T) \leq x, M(T) \leq m \right) \\ &= \Phi\left(\frac{x-\mu T}{\sigma \sqrt{T}}\right) - e^{\frac{2\mu}{\sigma^2}m}\Phi\left(\frac{x-2m-\mu T}{\sigma \sqrt{T}}\right) \end{align} \]
Note that \(\Phi\) indicates the cdf of the standard normal distribution. Then, the price of the up-and-out put barrier option with parameters.
- Interest rate: \(r\)
- Strike price: \(K\)
- Barrier level: \(L\)
- Stock volatility: \(\sigma\)
- Stock drift: \(\mu\)
- Maturity: \(T\)
- Stock value at time \(0\): \(S(0)\)
can be written as the following compact form using the above probability function \(P_{UO}\):
\[ \begin{align} P_0^{UO} = K e^{-rT}P_{UO}&\left(a, b; r-\frac{1}{2}\sigma^2, \sigma, T\right) - \\ &S(0)P_{UO}\left(a, b;r+\frac{1}{2}\sigma^2, \sigma, T\right) \end{align} \] where \(a\) and \(b\) represent the \(a = ln\left(\frac{K}{S(0)}\right), b = ln\left(\frac{L}{S(0)}\right)\).
Let us define p_UO
function.
p_UO <- function(x, m, mu, sigma, maturity){
d_a <- (x - mu * maturity) / (sigma * sqrt(maturity))
d_b <- (x - 2*m - mu * maturity) / (sigma * sqrt(maturity))
pnorm(d_a) - exp(2*mu*m / sigma^2) * pnorm(d_b)
}
The price of the up-and-out put option with the parameters
- Interest rate: \(r = 0.05\)
- Strike price: \(K = 110\)
- Barrier level: \(L = 200\)
- Stock volatility: \(\sigma = 0.2\)
- Stock drift: \(\mu = 0.126\)
- Maturity: \(T = 1\)
- Stock value at time \(0\): \(S(0)=100\)
will be the same as follows:
S_0 <- 100
r <- 0.05
K <- 110
L <- 110
mu <- 0.126
sigma <- 0.2
maturity <- 1
delta_t <- 1/365
put_UpOut <- function(S_0, K, L, r, sigma, maturity){
a <- log(K / S_0)
b <- log(L / S_0)
N_da <- p_UO(a, b, r - 0.5 * sigma^2, sigma, maturity)
N_db <- p_UO(a, b, r + 0.5 * sigma^2, sigma, maturity)
K*exp(-r*maturity)*N_da - S_0*N_db
}
put_uo <- put_UpOut(S_0 = 100, K = 110, L = 110,
r = 0.05, sigma = 0.2, maturity = 1)
put_uo
## [1] 7.139859
Plain MC simulation the price of up-and-out put barrier
By generating the stock path during the period from zero to maturity, the price of the up-and-out put barrier can be obtained as follows:
stock_path <- genStock2(S_0 = 100, mu = r,
sigma = 0.2, maturity = 1,
numPath = 100000, dt = delta_t)
# checking out the hitting barrier
check_barrier <- as.integer(apply(stock_path, 2, max) < L)
activated_paths <- stock_path[,which(check_barrier == 1)]
# filter payoff
activated_paths <- activated_paths[maturity * delta_t^(-1) + 1,]
activated_payoff <- pmax(K - activated_paths, 0)
# attached knock out payoff
activated_payoff <- c(activated_payoff, rep(0, sum(!check_barrier)))
# PV and take expectation
plain_mc <- mean(exp(-r*maturity) * activated_payoff)
plain_mc
## [1] 7.467147
# theoretical value
put_uo
## [1] 7.139859
However, this method is little slow and efficient, but also inaccurate. This is because, since we are gerating the stock path discritely, we can not determine that wheter the stock hit the barrier or not eventhough the two consecutive stock prices are close to the barrier. For example, let us assume we have observed the following for \(\epsilon > 0\):
\[ L - \epsilon < S(t_i) < L \text{ and } L - \epsilon < S(t_{i+1}) < L \]
Even if the epsilon is really small, we cannot say the stock hit the barrier level \(L\) until we generate the stock path during the interval. Thus, the accuracy of the plain MC method for the up-and-out put barrier depends on the sample size but also the step size.
MC simulation with the Brownian bridge
This method does not use the brownian bridge directly, however, it is quite clever since it allow us to make the accuracy of the MC only to depend on the sample size \(n\). From the Brownian bridge property, we know that the following probability holds;
The law of Brownian bridge
For a Brownian motion \(B(t)\) with drift \(\mu\) and \(\sigma\) where \(0 \leq t \leq T\) and for \(a<L\), the probability of the given brownian bridge cross the barrier is as follows:
\[ \begin{align} & P(M(T) > m | B(0) = a, B(T) = b) \\ = &min\left(exp\left(-2 \frac{(m-a)(m-b)}{\sigma^2T} \right), 1\right) \end{align} \] where \(M(T)\) represents the maximum of the brownian motion \(B(t)\) during the time \(0 \leq t \leq T\).
Note that the probability that the brownian bridge crossing the barrier does not depends on the drift term. Now, we know that
\[ \frac{dS(t)}{S(t)} = (r - \frac{1}{2}\sigma^2)dt + \sigma dB(t) \] which means, under \(Q\) measure
\[ S(t) = S(0)e^{X(t)}, \quad X(t) \sim \mathcal{N}((r - \frac{1}{2}\sigma^2)t, \sigma^2 t) \]
thus, the crossing probability for the pinned geometric brownian motion for the barrier level \(L\) will be
\[ \begin{align} &P\left(\underset{0\leq t \leq T}{max} (S(t)) > L \Bigg\vert S(0) = s_0, S(T) = s_T \right) \\ =&P\left( \underset{0\leq t \leq T}{max} (X(t)) > log(L / s_0) \Bigg\vert X(0) = 0, X(T) = log(s_T / s_0) \right) \\ \end{align} \]
Let us define the function as follows:
crossp <- function(a, b, m, sigma, maturity){
pmin(exp(-2 * (m - a)*(m - b) / (sigma^2 * maturity)), 1)
}
crosspStock <- function(s_0, s_T, L, sigma, maturity){
m <- log(L / s_0)
a <- 0
b <- log(s_T / s_0)
cross_prob <- crossp(a, b, m, sigma, maturity)
# prob. of hitting the barrier
cross_prob
}
We can confirm the brownian bridge MC generates more accurate pricing value for the same stock path as follows:
# calculate final stock price & calculate crossing prob.
payoff <- stock_path[maturity * delta_t^(-1) + 1,]
cross_prob <- crosspStock(100, payoff, L, 0.2, 1)
# calculate vanilla put option payoff
payoff <- pmax(K - payoff, 0)
# turn off payoff when it hits the barrier
u <- runif(length(cross_prob))
check_barrier <- rep(1, length(u))
check_barrier[u < cross_prob] <- 0
payoff <- payoff * check_barrier
# calculate price
bridge_mc <- mean(exp(-r*maturity) * payoff)
bridge_mc
## [1] 7.172284
# plain MC
plain_mc
## [1] 7.467147
# theoretical value
put_uo
## [1] 7.139859
Note that stock_path
variable was generated in the Plain MC section for up-and-out put barrier.