Variance reduction methods for option pricing with Monte Carlo simulation
In this post, we assume a hypothetical (asian) option with the following specifications and try different varience reduction methods when employing Monte Carlo simulation for pricing this option.
S_0 = 50
K = 52
sigma = 0.5
r = 0.05
N = 30
Before starting, I define a function that returns estimation, 95% confidence interval and its length:
estimate = function(vector){
NREP = length(vector)
est = mean(vector)
lower_b = est - sd(vector) * qnorm(0.975) / sqrt(NREP)
upper_b = est + sd(vector) * qnorm(0.975) / sqrt(NREP)
len = upper_b - lower_b
return(c(est, lower_b, upper_b, len))
}
European option pricing
First I define a price calculator function which calculates payoff at time T and then discounts it back to the present time.
price_calc = function(S_T, t, K, r){
library(Rfast)
payoff = rowMaxs(cbind(0, S_T - K), value = TRUE)
return (exp(-r * t / 365) * payoff)
}
set.seed(16)
NREP = 1000
Z = rnorm(NREP)
S_T = S_0 * exp((r - 0.5 * sigma^2) * N / 365 + sigma * Z * sqrt(N / 365))
current_price = price_calc(S_T, N, K, r)
est = estimate(current_price)
paste("The fair price for this option is:", est[1])
## [1] "The fair price for this option is: 2.14767605028461"
paste("Confidence interval for the estimation is: (",est[2], ",", est[3], ")")
## [1] "Confidence interval for the estimation is: ( 1.90133340831646 , 2.39401869225276 )"
Asian Option Pricing (using naive MC)
asian_price_calc = function(price_path, t, K, r){
library(Rfast)
s_bar = mean(price_path)
payoff = rowMaxs(cbind(0, s_bar - K), value = TRUE)
return (exp(-r * t / 365) * payoff)
}
set.seed(5)
NREP = 1000
steps = 30
price_nmc = c()
Z = matrix(rnorm(NREP*steps), NREP, steps)
for (i in 1:NREP){
price_path = c(S_0)
for (j in 1:steps){
new_price = price_path[j] * exp((r - 0.5 * sigma^2) / 365 + sigma * Z[i,j] * sqrt(1 / 365))
price_path = c(price_path, new_price)
}
p = asian_price_calc(price_path, 30, K, r)
price_nmc = c(price_nmc, p)
}
est = estimate(price_nmc)
paste("The fair price for this option is:", est[1])
## [1] "The fair price for this option is: 0.90531167321582"
paste("Confidence interval for the estimation is: (",est[2], ",", est[3], ")")
## [1] "Confidence interval for the estimation is: ( 0.791689961310881 , 1.01893338512076 )"
results = est
Control Variate for Asian Option Pricing
We choose geometric-average asian call option as the control variate for the arithmatic-average asian call option. We call the geometric asian option price θ and we can analytically obtain it by (6.79) here and I will implement the computing function here:
exact_geo_asian = function(S_0, K, t, r, sigma, N){
c3 = 1 + 1/N
c2 = sigma * ((c3 * t / 1095) * (1 + 1 / (2 * N))) ^ 0.5
c1 = (1 / c2) * (log(S_0 / K) + (c3 * t / 730) * (r - 0.5 * sigma^2) + (c3 * sigma^2 * t / 1095) * (1 + 1 / (2 * N)))
theta = S_0 * pnorm(c1) * exp(-t * (r + c3 * sigma^2 / 6) * (1 - 1/N) / 730) - K * pnorm(c1 - c2) * exp(-r * t / 365)
return(theta)
}
So, the exact price of geometric asian option is:
theta = exact_geo_asian(S_0, K, t=30, r, sigma, N)
theta
## [1] 0.9096794
So, \(\theta = 0.9097\). Now, control variate estimation of arithmatic asian option can be obtained by:
\[\hat \mu_{CV} = \hat \mu_{MC} + \lambda (\hat \theta_{MC} - \theta)\]Where the best choice for λ (which decreases the variance most) is:
\[\lambda = \frac{-cov(\hat \mu_{MC}, \hat \theta_{MC})}{var(\hat \theta_{MC})}\]And the covarience and varience need to be estimated using
simulation.
We also need to simulate geometric asian option:
geo_asian_price_calc = function(price_path, t, K, r){
library(Rfast)
s_tilda = exp(mean(log(price_path)))
payoff = rowMaxs(cbind(0, s_tilda - K), value = TRUE)
return (exp(-r * t / 365) * payoff)
}
set.seed(5)
NREP = 1000
steps = 30
g_price_nmc = c()
Z = matrix(rnorm(NREP*steps), NREP, steps)
for (i in 1:NREP){
price_path = c(S_0)
for (j in 1:steps){
new_price = price_path[j] * exp((r - 0.5 * sigma^2) / 365 + sigma * Z[i,j] * sqrt(1 / 365))
price_path = c(price_path, new_price)
}
p = geo_asian_price_calc(price_path, 30, K, r)
g_price_nmc = c(g_price_nmc, p)
}
mean(g_price_nmc)
## [1] 0.8691086
Therefore, \(\hat \theta_{MC} = 0.8691\). Now, we estimate \(\lambda\) as follows:
est_cov = sum((price_nmc - mean(price_nmc)) * (g_price_nmc - mean(g_price_nmc))) / (NREP * (NREP - 1))
est_var = sum((g_price_nmc - mean(g_price_nmc))^2) / (NREP * (NREP - 1))
lambda = - est_cov / est_var
lambda
## [1] -1.035134
CV_prices = price_nmc + lambda * (g_price_nmc - theta)
est = estimate(CV_prices)
paste("The fair price for this option is:", est[1])
## [1] "The fair price for this option is: 0.94730790041362"
paste("Confidence interval for the estimation is: (",est[2], ",", est[3], ")")
## [1] "Confidence interval for the estimation is: ( 0.944336365429991 , 0.95027943539725 )"
results = rbind(results, est)
Antithetic method for Asian Option Pricing
set.seed(345)
NREP = 1000
steps = 30
anti_price1 = c()
anti_price2 = c()
Z = matrix(rnorm(NREP*steps), NREP, steps)
for (i in 1:NREP){
price_path1 = c(S_0)
price_path2 = c(S_0)
for (j in 1:steps){
new_price1 = price_path1[j] * exp((r - 0.5 * sigma^2) / 365 + sigma * Z[i,j] * sqrt(1 / 365))
price_path1 = c(price_path1, new_price1)
new_price2 = price_path2[j] * exp((r - 0.5 * sigma^2) / 365 - sigma * Z[i,j] * sqrt(1 / 365))
price_path2 = c(price_path2, new_price2)
}
p1 = asian_price_calc(price_path1, 30, K, r)
anti_price1 = c(anti_price1, p1)
p2 = asian_price_calc(price_path2, 30, K, r)
anti_price2 = c(anti_price2, p2)
}
anti_price = 0.5 * (anti_price1 + anti_price2)
est = estimate(anti_price)
paste("The fair price for this option is:", est[1])
## [1] "The fair price for this option is: 0.895692687977585"
paste("Confidence interval for the estimation is: (",est[2], ",", est[3], ")")
## [1] "Confidence interval for the estimation is: ( 0.821176444513217 , 0.970208931441953 )"
results = rbind(results, est)
Comparing the methods
Results for arithmatic-average asian call option using different methods of MC are as follows:
rownames(results) = c("Naive MC", "Control variate MC", "Antithetic MC")
colnames(results) = c("Price estimation", "CI lower bound", "CI upper bound", "CI length")
results
## Price estimation CI lower bound CI upper bound CI length
## Naive MC 0.9053117 0.7916900 1.0189334 0.22724342
## Control variate MC 0.9473079 0.9443364 0.9502794 0.00594307
## Antithetic MC 0.8956927 0.8211764 0.9702089 0.14903249
library(dotwhisker)
## Loading required package: ggplot2
library(dplyr)
##
## Attaching package: 'dplyr'
## The following object is masked from 'package:Rfast':
##
## nth
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(broom)
NREP = 1000
term = c("Naive MC", "Control variate MC", "Antithetic MC")
estim = results[,1]
std = c(sd(price_nmc) / sqrt(NREP), sd(CV_prices) / sqrt(NREP), sd(anti_price) / sqrt(NREP))
std
## [1] 0.057971326 0.001516117 0.038019190
results_df = data.frame(term=term, estimate=estim, std.error=std)
dwplot(results_df)
It is observable that control variate method provides much smaller variance. However, it seems that the estimated price is a little bit biased.