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 and out 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.

  1. (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] \]
  2. (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] \]
  3. (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] \]
  4. (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:

  1. (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] \]
  2. (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] \]
  3. (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] \]
  4. (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.

Avatar
Issac Lee
PhD candidate, ABD

I believe anyone can learn anything with a proper education process.