Options and VaR with R

Author

Dr. Martín Lozano.

Modified

January 2, 2024, 23:54:33 pm.

Abstract
This material relies on Hull (2015). Some mathematical background is skipped to emphasize the data analysis, model logic, discussion, graphical approach and R coding (R Core Team 2024). As in the philosophy of Knuth (1984), the objective of this document is to explain to human beings what we want a computer to do as literate programming. This is a work in progress and it is under revision.

Back to Quantitative Finance with R Icon

martin-lozano-21818a22 mailto:mlozanoqf@gmail.com martin-lozano-21818a22 https://www.youtube.com/channel/drmartinlozano https://www.youtube.com/channel/drmartinlozano zoom


Introduction.

Option pricing and Value at Risk (VaR) play pivotal roles in risk management and quantitative finance, serving as indispensable tools for assessing and mitigating financial risks. Option pricing, exemplified by models like Black-Scholes, provides a systematic framework for valuing financial derivatives, enabling market participants to determine fair prices and make informed investment decisions. By understanding the potential future value of options, investors can hedge against adverse market movements and strategically allocate resources. On the other hand, VaR quantifies the potential loss that a portfolio may incur within a specified confidence interval and time horizon, offering a comprehensive measure of risk exposure. This metric assists financial institutions and investors in setting risk tolerance levels, optimizing portfolio diversification, and ensuring regulatory compliance. Together, option pricing and VaR contribute to the development of robust risk management strategies, enhancing the stability and resilience of financial markets in the face of uncertainties.

1 Payoff functions.

The payoff is the cash realized by the holder of an option or other asset at the end of its life.

1.1 European options.

It is often useful to characterize a European option in terms of its payoff to the purchaser of the option. The initial cost of the option is then not included in the calculation. The payoff functions for European options are the following.

  • Payoff from a long position in a European call option: \(c^{+} = \mathrm{max}(S_T-K, 0)\). Or equivalently: \(c^{+}=-\mathrm{min}(K-S_T, 0)\).
  • Payoff to the holder of a short position in the European call option: \(c^{-}=\mathrm{min}(K-S_T, 0)\). Or equivalently: \(c^{-}=-\mathrm{max}(S_T-K, 0)\).
  • Payoff to the holder of a long position in a European put option: \(p^{+}=\mathrm{max}(K-S_T, 0)\). Or equivalently: \(p^{+}=-\mathrm{min}(S_T-K, 0)\).
  • Payoff from a short position in a European put option: \(p^{-}=\mathrm{min}(S_T-K, 0)\). Or equivalently: \(p^{-}=-\mathrm{max}(K-S_T, 0)\).

Options are zero sum games because the payoffs to the holder or a long position and to the holder of a short position offset each other.

In the case of a call option:

\(c^{+}+c^{-}=\mathrm{max}(S_T-K, 0) + \mathrm{min}(K-S_T, 0)\),

\(\rightarrow c^{+}+c^{-}=\mathrm{max}(S_T-K, 0) - \mathrm{max}(S_T-K, 0)\),

\(\rightarrow c^{+}+c^{-}=0\).

In the case of a put option:

\(p^{+}+p^{-}=\mathrm{max}(K-S_T, 0) + \mathrm{min}(S_T-K, 0)\),

\(\rightarrow p^{+}+p^{-}=\mathrm{max}(K-S_T, 0) - \mathrm{max}(K-S_T, 0)\),

\(\rightarrow p^{+}+p^{-}=0\).

Code
ST.seq <- seq(from = 0, to = 150, length.out = 150)
K = 75
cl <- pmax(ST.seq - K, 0) # long call payoff.
pl <- pmax(K - ST.seq, 0) # long put payoff.
cs <- pmin(K - ST.seq, 0) # short call payoff.
ps <- pmin(ST.seq - K, 0) # short put payoff.

Graphically (assuming \(K=\$75\)):

Code
par(mfrow = c(2, 2), mai = c(0.7, 0.4, 0.4, 0.4))
par(pty = "s")
plot(ST.seq, cl, type = "l", ylim = c(0, 150), lwd = 4,
     ylab = "Payoff", xlab = "")
legend("topleft", legend = c("Long call"),
       bg = "white", bty = "n")
par(pty = "s")
plot(ST.seq, pl, type = "l", ylim = c(0, 150), lwd = 4,
     ylab = "", xlab = "")
legend("topleft", legend = c("Long put"),
       bg = "white", bty = "n")
par(pty = "s")
plot(ST.seq, cs, type = "l", ylim = c(-150, 0), lwd = 4,
     ylab = "Payoff", xlab = expression(paste(S[T])))
legend("bottomleft", legend = c("Short call"),
       bg = "white", bty = "n")
par(pty = "s")
plot(ST.seq, ps, type = "l", ylim = c(-150, 0), lwd = 4,
     ylab = "", xlab = expression(paste(S[T])))
legend("bottomleft", legend = c("Short put"),
       bg = "white", bty = "n")

Figure 1.1: Payoffs of European option positions.

1.2 Stocks.

The payoff functions for stocks are the following.

  • Long position in a stock: \(S_T-S_0\).

  • Short position in a stock: \(S_0-S_T\).

Graphically:

Code
S0 = 75
par(pty = "s")
par(mfrow = c(1, 2), oma = c(0, 0, 2, 0))
par(pty = "s")
# Long position
plot(ST.seq, ST.seq-S0, type = "l", ylab = "Payoff", main = "Long stock",
     xlab = expression(paste(S[T])), lwd = 4, col = "blue", 
     ylim = c(-75, 75))
abline(h = 0, lty = 2)
abline(v = 0, lty = 2)
par(pty = "s")
# Short position
plot(ST.seq, S0-ST.seq, type = "l", ylab = "", main = "Short stock",
     xlab = expression(paste(S[T])), lwd = 4, col = "red", 
     ylim = c(-75, 75))
abline(h = 0, lty = 2)
abline(v = 0, lty = 2)

Figure 1.2: Long and short stock payoffs.

1.3 Bonds.

The payoff functions for zero coupon bonds are the following.

  • Long position in a bond: \(K\).

  • Short position in a bond: \(-K\).

Graphically (assuming \(K=\$75\)):

Code
K = 75
par(pty = "s")
par(mfrow = c(1, 2), oma = c(0, 0, 2, 0))
par(pty = "s")
# Long position
plot(ST.seq, rep(K, 150), type = "l", ylab = "Payoff", main = "Long bond",
     xlab = expression(paste(S[T])), lwd = 4, col = "blue", 
     ylim = c(0, 150))
abline(h = 0, lty = 2)
abline(v = 0, lty = 2)
par(pty = "s")
# Short position
plot(ST.seq, rep(-K, 150), type = "l", ylab = "", main = "Short bond",
     xlab = expression(paste(S[T])), lwd = 4, col = "red", 
     ylim = c(-150, 0))
abline(h = 0, lty = 2)
abline(v = 0, lty = 2)

Figure 1.3: Long and short zero coupon bond payoffs.

1.4 Options, stocks and bonds.

We look at what can be achieved when an option is traded in conjunction with other assets. In particular, we examine the properties of portfolios consisting of (a) an option and a zero-coupon bond, (b) an option and the asset underlying the option, and (c) two or more options on the same asset. This section is based in Hull (2015) chapter 12, you can also see Brealey et al. (2020).

Code
S0 = 65
# Here I assume the stock price is currently 65.
Sl <- ST.seq - S0
# I assume the option price is 5.
cs.pr <- pmin(K - ST.seq, 0) + 5
# A short put profit is replicated by taking a short call and a long stock.
ans <- cs.pr + Sl
par(pty = "s")
plot(ST.seq, ans, type = "l", ylim = c(-75, 75), lwd = 2, col = "red",
     xlab = expression(paste(S[T])), 
     ylab = "Profit")
lines(ST.seq, Sl, lty = 2, lwd = 2, col = "orange")
lines(ST.seq, cs.pr, lty = 2, lwd = 2, col = "blue")
abline(v = 0)
abline(h = 0)
abline(v = 65, lty = 2, col = "grey")
abline(v = 75, lty = 2, col = "grey")
abline(v = 80, lty = 2, col = "grey")
legend("topleft", legend = c("Short call", "Long stock", "Short put"),
       col = c("blue", "orange", "red"), lty = c(2, 2, 1), 
       lwd = 2, bg = NULL)

Figure 1.4: Hull: Figure 12.1a, profit diagram. A synthetic short put.
Code
S0 = 65
c_price = 5
# Here I assume the stock price is currently 65.
Sl <- ST.seq - S0
# I assume the option price is 5.
cs.pr <- pmin(K - ST.seq, 0) + c_price
# A short put profit is replicated by taking a short call and a long stock.
ans <- cs.pr + Sl
par(pty = "s")
plot(ST.seq, ans, type = "l", ylim = c(-5, 15), xlim = c(60, 80),
     lwd = 4, col = "red",
     xlab = expression(paste(S[T])), 
     ylab = "Profit")
lines(ST.seq, Sl, lwd = 4, col = "orange")
lines(ST.seq, cs.pr, lwd = 4, col = "blue")
abline(v = 0)
abline(h = 0)
abline(v = 65, lty = 2, col = "grey")
abline(v = 75, lty = 2, col = "grey")
abline(v = 80, lty = 2, col = "grey")
points(S0 - c_price, 0, cex = 3, col = "red", lwd = 3)
points(K, K - S0 + c_price, cex = 3, col = "red", lwd = 3, pch = 0)
points(K + c_price, 0, cex = 3, col = "blue", lwd = 3)
points(K, c_price, cex = 3, col = "blue", lwd = 3, pch = 0)
points(S0, 0, cex = 3, col = "orange", lwd = 3)
points(K, K - S0, cex = 3, col = "orange", lwd = 3, pch = 0)
points(K, 0, cex = 3, lwd = 3)

Figure 1.5: Hull: Figure 12.1a, profit diagram (zoom). A synthetic short put.

We can identify at least three isosceles right triangles, they have two sides of equal measure. The short call profit is zero at \(S_T = \$80\), in the code above: K + c_price. The stock profit is zero at \(S_T=\$65\), in the coded above: S0. Finally, the short put profit is zero at \(S_T=\$60\), in the code above: S0 - c_price.

Now see how it looks if we plot a payoff diagram instead.

Code
S0 = K
Sl <- ST.seq - S0
cs.pr <- pmin(K - ST.seq, 0)
# A short put profit is replicated by taking a short call and a long stock.
ans <- cs.pr + Sl
par(pty = "s")
plot(ST.seq, ans, type = "l", ylim = c(-75, 75), lwd = 5, col = "red",
     xlab = expression(paste(S[T])), 
     ylab = "Payoff")
lines(ST.seq, Sl, lty = 2, lwd = 2, col = "orange")
lines(ST.seq, cs.pr, lty = 2, lwd = 2, col = "blue")
abline(v = 0, lty = 2)
abline(h = 0, lty = 2)
legend("topleft", legend = c("Short call", "Long stock", "Short put"),
       col = c("blue", "orange", "red"), lty = c(2, 2, 1), 
       lwd = 2, bg = "white")

Figure 1.6: Hull: Figure 12.1a, payoff diagram.
Code
library(knitr)
library(kableExtra)
library(plotly)
library(ggplot2)
library(tidyr)
library(dplyr)
library(vembedr)

2 Options properties.

Financial options are one of the most flexible, popular and versatile financial instruments. They are relevant by themselves and important as building blocks to create other financial products and financial strategies. In order to fully understand options we need to understand their properties first. This is, what are the determinant of financial options, how these determinants change the option prices, what are the limits of financial options, what happens when these limits are violated, methods for determining the theoretical option prices and the relationship between call and put options. This section is based in Hull (2015) chapter 11.

2.1 Determinants.

The main determinants of option prices are those parameters of the Black-Scholes formula:

  • The price of the underlying stock at time zero: \(S_0\).
  • The option strike price: \(K\).
  • The risk-free rate: \(rf\).
  • The option maturity: \(T\).
  • The volatility of the underlying stock \(\sigma\).

This is, \(BS = f(S_0, K, rf, T, \sigma)\). Here we set up the BS function and then evaluate it at \(BS = f(S_0=50, K=50, rf=0.05, T=1, \sigma=0.3)\).

Code
# Black-Scholes call.
c.bs <- function(S, K, rf, TT, sigma) {
  d1 <- (log(S / K) + (rf + sigma^2 / 2) * TT) / (sigma * sqrt(TT))
  d2 <- d1 - sigma * sqrt(TT)
  c <- S * pnorm(d1) - K * exp(-rf * TT) * pnorm(d2)
}
# Black-Scholes put.
p.bs <- function(S, K, rf, TT, sigma) {
  d1 <- (log(S / K) + (rf + sigma^2 / 2) * TT) / (sigma * sqrt(TT))
  d2 <- d1 - sigma * sqrt(TT)
  p <- K * exp(-rf * TT) * pnorm(-d2) - S * pnorm(-d1)
}
# Calculate and report results.
c <- c.bs(50, 50, 0.05, 1, 0.3)
p <- p.bs(50, 50, 0.05, 1, 0.3)
data.frame("Black-Scholes call" = c, "Black-Scholes put" = p)
  Black.Scholes.call Black.Scholes.put
1           7.115627          4.677099

Now, let’s see how Black-Scholes option prices changes (call and put) when we leave \(S_0\) as a variable and the rest of the parameters as fixed: \(BS = f(S_0, K=50, rf=0.05, T=1, \sigma=0.3)\).

Code
# Parameters.
K <- 50
rf <- 0.05
TT <- 1
sigma <- 0.3
S0 <- 50
# Sequence of S0.
S0.seq <- seq(from = 0, to = 100, length.out = 50)
# Evaluation of the c.bs and p.bs function.
c.S0 <- mapply(c.bs, S0.seq, K, rf, TT, sigma)
p.S0 <- mapply(p.bs, S0.seq, K, rf, TT, sigma)

We produce as many c.S0 as values for S0.seq. Then, we can plot.

Code
# See the results.
plot(S0.seq, c.S0, type = "l", lwd = 4, xlab = expression(paste(S[0])), 
     ylab = "Call option price, c")

Figure 2.1: Call price as a function of \(S_0\).

The call option price is an increasing function of \(S_0\). This makes sense because a right to buy the stock at a fix price in the future becomes more valuable as the current value of the stock increases. In other words, it is more expensive to lock the price of an expensive asset for a potential buyer.

See the case of the put option.

Code
plot(S0.seq, p.S0, type = "l", lwd = 4, xlab = expression(paste(S[0])), 
     ylab = "Put option price, p")

Figure 2.2: Put price as a function of \(S_0\).

Here, it is cheaper to lock the price of an expensive asset for a potential seller. This makes sense because it looks unnecessary to lock the selling price of an expensive stock.

Let’s put both plots together.

Code
# What is the interpretation of S0.star, and how did I found this value?
S0.star <- K * exp(-rf * TT)
c.S0.star <- mapply(c.bs, S0.star, K, rf, TT, sigma)
# Plot.
plot(S0.seq, c.S0, type = "l", lwd = 4, col = "blue", ylim = c(4, 8), 
     xlab = expression(paste(S[0])), ylab = "Option theoretical price", 
     xlim = c(40, 55))
lines(S0.seq, p.S0, lwd = 4, col = "red")
abline(v = S0.star, lty = 2)
abline(h = c.S0.star, lty = 2)
abline(h = c, lty = 2, col = "blue")
abline(h = p, lty = 2, col = "red")
abline(v = S0, lty = 2)
points(S0, c, pch = 19, col = "blue", cex = 3)
points(S0, p, pch = 19, col = "red", cex = 3)
points(S0.star, c.S0.star, pch = 19, cex = 3)
legend("topleft", legend = c("Call", "Put"),
       col = c("blue", "red"), lwd = 3, bg = "white")

Figure 2.3: Call and put prices as a function of \(S_0\) (zoom).

Note that there is a single case in which both options (call and put) are the same. It is interesting to find out the value of \(S_0\) that makes \(c=p\). Clearly, this value is lower than \(S_0=\$50\), the point here is to find the mathematical expression of \(S_0\) that makes \(c=p\).

2.2 Option bounds.

Option values (or prices) are subject or constrained to upper and lower bounds. This basically mean that in theory option prices cannot exceed upper bounds and cannot decrease below lower bounds. If they do, then arbitrage opportunities arise.

We can extend our analysis by incorporating the corresponding call and put option bounds.

Code
# Put lower bound.
lb.put <- pmax(K * exp(-rf * TT) - S0.seq, 0)
# Call lower bound.
lb.call <- pmax(S0.seq - K * exp(-rf * TT), 0)
# Again the S0.star. Where did this value comes from?
line.star <- S0.star * 2
# Plot.
par(pty = "s")
plot(S0.seq, c.S0, type = "l", lwd = 3, col = "blue", ylim = c(0, 100),
     xlab = expression(paste(S[0])), ylab = "Call (blue), put (red)")
lines(S0.seq, p.S0, lwd = 3, col = "red")
abline(v = S0.star, lty = 2) 
abline(v = 0, lty = 2)
abline(v = line.star, lty = 2, col = "green")
abline(h = line.star, lty = 2, col = "green")
# Put upper bound.
abline(h = K * exp(-rf * TT), lwd = 2, col = "red", lty = 2)
lines(S0.seq, lb.put, lwd = 2, col = "red", lty = 2)
# Call upper bound.
lines(S0.seq, S0.seq, lwd = 2, col = "blue", lty = 2) 
lines(S0.seq, lb.call, lwd = 2, col = "blue", lty = 2)

Figure 2.4: Option values as a function of \(S_0\) including option bounds.

Note that the stock price range of values is from zero to positive infinity. However, this is not the case for the options. The option values depend on these bounds. We can illustrate the same plot in a cleaner way, to make emphasis on a geometric approach.

Code
par(pty = "s")
plot(S0.seq, lb.put, type = "l", lwd = 2, col = "red",  ylim = c(0, 100), 
     xlab = expression(paste(S[0])), ylab = "Call (blue), put (red)")
abline(v = S0.star, lty = 2) 
abline(v = 0, lty = 2)
abline(v = line.star, lty = 2)
abline(h = line.star, lty = 2)
abline(h = S0.star, lwd = 2, col = "red")
lines(S0.seq, S0.seq, lwd = 2, col = "blue")
lines(S0.seq, lb.call, lwd = 2, col = "blue")
# Enumerate the areas.
text(S0.star / 2, (S0.star + (S0.star / 2)), "1")
text(60, 80, "2")
text(80, 60, "3")
text(10, 20, "4")
text(25, 35, "5")
text(25, 10, "6")
text(40, 20, "7")
text(60, 35, "8")
text(80, 20, "9")

Figure 2.5: Call and put option bounds: geometric inspection (or ‘the puzzle’).

Every number from 1 to 9 is assigned to a specific geometric shape in the plot above. This is, a square as in 1, and triangles of two different shapes in the rest of the cases (2 to 9). It is interesting to analyze whether each area represents a possible price range for call and/or put options.

Now, let’s see how Black-Scholes option prices changes (call and put) when we leave \(S_0\) as a variable, three levels of \(\sigma\) and the rest of the parameters as fixed: \(BS = f(S_0, K=50, rf=0.05, T=1, \sigma)\). We can show this in a 2 dimension plot, including the correspondent bounds.

Code
# Evaluate the function.
c.S0.s1 <- mapply(c.bs, S0.seq, K, rf, TT, sigma)
c.S0.s5 <- mapply(c.bs, S0.seq, K, rf, TT, sigma * 5)
c.S0.s15 <- mapply(c.bs, S0.seq, K, rf, TT, sigma * 15)
# Plot.
plot(S0.seq, c.S0.s1, type = "l", lwd = 3,  xlab = expression(paste(S[0])),
     ylab = "Call option price, c")
lines(S0.seq, c.S0.s5, lwd = 3, col = "blue") # call with higher sigma
lines(S0.seq, c.S0.s15, lwd = 3, col = "red") # call with even higher sigma
lines(S0.seq, lb.call, lwd = 3, lty = 2) # lower bound
lines(S0.seq, S0.seq, lwd = 3, lty = 2) # upper bound
legend("topleft",legend=expression(sigma == 0.3, sigma == 1.5,
                                   sigma == 4.5, "call bounds"), 
       text.width = 12, col = c("black", "blue", "red", "black"), lwd = 2,
       lty = c(1, 1, 1, 2), bg = "white", cex = 0.8)

Figure 2.6: Call option changes as \(S_0\) and \(\sigma\) increases: the role of bounds.

And the same for the put option.

Code
# Evaluate the function.
p.S0.s1 <- mapply(p.bs, S0.seq, K, rf, TT, sigma)
p.S0.s5 <- mapply(p.bs, S0.seq, K, rf, TT, sigma * 5)
p.S0.s15 <- mapply(p.bs, S0.seq, K, rf, TT, sigma * 15)
# Plot.
plot(S0.seq, p.S0.s1, type = "l", lwd = 3,  xlab = expression(paste(S[0])),
     ylab = "Put option price, p")
lines(S0.seq, p.S0.s5, lwd = 3, col = "blue") # put with higher sigma
lines(S0.seq, p.S0.s15, lwd = 3, col = "red") # put with even higher sigma
lines(S0.seq, lb.put, lwd = 3, lty = 2) # lower bound
lines(S0.seq, rep(S0.star, 50) , lwd = 3, lty = 2) # upper bound
legend("bottomleft", legend = expression(sigma == 0.3, sigma == 1.5,
                                   sigma == 4.5, "put bounds"), 
       text.width = 12, col = c("black", "blue", "red", "black"), lwd = 2,
       lty = c(1, 1, 1, 2), bg = "white", cex = 0.8)

Figure 2.7: Put option changes as \(S_0\) and \(\sigma\) increase: the role of bounds.
Code
# Evaluate the function.
p.S0.s1 <- mapply(p.bs, S0.seq, K, rf, TT, sigma)
p.S0.s5 <- mapply(p.bs, S0.seq, K, rf, TT, sigma * 5)
p.S0.s15 <- mapply(p.bs, S0.seq, K, rf, TT, sigma * 15)
# Plot.
plot(S0.seq, p.S0.s5, type = "l", lwd = 3,  xlab = expression(paste(S[0])),
     ylab = "Put option price, p", col = "blue",
     xlim = c(0, 1), ylim = c(47, 47.8))
lines(S0.seq, p.S0.s15, lwd = 3, col = "red") # put with even higher sigma
lines(S0.seq, lb.put, lwd = 1, lty = 2) # lower bound
lines(S0.seq, rep(S0.star, 50) , lwd = 1, lty = 2) # upper bound
legend("topleft", legend = expression(sigma == 1.5,
                                   sigma == 4.5, "put bounds"), 
       text.width = 12, col = c("blue", "red", "black"), lwd = 2,
       lty = c(1, 1, 2), bg = "transparent", cex = 0.8, bty = "n")

Figure 2.8: Put option changes as \(S_0\) and \(\sigma\) increase: the role of bounds (zoom).

Then, regardless of the extreme value of parameters, option bounds represent the maximum and minimum values for the call and put options. These option prices are theoretical as they are calculated by implementing the Black-Scholes formula. We normally evaluate whether the option market prices violates these bounds because this will flag an opportunity to implement an arbitrage strategy to generate a risk-free profit.

A different way to illustrate the option properties is by looking at a plane. Here, we let \(S_0\) and \(K\) as a free variables and remain the rest as fixed: \(BS = f(S_0, K, rf=0.05, T=1, \sigma = 0.3)\).

Code
# K as a variable.
K.seq <- S0.seq
# Create the empty matrix.
c.S0.s <- matrix(0, nrow = 50, ncol = 50)
# Fill the empty matrix.
for(i in 1:50){ # Is there an easier way to do this?
  for(j in 1:50){
    c.S0.s[i, j] <- c.bs(S0.seq[i], K.seq[j], rf, TT, sigma) } }
# Plot.
c.S0.s.plot <- persp(S0.seq, K.seq, c.S0.s, zlab = "Call", 
                     xlab = "S_0", 
      ylab = "K", theta = -60, phi = 10, expand =  0.5, col = "orange", 
      shade = 0.2, ticktype = "detailed") 
points(trans3d(S0, K, c, c.S0.s.plot), cex = 2, pch = 19, col = "blue")

Figure 2.9: Call value as \(S_0\) and \(K\) change, a plane view.

An alternative view is by showing a contour plot. A contour plot is a graphical technique for representing a 3-dimensional surface by plotting constant \(z\) slices (option prices), called contours, on a 2-dimensional format. That is, given a value for \(z\), lines are drawn for connecting the \((x, y)\) coordinates (\(S_0\), \(K\)) where that \(z\) value occurs.

Code
contour(S0.seq, K.seq, c.S0.s, xlab = expression(paste(S[0])), 
        ylab = "K", lwd = 2, nlevels = 20)
points(S0, K, pch = 19, col = "blue", cex = 2)

Figure 2.10: Call value as \(S_0\) and \(K\) change, a contour view.

Note that the blue circle \(S_0=\$50, K=\$50\) is between the contour line 5 and 10, this makes sense because the call option at \(call(S_0=50, K=50, rf=0.05, T=1, \sigma = 0.3) = \$7.115627\).

Code
plot_ly(type = "surface" , x = S0.seq, y = K.seq , z = c.S0.s ) %>%
layout(scene = list(xaxis = list(title = "Stock price"), 
                    yaxis = list(title = "K", 
                    zaxis = list(title = "Call price"))))  %>%
hide_colorbar()
Figure 2.11: Call value as \(S_0\) and \(K\) change, an interactive view.

An interesting case is the value of the put as a function of the time to maturity. Let’s see the simplest case.

Code
TT.seq <- seq(from = 1, to = 25, length.out = 50)
p.TT <- mapply(p.bs, S0, K, rf, TT.seq, sigma)
plot(TT.seq, p.TT, type = "l", lwd = 4, ylab = "Put value", xlab = "T")
points(TT, p, col = "red", cex = 2, pch = 19)

Figure 2.12: European put as \(T\) increases: the mysterious hump.

The put option price first increases as the time to maturity increases and after reaching a maximum, the put option price decreases. What is the mathematical expression for the time to maturity which makes the put option price maximum? What is the reason or logic behind this mysterious hump? Those are interesting questions to address.

Code
p1.bs <- function(S, K, rf, TT, sigma) {
  d1 <- (log(S / K) + (rf + sigma^2 / 2) * TT) / (sigma * sqrt(TT))
  d2 <- d1 - sigma * sqrt(TT)
  p1 <- K * exp(-rf * TT) * pnorm(-d2)
}

p2.bs <- function(S, K, rf, TT, sigma) {
  d1 <- (log(S / K) + (rf + sigma^2 / 2) * TT) / (sigma * sqrt(TT))
  d2 <- d1 - sigma * sqrt(TT)
  p2 <- S * pnorm(-d1)
}

TT.seq <- seq(from = 1, to = 25, length.out = 50)
p1.TT <- mapply(p1.bs, S0, K, rf, TT.seq, sigma)
p2.TT <- mapply(p2.bs, S0, K, rf, TT.seq, sigma)
plot(TT.seq, p.TT, type = "l", lwd = 4, ylab = "Put value", xlab = "T",
     ylim = c(-20, 25))
lines(TT.seq, p1.TT, col = "blue")
lines(TT.seq, p2.TT, col = "red")
points(TT, p, col = "red", cex = 2, pch = 19)

Let’s illustrate the case when time to maturity and the stock price at time zero changes.

Code
p.S0.T <- matrix(0, nrow = 50, ncol = 50)
for(i in 1:50) { # Is there an easier way to do this?
  for(j in 1:50) {
    p.S0.T[i, j] <- p.bs(S0.seq[i], K, rf, TT.seq[j], sigma) } }
p.S0.T.plot <- persp(S0.seq, TT.seq, p.S0.T, zlab = "Put", xlab = "S0", 
               ylab = "T", theta = 330, phi = 10, expand =  0.5, 
               col = "orange", shade = 0.2, ticktype = "detailed") 
points(trans3d(S0, TT, p, p.S0.T.plot), cex = 2, pch = 19, col = "blue")

Figure 2.13: European put value as \(S_0\) and \(T\) change: a plane.

That is not quite clear. So, here is the contour view.

Code
contour(S0.seq, TT.seq, p.S0.T, xlab = expression(paste(S[0])), 
        ylab = "T", lwd = 2, nlevels = 40, drawlabels = TRUE)
points(S0, TT, pch = 19, col = "blue", cex = 2)

Figure 2.14: Put value as \(S_0\) and \(T\) change, a contour view, or the hypnotic plot.

Now the mysterious hump is clearer than before. Remember the value of \(put(S_0=50, K=50, rf=0.05, T=1, \sigma = 0.3) = \$4.677099\).

Code
plot_ly(type = "surface" , x = S0.seq, y = TT.seq , z = p.S0.T ) %>%
layout(scene = list(xaxis = list(title = "Stock price"), 
                    yaxis = list(title = "T", 
                    zaxis = list(title = "Put price"))))  %>%
hide_colorbar()
Figure 2.15: Put value as \(S_0\) and \(T\) change, an interactive view.

2.3 Put-call parity.

The main idea behind the put-call parity is to understand how call and put option prices are related as today, at \(t=0\). In Hull, the procedure to derive the put-call parity starts with the definition of two portfolios: (1) a call and a bond; (2) a put and a stock. Then, derive the corresponding payoff of each portfolio at maturity. Doing this is relatively easy because we do not need a valuation method as we know the payoff functions for options, bonds and stocks. Given that we can demonstrate that the value of two different portfolios are the same at \(T\), then we can conclude that these two portfolios are worth the same at \(t=0\) as well.

The put-call parity is: \(c+Ke^{-rT}=p+S_0\).

We can verify that this equation holds.

\(p=c+Ke^{-rT}-S_0 \rightarrow p=7.115627+50e^{-0.05\times 1}- 50 \rightarrow p=\$4.677098\).

\(c=p+S_0-Ke^{-rT} \rightarrow c=4.677098+50-50e^{-0.05\times 1} \rightarrow c=\$7.115627\).

Let’s take one step back and demonstrate that both portfolios are worth the same at maturity. We first define the assets payoffs.

Code
ST.seq <- seq(from = 0, to = 150, length.out = 50)
cT <- pmax(ST.seq - K, 0) # call payoff.
pT <- pmax(K - ST.seq, 0) # put payoff.
BT <- rep(K, 50) # bond payoff (at maturity).
pc <- pmax(ST.seq, K) # this will be important.

Then we plot.

Code
par(pty = "s")
par(mfrow = c(1, 2), oma = c(0, 0, 2, 0))
par(pty = "s")
# Portfolio A.
plot(ST.seq, cT, type = "l", ylab = "Payoff", 
     xlab = expression(paste(S[T])), lwd = 2, 
     lty = 2, col = "blue", ylim = c(0, 150))
lines(ST.seq, (cT + BT), lwd = 6)
lines(ST.seq, BT, lwd = 2, lty = 2, col = "red")
legend("topleft", legend = c("Call option", "Zero coupon bond", 
                           "Total (Portfolio A)"),
       col = c("blue", "red", "black"), lwd = c(2, 2, 2), lty = c(2, 2, 1), 
       bg = "white", cex = 0.7)
par(pty = "s")
# Portfolio C.
plot(ST.seq, (pT + ST.seq), lwd = 6, type = "l", ylab = "", 
     xlab = expression(paste(S[T])), ylim = c(0, 150))
lines(ST.seq, ST.seq, lwd = 2, lty = 2, col = "orange")
lines(ST.seq, pT, lwd = 2, lty = 2, col = "purple")
legend("topleft", legend = c("Put option", "Share", "Total (Portfolio C)"),
       col = c("purple", "orange", "black"), lwd = 2, 
       lty = c(2, 2, 1), bg = "white", cex = 0.7)

Figure 2.16: Portfolio A and C payoffs, the put-call parity.

The point here is that the black line (total) is the same in both cases. This is why we argue that the payoffs of both portfolios are worth the same. If this is so, then they are worth the same in time \(t=0\).

In sum, this is the payoff of portfolios A and C: \(\mathrm{max}(S_T,K)\). This is already stored in pc.

Code
par(pty = "s")
plot(ST.seq, pc, type = "l", ylab = "Payoff", 
     xlab = expression(paste(S[T])), lwd = 5, ylim = c(0, 150))

Figure 2.17: Portfolio A and C payoffs, \(\mathrm{max}(S_T,K)\).

Nice. We can manipulate the put-call parity to create a synthetic stock \(S_0\). This is:

\(c+Ke^{-rT}=p+S_0 \rightarrow S_0 = c+Ke^{-rT}-p\).

Graphically:

Code
par(pty = "s")
plot(ST.seq, (cT + BT - pT), type = "l", ylab = "Payoff", 
     xlab = expression(paste(S[T])), 
     lwd = 5, ylim = c(-50, 100), xlim = c(0, 150), col = "orange")
lines(ST.seq, cT, lwd = 2, lty = 2, col = "blue") # positive call.
lines(ST.seq, BT, lwd = 2, lty = 2, col = "red") # positive bond.
lines(ST.seq, -pT, lwd = 2, lty = 2, col = "purple") # negative put.
legend("bottomright", legend = c("Call option", "Zero coupon bond", 
                               "Put option", "Total (stock)"),
       col = c("blue", "red", "purple", "orange"), lwd = 2, 
       lty = c(2, 2, 2, 1), bg = "white", cex = 0.8)

Figure 2.18: A synthetic stock \(S_0\).

So, we created a stock that did not exist with a call, a bond and a put. We can manipulate the put-call parity to create a synthetic bond \(Ke^{-rT}\). This is:

\(c+Ke^{-rT}=p+S_0 \rightarrow Ke^{-rT} = p+S_0-c\).

Graphically:

Code
par(pty = "s")
plot(ST.seq, (-cT + ST.seq + pT), type = "l", ylab = "Payoff", 
     xlab = expression(paste(S[T])), 
     lwd = 5, ylim = c(-50, 100), xlim = c(0, 150), col = "orange")
lines(ST.seq, -cT, lwd = 2, lty = 2, col = "blue") 
lines(ST.seq, ST.seq, lwd = 2, lty = 2, col = "red") 
lines(ST.seq, pT, lwd = 2, lty = 2, col = "purple") 
legend("bottomleft", legend = c("Call option (short)", "Stock", 
                               "Put option", "Total (bond)"),
       col = c("blue", "red", "purple", "orange"), lwd = 2, 
       lty = c(2, 2, 2, 1), bg = "white", cex = 0.7)

Figure 2.19: A synthetic bond \(Ke^{-rT}\).

There is an alternative to the Black-Scholes formula we indirectly review before. Next section introduces the binomial trees.

3 Binomial trees.

Binomial trees are a flexible valuation method for options because we can use them to value not only European but also American options. This section is based in Hull (2015) chapter 13.

3.1 Implementation.

The function is the following.

Code
bin <- function(S0, K, sigma, TM, r, steps) {
# the parameters
dt <- TM / steps
u <- exp(sigma * sqrt(dt))
d <- exp(-sigma * sqrt(dt))
a <- exp(r * dt)
p <- (a - d) / (u - d)
S <- matrix(0, steps + 1, steps)
pam <- S
peu <- S
cam <- S
ceu <- S
# the stock price process
  for(i in 1:steps) {
    j <- i + 1
    do <- seq(0, i)
    up <- rev(do)
    S[(1:j), i] <- S0 * (u ^ up) * (d ^ do) }
# the option prices at maturity.
peu[(1:(steps + 1)), steps] <- pmax(K - S[, i], 0)
pam[(1:(steps + 1)), steps] <- pmax(K - S[, i], 0)
ceu[(1:(steps + 1)), steps] <- pmax(S[, i] - K, 0)
cam[(1:(steps + 1)), steps] <- pmax(S[, i] - K, 0)
# the binomial method to price stock options.
for(j in steps:1) { # this is a reverse loop from steps to 1.
  cd <- (seq(steps:1)) # every round we compute less option prices.
  for(i in 1:cd[j]) { # option prices per step.
    peu[i, (j - 1)] <- exp(-r * dt) * (p * peu[i, j] + 
                                       (1 - p) * peu[(i + 1), j])
    ceu[i, (j - 1)] <- exp(-r * dt) * (p * ceu[i, j] + 
                                       (1 - p) * ceu[(i + 1), j])
    pam[i, (j - 1)] <- max((K - S[i, (j - 1)]), exp(-r * dt) * 
                           (p * pam[i, j] + (1 - p) * pam[(i + 1), j]))
    cam[i, (j - 1)] <- max((S[i, (j - 1)] - K), exp(-r * dt) * 
                           (p * cam[i, j] + (1 - p) * cam[(i + 1), j])) } }
# This is the final step in the binomial tree.
p.eu <- exp(-r * dt) * (p * peu[1, 1] + (1 - p) * peu[2, 1])
c.eu <- exp(-r * dt) * (p * ceu[1, 1] + (1 - p) * ceu[2, 1])
p.am <- exp(-r * dt) * (p * pam[1, 1] + (1 - p) * pam[2, 1])
c.am <- exp(-r * dt) * (p * cam[1, 1] + (1 - p) * cam[2, 1])
# Results.
option <- data.frame(c.eu, p.eu, c.am, p.am)
option
}

We can evaluate the function above to see how the price change depending on the number of steps in the binomial tree. Let’s evaluate the bin() function assuming \(S_0=\$50\), \(K=\$50\), \(rf=0.05\), \(T=1\), and \(\sigma=0.3\) at different time steps.

Code
b1t <- bin(S0, K, sigma, TT, rf, 1)
b4t <- bin(S0, K, sigma, TT, rf, 4)
b20t <- bin(S0, K, sigma, TT, rf, 20)
b50t <- bin(S0, K, sigma, TT, rf, 50)
b200t <- bin(S0, K, sigma, TT, rf, 200)
b500t <- bin(S0, K, sigma, TT, rf, 500)

See the results.

Code
Black.Scholes <- data.frame(c.eu = c, p.eu = p, c.am = NA, p.am = NA)
bin.bs <- rbind("Binomial (1 step)" = b1t, "Binomial (4 steps)" = b4t,
                "Binomial (20 steps)" = b20t, "Binomial (50 steps)" = b50t,
                "Binomial (200 steps)" = b200t, 
                "Binomial (500 steps)" = b500t, 
                "Black-Scholes" = Black.Scholes)
bin.bs
                         c.eu     p.eu     c.am     p.am
Binomial (1 step)    8.481986 6.043457 8.481986 6.043457
Binomial (4 steps)   6.762001 4.323472 6.762001 4.767526
Binomial (20 steps)  7.042462 4.603934 7.042462 4.898985
Binomial (50 steps)  7.086241 4.647713 7.086241 4.921038
Binomial (200 steps) 7.108267 4.669738 7.108267 4.931581
Binomial (500 steps) 7.112682 4.674153 7.112682 4.933664
Black-Scholes        7.115627 4.677099       NA       NA

In the extreme, as the number of steps increases, the binomial method converges to the Black-Scholes method. Let’s explore these differences.

Code
b1 <- mapply(bin, S0.seq, K, sigma, TT, rf, 1)
b2 <- mapply(bin, S0.seq, K, sigma, TT, rf, 2)
b3 <- mapply(bin, S0.seq, K, sigma, TT, rf, 3)
b4 <- mapply(bin, S0.seq, K, sigma, TT, rf, 4)
b20 <- mapply(bin, S0.seq, K, sigma, TT, rf, 20)

Here, we compare the value of the binomial method and the Black-Scholes at different time steps as a function of \(S_0\).

Code
plot(S0.seq, b1[1, ], type = "l", col = "red", lwd = 5, 
     xlab = expression(paste(S[0])), ylab = "European call")
lines(S0.seq, c.S0.s1, col = "black", lwd = 5)
legend("topleft", legend = c("1 step binomial", "Black-Scholes"),
       col = c("red", "black"), lwd = 2, bg = "white")

Figure 3.1: One-step binomial and Black-Scholes comparison.

There are some differences between the binomial and the Black-Scholes. Let’s zoom to see the differences clearer.

Code
plot(S0.seq, b1[1,], type = "l", ylim = c(0, 4), xlim = c(30, 45), 
     col = "green", lwd = 2, xlab = expression(paste(S[0])), 
     ylab = "European call")
lines(S0.seq, b2[1,], col = "purple", lwd = 2)
lines(S0.seq, b3[1,], col = "orange", lwd = 2)
lines(S0.seq, b4[1,], col = "red", lwd = 2)
lines(S0.seq, b20[1,], col = "black", lwd = 2)
lines(S0.seq, c.S0.s1, col = "black", lwd = 2)
legend("topleft", legend = c("1 step binomial", "2 steps binomial", 
                             "3 steps binomial", "4 steps binomial",
                             "20 steps binomial", "Black-Scholes"),
       col = c("green", "purple", "orange", "red", "black", "black"), 
       lwd = 2, bg = "white")

Figure 3.2: Binomial and Black-Scholes convergence, a zoom view.

Not clear yet. Let’s see a panel view.

Code
par(mfrow = c(2, 2), mai = c(0.7, 0.4, 0.4, 0.4))
par(pty = "s")
plot(S0.seq, c.S0.s1, type = "l", ylim = c(0, 4), xlim = c(30, 45), 
     col = "black", lwd = 6, xlab = "", ylab = "European call")
lines(S0.seq, b2[1,], col = "purple", lwd = 2)
legend("topleft", legend = c("2-steps"),
       bg = "white", bty = "n")
par(pty = "s")
plot(S0.seq, c.S0.s1, type = "l", ylim = c(0, 4), xlim = c(30, 45), 
     col = "black", lwd = 6, xlab = "", ylab = "")
lines(S0.seq, b3[1,], col = "orange", lwd = 2)
legend("topleft", legend = c("3-steps"),
       bg = "white", bty = "n")
par(pty = "s")
plot(S0.seq, c.S0.s1, type = "l", ylim = c(0, 4), xlim = c(30, 45), 
     col = "black", lwd = 6, xlab = expression(paste(S[0])), 
     ylab = "European call")
lines(S0.seq, b4[1,], col = "red", lwd = 2)
legend("topleft", legend = c("4-steps"), bg = "white", bty = "n")
par(pty = "s")
plot(S0.seq, c.S0.s1, type = "l", ylim = c(0, 4), xlim = c(30, 45), 
     col = "black", lwd = 6, xlab = expression(paste(S[0])), ylab = "")
lines(S0.seq, b20[1,], col = "grey", lwd = 2)
legend("topleft", legend = c("20-steps"),
       bg = "white", bty = "n")

Figure 3.3: Binomial and Black-Scholes convergence, a panel view.

As stated above, the binomial method converges to the Black-Scholes as time steps increase. We can also create a function to visualize the price path of the stock from \(S_0\) to \(S_T\) given the assumptions of the binomial method.

3.2 Stock price paths.

In order to value option prices, we first need to understand the evolution of the underlying (in this case the stock price) from time zero to maturity, this is from \(S_0\) to \(S_T\). The binomial method assumes that the price can increase or decrease with a certain probability in each time step. Here, we can show how the binomial tree method assumes this stock price evolution.

Let’s assume 10-steps, from \(t=0.1, t=0.2,..., t=1\)

Code
# Function to generate stock prices paths given the binomial method.
S.paths <- function(S0, sigma, TM, steps) {
  dt <- TM / steps
  u <- exp(sigma * dt^0.5) # Here we set u and d as a function of sigma.
  d <- exp(-sigma * dt^0.5)
  S <- matrix(0, (steps + 1), (steps + 1))
  S[1, 1] <- S0
  for(i in 2:(steps + 1)) {
    do = seq(0, i - 1)
    up = rev(do) # rev provides a reversed version of its argument. 
    S[(1:i), i] = S0 * (u ^ up) * (d ^ do) }
  S }
# Evaluate the function.
Spaths <- S.paths(50, 0.3, 1, 10)
# A table.
colnames(Spaths) <- c(0, cumsum(rep(1/10, 10)))
round(Spaths, 2)
       0   0.1   0.2   0.3   0.4   0.5   0.6   0.7    0.8    0.9      1
 [1,] 50 54.98 60.45 66.46 73.08 80.35 88.34 97.13 106.80 117.43 129.12
 [2,]  0 45.47 50.00 54.98 60.45 66.46 73.08 80.35  88.34  97.13 106.80
 [3,]  0  0.00 41.36 45.47 50.00 54.98 60.45 66.46  73.08  80.35  88.34
 [4,]  0  0.00  0.00 37.62 41.36 45.47 50.00 54.98  60.45  66.46  73.08
 [5,]  0  0.00  0.00  0.00 34.21 37.62 41.36 45.47  50.00  54.98  60.45
 [6,]  0  0.00  0.00  0.00  0.00 31.11 34.21 37.62  41.36  45.47  50.00
 [7,]  0  0.00  0.00  0.00  0.00  0.00 28.30 31.11  34.21  37.62  41.36
 [8,]  0  0.00  0.00  0.00  0.00  0.00  0.00 25.74  28.30  31.11  34.21
 [9,]  0  0.00  0.00  0.00  0.00  0.00  0.00  0.00  23.41  25.74  28.30
[10,]  0  0.00  0.00  0.00  0.00  0.00  0.00  0.00   0.00  21.29  23.41
[11,]  0  0.00  0.00  0.00  0.00  0.00  0.00  0.00   0.00   0.00  19.36

This does not look like a typical binomial tree. In fact, it is not very clear whether a given price corresponds to an increase or decrease from a previous time step. We can make a few arrangements to visualize this as a tree.

Code
S.paths <- function(S0, sigma, TM, steps) {
  dt <- TM / steps
  u <- exp(sigma * dt^0.5)
  d <- exp(-sigma * dt^0.5)
  S <- matrix(0, 2 * ((steps + 1)), (steps + 1))
  S2 <- matrix(NA, ((2 * steps) + 2), (steps + 1))
  S2[(steps + 1), 1] <- S0
  for(i in 2:(steps + 1)) {
    do = seq(0, i - 1)
    up = rev(do) # rev provides a reversed version of its argument. 
    S[(1:i), i] = S0 * (u ^ up) * (d ^ do)
    x = rep(NA, i) # These are the NA between stock prices.
    r = rev(c(seq(0, (steps - 1)), 0)) # These creates the blank spaces.
    # Here we combine NA and stock prices for each column.
    S2[(1 + r[i]):((2 * i) + r[i]), i] = as.numeric(rbind(S[(1:i), i], x))
    }
  S2 }
# Evaluate the function.
Spaths <- S.paths(50, 0.3, 1, 10)
# A table.
colnames(Spaths) <- round(c(0, cumsum(rep(1/10, 10))), 2)
round(Spaths, 2)
       0   0.1   0.2   0.3   0.4   0.5   0.6   0.7    0.8    0.9      1
 [1,] NA    NA    NA    NA    NA    NA    NA    NA     NA     NA 129.12
 [2,] NA    NA    NA    NA    NA    NA    NA    NA     NA 117.43     NA
 [3,] NA    NA    NA    NA    NA    NA    NA    NA 106.80     NA 106.80
 [4,] NA    NA    NA    NA    NA    NA    NA 97.13     NA  97.13     NA
 [5,] NA    NA    NA    NA    NA    NA 88.34    NA  88.34     NA  88.34
 [6,] NA    NA    NA    NA    NA 80.35    NA 80.35     NA  80.35     NA
 [7,] NA    NA    NA    NA 73.08    NA 73.08    NA  73.08     NA  73.08
 [8,] NA    NA    NA 66.46    NA 66.46    NA 66.46     NA  66.46     NA
 [9,] NA    NA 60.45    NA 60.45    NA 60.45    NA  60.45     NA  60.45
[10,] NA 54.98    NA 54.98    NA 54.98    NA 54.98     NA  54.98     NA
[11,] 50    NA 50.00    NA 50.00    NA 50.00    NA  50.00     NA  50.00
[12,] NA 45.47    NA 45.47    NA 45.47    NA 45.47     NA  45.47     NA
[13,] NA    NA 41.36    NA 41.36    NA 41.36    NA  41.36     NA  41.36
[14,] NA    NA    NA 37.62    NA 37.62    NA 37.62     NA  37.62     NA
[15,] NA    NA    NA    NA 34.21    NA 34.21    NA  34.21     NA  34.21
[16,] NA    NA    NA    NA    NA 31.11    NA 31.11     NA  31.11     NA
[17,] NA    NA    NA    NA    NA    NA 28.30    NA  28.30     NA  28.30
[18,] NA    NA    NA    NA    NA    NA    NA 25.74     NA  25.74     NA
[19,] NA    NA    NA    NA    NA    NA    NA    NA  23.41     NA  23.41
[20,] NA    NA    NA    NA    NA    NA    NA    NA     NA  21.29     NA
[21,] NA    NA    NA    NA    NA    NA    NA    NA     NA     NA  19.36
[22,] NA    NA    NA    NA    NA    NA    NA    NA     NA     NA     NA

Graphically:

Code
nona <- ! is.na(Spaths)
bin_paths <- data.frame(st = rep(1:11, 1:11)/10-0.1, pa = Spaths[nona])
plot(bin_paths$st, bin_paths$pa, pch = 19, ylab = expression(paste(S[t])),
     xlab = "t")

Figure 3.4: \(S_t\) binomial paths from \(S_0\) to \(S_T\).

Nice. Evaluate the function for a single 2-step case: \(S_0=\$50\), \(K=\$52\), \(\sigma = 0.3\), \(T=2\), \(rf=0.05\).

Code
# bin <- function(S0, K, sigma, TM, r, steps) {
b2 <- mapply(bin, 50, 52, 0.3, 2, 0.05, 2)
b2
     [,1]    
c.eu 9.194163
p.eu 6.245708
c.am 9.194163
p.am 7.428402

There is no difference between American and European call options theoretical prices as there are no incentives to exercise American call options early. The case of put options is different as incentives to exercise American put options early may arise. As a consequence, American put options are in general more expensive than European put options.

Let’s evaluate this difference in a 50-step case: \(K=\$52\), \(\sigma = 0.3\), \(T=2\), \(rf=0.05\), and different values of \(S_0\).

Code
S.seq <- seq(0, 80, 0.5)
AEoptions <- mapply(bin, S.seq, 52, 0.3, 1, 0.05, 50)
plot(S.seq, unlist(AEoptions[4,]) - unlist(AEoptions[2,]), 
     type = "l", lwd = 5, main = "The value of being American.",
     xlab = expression(paste(S[0])),
     ylab = "American minus European put")
abline(v = 52, lty = 2)
abline(h = 0, lty = 2)

Figure 3.5: The value of being American; or the cost of being European.

3.3 Real vs. risk-neutral world.

This section is based on Hull (2015) section 13.1. A stock price is currently $20, and it is known that at the end of 3 months it will be either $22 or $18. We are interested in valuing a European call option to buy the stock for $21 in 3 months. The risk-neutral probability \(p\) for the case of \(rf=0.12\), \(T=3/12\), \(u=1.1\) and \(d=0.9\):

\(p = \frac{e^{rT}-d}{u-d} \rightarrow p = \frac{e^{0.12\times(3/12)}-0.9}{1.1-0.9} \rightarrow p = \$0.6522727\).

Code
# This is the substitution of equation 13.3.
rnp <- (exp(0.12*(3/12))-0.9)/(1.1-0.9)
rnp
[1] 0.6522727

Following Hull (2015), this option will have one of two values at the end of the 3 months. If the stock price turns out to be $22, the value of the option will be $1; if the stock price turns out to be $18, the value of the option will be zero. This is the European call option expected value at time \(T\):

\([p\times(\$1)] + [(1-p)\times \$0]\)

\(\rightarrow [0.6522727\times(\$1)] + [(1-0.6522727)\times \$0] = \$0.6522727\).

Code
option_T <- rnp * 1 + (1 - rnp) * 0
option_T
[1] 0.6522727

We can calculate the European call option value at \(t=0\) according to the risk-neutral valuation. This option value \(f\) is valid in the real world, not only in the risk-neutral world.

\(f = \$0.6522727 \times e^{-rT} \rightarrow f = \$0.6522727 \times e^{-0.12\times3/12} \rightarrow f = \$0.6329951\).

Code
option_0 <- option_T * exp(-0.12*(3/12))
option_0
[1] 0.6329951

Suppose that, in the real world, we know the expected return on the stock is 16%. This is easily calculated as we have historical information of stock returns. It is also a reasonable assumption as this is higher than the risk-free rate. Then, the probability of an up movement in the real world \(p^*\) is:

\(p^* = \frac{e^{rT}-d}{u-d} \rightarrow p^* = \frac{e^{0.16\times(3/12)}-0.9}{1.1-0.9} \rightarrow p^* = 0.7040539\).

Code
# Real world probability.
p.real <- (exp(0.16*(3/12))-0.9)/(1.1-0.9)
p.real
[1] 0.7040539

This is the option expected value in the real world at time \(T\):

\([p^*\times(\$1)] + [(1-p^*)\times \$0]\)

\(\rightarrow 0.7040539\times(\$1) + (1-0.7040539)\times \$0 = \$0.7040539\).

Code
option_T_real <- p.real*1 + (1-p.real)*0
option_T_real
[1] 0.7040539

Can we calculate the present value of $0.7040539? We only need the option discount factor in the real world. The problem is that we do not know this rate, it should be higher than the risk-free rate, even higher than the the expected return on the stock. It is not easy to know the correct discount rate of the option to apply to the expected payoff in the real world. Using risk-neutral valuation solves this problem because we know that in a risk-neutral world the expected return on all assets (and therefore the discount rate to use for all expected payoffs) is the risk-free rate.

What can we do to find out the option discount factor? Since we know the correct value of the option is \(f=\$0.6329951\) in both real and risk-neutral world, we can deduce the real-world discount rate \(r\) of this European call option.

We know this is true: \(\$0.6329951 = \$0.704053e^{-r\times3/12}\).

Then, solve for \(r\): \(\rightarrow \frac{\$0.6329951}{\$0.704053} = e^{-r\times3/12}\),

\(\rightarrow log{\frac{\$0.6329951}{\$0.704053}} = -r\times3/12\),

\(\rightarrow r = -log{\frac{\$0.6329951}{\$0.704053}}\times12/3\),

\(\rightarrow r=0.4255688\).

Code
option.df <- -log(option_0/option_T_real)*(12/3)
option.df
[1] 0.4255688

The implied real-world discount rate for the option is 42.55688%. Let’s put everything in a function.

Code
option.df.fun <- function(r) {
  p.real <- (exp(r*(3/12))-0.9)/(1.1-0.9)
  option_T_real <- p.real*1 + (1-p.real)*0
  option.df <- -log(option_0/option_T_real)*(12/3)
option.df
}

Evaluate for known values:

Code
option.df.fun(0.12)
[1] 0.12
Code
option.df.fun(0.16)
[1] 0.4255688

Everything looks correct.

Code
# Evaluate the function in a range of values.
r.seq <- seq(0.11, 0.17, 0.001)
df <- mapply(option.df.fun, r.seq)
plot(r.seq, df, type = "l", lwd = 5, 
     xlab = "Expected return of the stock (the underlying)",
     ylab = "Expected return of the option")
points(0.12, 0.12, pch = 19, col = "blue", cex = 3)
points(0.16, 0.4255688, pch = 19, col = "red", cex = 3)
abline(v = 0.12, lty = 2, col = "blue", lwd = 2)
abline(h = option.df.fun(0.12), lty = 2, col = "blue", lwd = 2)
abline(v = 0.16, lty = 2, col = "red", lwd = 2)
abline(h = option.df.fun(0.16), lty = 2, col = "red", lwd = 2)

Figure 3.6: Red pill, blue pill.

As in the movie, The Matrix, the blue pill describes living life without knowing its meaning or running away from the truth in order to stay as is. This is equivalent to the risk-neutral world. The red pill is described as the solution for knowing the real truth in life. This is equivalent to the real-world.

3.4 Parrondo’s paradox.

The Parrondo’s paradox is controversial in the literature, we are not going to elaborate on that. However, it is indeed interesting as it looks counterintuitive: Can we combine two losing investments into a winner?

This video was created by Professor Humberto Barreto from DePauw University, Indiana, based on a now-defunct app from Alexander Bogomolny of the well-known maths site Cut the Knot. It illustrates the general idea of Parrondo’s paradox.

Code
embed_url("https://youtu.be/cEuyfD2qVgQ")

Consider asset A which has a known 6% gross return. In the context of a binomial tree: \([p\times(1+u)]+[(1-p)\times(1-d)]=1.06\). Let’s assume \(p=0.5\), so we have:

\(0.5(1+u)+0.5(1-d)=1.06\).

Asset A has a known 40% volatility, so:

\(0.4=\sqrt{0.5(1+u)^2+0.5(1-d)^2-1.06^2}\).

We have two equations and two unknown variables, so we can solve for \((1+u)\) and \((1-d)\): \((1+u)=1.46\) and \((1-d)=0.66\). In a period of 5 years, we would have a random path of ups (1.46) and downs (0.66) for asset A.

Code
u = 0.46
d = 0.34
set.seed(2, sample.kind = "Rounding")
a <- sample(c(1+u, 1-d), 5, replace = TRUE)
a
[1] 1.46 0.66 0.66 1.46 0.66

Graphically:

Code
barplot(c(0, a-1), ylim = c(-0.6, 0.6), ylab = c("Returns: u or d."),
        col = ifelse(c(0, a-1) < 0, "red", "blue"))
abline(h = 0)

Figure 3.7: 5-year simulation of \(u\) and \(d\) for asset A.

The evolution of $1 dollar invested in this 5 year period would look like this:

Code
cumprod(a)
[1] 1.4600000 0.9636000 0.6359760 0.9285250 0.6128265

Graphically:

Code
plot(c(0:5), c(1, cumprod(a)), type = "b", lwd = 5,
     ylab = c("Cumulative $ return"), xlab = c("Year"))

Figure 3.8: 5-year simulation of cumulative returns for asset A.

Even simpler and quicker, a \(\$1\) dollar invested at \(t=0\) would lead to \(\$0.6128265\) at \(t=5\).

Code
prod(a)
[1] 0.6128265

Let’s use a function now.

Code
binomial_tree <- function(u, d, n) {
  prod(sample(c(1+u, 1-d), n, replace = TRUE))
}

See if it works. Calculate the asset A cumulative return of a 5-year and 30-year investment.

Code
set.seed(2, sample.kind = "Rounding")
binomial_tree(u = 0.46, d = 0.34, n = 5)
[1] 0.6128265
Code
set.seed(2, sample.kind = "Rounding")
binomial_tree(u = 0.46, d = 0.34, n = 30)
[1] 2.805884

It works, a \(\$1\) dollar invested at \(t=0\) would lead to \(\$0.6128265\) in 5 years as we show before. And now we know that a \(\$1\) dollar invested at \(t=0\) would lead \(\$2.805884\) in 30 years. Let’s view the 30 year case:

Code
u = 0.46
d = 0.34
n = 30
set.seed(2, sample.kind = "Rounding")
a.30 <- c(1, cumprod(sample(c(1+u, 1-d), n, replace = TRUE)))
a.30 <- as.data.frame(cbind(year = c(0:30), ret = a.30))

ggplot(a.30, aes(x = year, y = ret)) +
  geom_line(size = 1) +
  geom_point(aes(x = 5, y = 0.6128265), size = 6) +
      labs(y = "Cumulative return", x = "Year",
       subtitle = "Evolution of $1 invested in year 0.") +
  scale_y_continuous(labels = scales::dollar)

Figure 3.9: 30-year cumulative return asset A.

These results represent one single path. So, if we are interested in the most likely value of our investment we need to simulate many paths and then estimate the median. Let’s simulate 10,000 paths and see the most likely value of our \(\$1\) dollar investment in 30 years.

Code
u = 0.46
d = 0.34
n = 30
set.seed(2, sample.kind = "Rounding")
a.30x10k <- replicate(10000, 
                  cumprod(sample(c(1+u, 1-d), n, replace = TRUE))) |>
  as.data.frame() |>
  mutate(year = c(1:30)) |>
  gather(V1:V10000, key = name, value = c.ret)

See the results.

Code
a.30x10k |>
ggplot(aes(x = year, y = c.ret, color = name)) +
  geom_line(size = 1) +
  geom_point(aes(x = 0, y = 1), col = "black", size = 4) +
  theme(legend.position = "none", legend.title = element_blank()) +
  labs(y = "Cumulative return", x = "Year",
  subtitle = "10,000 paths of the evolution of $1 invested in year 0.") +
  scale_y_continuous(labels = scales::dollar)

Figure 3.10: 30-year cumulative return asset A, many paths.

Most of these paths are concentrated in low cumulative returns by the end of the 30 years. In fact, 1,586 paths end up with a cumulative return lower than $1 by the end of the 30 years:

Code
a.30x10k |>
  group_by(name) |>
  filter(c.ret < 1) |>
  filter(length(year) == 30) |>
  distinct(name) |>
  nrow()
[1] 1586

Let’s plot these 1,586 paths.

Code
a.30x10k |>
  group_by(name) |>
  filter(c.ret < 1) |>
  filter(length(year) == 30) |>
  ggplot(aes(x = year, y = c.ret, color = name)) +
  geom_line(size = 1) +
  geom_point(aes(x = 0, y = 1), col = "black", size = 4) +
  theme(legend.position = "none", legend.title = element_blank()) +
  labs(y = "Cumulative return", x = "Year",
  subtitle = "1,586 paths of the evolution of $1 invested in year 0.") +
  scale_y_continuous(labels = scales::dollar)

Figure 3.11: 30-year cumulative return asset A, 1,586 selected paths.

The figure above looks still unclear as the concentration around $0 is still high. It is better to show the results in logarithm form.

Code
a.30x10k |>
  group_by(name) |>
  filter(c.ret < 1) |>
  filter(length(year) == 30) |>
  ggplot(aes(x = year, y = log(c.ret), color = name)) +
  geom_line(size = 1) +
  geom_point(col = "black", alpha = 0.01, size = 4) +
  geom_point(aes(x = 0, y = log(1)), col = "black", size = 4) +
  theme(legend.position = "none", 
        legend.title = element_blank()) +
  labs(y = "Log cumulative return", x = "Year",
  subtitle = "Losing paths.")

Figure 3.12: 30-year log cumulative return asset A, 1,586 selected paths.

The figure now resembles a binomial tree. The black circles are more transparent for less concentrated values and less transparent for more concentrated values.

Let’s do a similar plot for all the 10,000 paths.

Code
a.30x10k |>
ggplot(aes(x = year, y = log(c.ret), color = name)) +
  geom_line(size = 1) +
  geom_point(aes(x = 0, y = log(1)), size = 3, col = "black") +
  geom_point(col = "black", alpha = 0.01, size = 3) +
  theme(legend.position = "none", legend.title = element_blank()) +
  labs(y = "Log cumulative returns", x = "Year",
  subtitle = "10,000 paths of the evolution of $1 invested in year 0.")

Figure 3.13: 30-year Log cumulative return asset A, many paths.

Here it is easier to see that the paths actually follow a binomial structure. Now, let’s see the summary statistics for the 10,000 possible investment value at year 30.

Code
set.seed(2, sample.kind = "Rounding")
summary(replicate(10000, binomial_tree(u = 0.46, d = 0.34, 30)))
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
   0.000    0.117    0.573    5.816    2.806 3559.180 

Not good news as our 30 year investment of \(\$1\) in this asset A with 6% gross return and 40% volatility leads to \(\$0.5733923\). Even if we drop the set.seed(), we end with the same result.

Code
median(replicate(10000, binomial_tree(u = 0.46, d = 0.34, 30)))
[1] 0.5733923

It would be a mistake to consider the mean $5.816 as the estimated value of our investment in asset A since the maximum value is very high and not likely to happen. Note that the mean is very close to \((\$1+0.06)^{30}=\$5.743491\).

Code
(1+0.06)^30
[1] 5.743491

The asset A is then a loosing investment, \(\$1\) would most likely lead to \(\$0.5733923\) in 30 years.

Now, let’s consider an asset B. Asset B has a known excess return of \(-0.1\%\). In the context of a binomial tree:

\(0.5(1+u)+0.5(1-d)=0.999\).

Asset B has no volatility, imagine is a Treasury Bill or a similar risk-free asset. Solving for \((1+u)\) and \((1-d)\) leads to \((1+u)=0.999\) and \((1-d)=0.999\). Asset B is for sure a loosing investment as well.

In a period of 5 years, we would have a random path of ups and downs in the context of a binomial path. Although in this case the result is the same as we have no risk.

Code
b <- sample(c(1+(-0.001), 1-(0.001)), 5, replace = TRUE)
b
[1] 0.999 0.999 0.999 0.999 0.999

The evolution of $1 dollar invested in this 5-year period would look like this:

Code
cumprod(b)
[1] 0.999000 0.998001 0.997003 0.996006 0.995010

Or simply:

Code
prod(b)
[1] 0.99501

As stated earlier, this asset B is a loosing investment. Let’s verify this for 5 and 30 years as we did before for the case of asset A:

Code
binomial_tree <- function(u, d, n) {
  prod(sample(c(1+u, 1-d), n, replace = TRUE))
}
set.seed(2, sample.kind = "Rounding")
binomial_tree(u = (-0.001), d = (0.001), 5)
[1] 0.99501
Code
binomial_tree(u = (-0.001), d = (0.001), 30)
[1] 0.970431

It works, a \(\$1\) dollar invested at \(t=0\) would lead to \(\$0.99501\) in 5 years. And now we know that a \(\$1\) dollar invested at \(t=0\) would lead to \(\$0.970431\) in 30 years. These results represent one single path, but we do not need more as there is no risk. Let’s confirm:

Code
set.seed(2, sample.kind = "Rounding")
median(replicate(10000, binomial_tree(u = -0.001, d = 0.001, 30)))
[1] 0.970431

The result is the same. Asset B is a loosing investment as well. But, what if we combine both assets A and B in an equally weighted portfolio C?

Code
set.seed(2, sample.kind = "Rounding")
median(replicate(1, binomial_tree(u = (0.46-0.001)/2, 
                                      d = (0.34+0.001)/2, 30)))
[1] 2.951197

This is how we combine two losing investments into a winner.

The new asset C or portfolio C has a return of: \(0.06/2 - 0.001/2=0.0295%\) or \(2.95\%\).

The volatility of C is \(\sqrt{0.5^2(0.4)^2+0.5^2(0)^2-0}=0.2\) or \(20\%\).

Therefore, portfolio C has a lower return and risk than asset A. Given that A and B are uncorrelated, the diversification gain is high so we can combine two losing investments into a winner.

Let’s visualize how these assets behave. I have to set the seed to have nice results. But as we show before this works well when simulating many paths and computing the median to evaluate the expected cumulative return of each asset.

Code
u = 0.46
d = 0.34
xx = 8
set.seed(xx, sample.kind = "Rounding")
a30 <- sample(c(1+u, 1-d), 30, replace = TRUE)
u = - 0.001
d = 0.001
b30 <- sample(c(1+u, 1-d), 30, replace = TRUE)
u = (0.46 - 0.001)/2
d = (0.34 + 0.001)/2
set.seed(xx, sample.kind = "Rounding")
c30 <- sample(c(1+u, 1-d), 30, replace = TRUE)

Below, it is clear that portfolio C wins less when asset A wins. Similarly, portfolio C looses less when asset A losses. This is because portfolio C is less risky than asset A.

Code
abc <- as.data.frame(cbind(
  year = c(1:30), A = cumprod(a30), 
  B = cumprod(b30), C = cumprod(c30)))
head(abc)
  year         A        B        C
1    1 1.4600000 0.999000 1.229500
2    2 2.1316000 0.998001 1.511670
3    3 1.4068560 0.997003 1.253930
4    4 0.9285250 0.996006 1.040135
5    5 1.3556464 0.995010 1.278846
6    6 0.8947267 0.994015 1.060803

This is the cumulative return evolution for each asset and portfolio C in one single path.

Code
abc |>
  gather(A:C, key = name, value = c.ret) |>
  ggplot(aes(x = year, y = c.ret, color = name)) +
  geom_line(size = 1) +
  geom_point(aes(x = 0, y = 1), size = 5, col = "black") +
  labs(y = "Cumulative return", x = "Year") +
  scale_y_continuous(labels = scales::dollar)

Figure 3.14: 30-year cumulative returns asset A, B and portfolio C, one single path.

Now we plot the 10,000 possible values of the 30 year cumulative returns.

Code
set.seed(2, sample.kind = "Rounding")
bx <- (replicate(10000, binomial_tree(u = -0.001, d = 0.001, 30)))
set.seed(2, sample.kind = "Rounding")
cx <- (replicate(10000, binomial_tree(u = (0.46-0.001)/2, 
                                      d = (0.34+0.001)/2, 30)))
set.seed(2, sample.kind = "Rounding")
ax <-(replicate(10000, binomial_tree(u = 0.46, d = 0.34, 30)))

abcx <- as.data.frame(cbind(year = c(1:30), A = ax, B = bx, C = cx))

abcx |>
  gather(A:C, key = name, value = c.ret) |>
  filter(year == 30) |>
  ggplot(aes(x = name, y = log(c.ret), color = name)) +
  geom_hline(yintercept = 0, lty = 2) +
  geom_boxplot() +
  theme(legend.position = "none") +
  labs(y = "Log cumulative returns at year 30", x = "")

Figure 3.15: 30-year log cumulative returns asset A, B and portfolio C, only the final cumulative return.

An alternative to the boxplot is a density plot.

Code
abcx |>
  gather(A:C, key = name, value = c.ret) |>
  filter(year == 30) |>
  filter(name == c("A", "C")) |>
  ggplot(aes(x = log(c.ret), fill = name)) +
  geom_vline(xintercept = 0, lty = 2) +
  geom_density(alpha = 0.5) +
  theme(legend.position = "bottom") +
  labs(x = "Log cumulative returns at year 30") +
  geom_hline(yintercept = 0) 

Figure 3.16: 30-year log cumulative returns asset A and portfolio C, only the final cumulative return, a density view.

Stock price paths are useful to value option stocks. Let’s review some stochastic processes in the next section.

Code
library(knitr)
library(kableExtra)
library(plotly)
library(vembedr)

4 Wiener processes.

Here we review stochastic process that are useful in finance, especially in the Black-Scholes context. This section is based on Hull (2015), chapter 14.

4.1 The basic process.

A simple Wiener process.

Code
# A typical Wiener process.
TT <- 5
N <- 10000
dt <- TT / N
Time <- seq(from = dt, to = TT, by = dt)
set.seed(560746) # for reproducibility purposes.
delta_z <- rnorm(N, 0, 1) * dt^0.5 
z <- 25 + cumsum(delta_z) # The initial price is 25.

Graphically:

Code
plot(Time, z, type = "l",
     ylab = "z in the texbook, it could be a stock price",
     xlab = "Time")
lines(Time, seq(25, (25 + sum(delta_z)), length.out = N), lty = 2,
      col = "purple")
points(Time[1], 25, col = "blue", cex = 2, pch = 16)
points(Time[N], z[N], col = "red", cex = 2, pch = 16)
legend("topleft", legend = c(round(z[N], 2)), bg = "white", 
       text.col = "red")

Figure 4.1: Wiener process 10,000 steps from blue \(z(0)=25\) to \(z(T)=26.2\) in red.

This is a 1,000 step version of four Wiener processes:

Code
embed_url("https://youtu.be/X-VRWeqG_I8")

This is supposed to mimic (at some extent) the evolution of a stock price.

Now, we propose to produce 100 processes.

Code
# 100 Wiener processes.
set.seed(365633)
mat.delta_z <- matrix(rnorm(100 * N, mean = 0, sd = 1), 100, N) 
mat.z1 <- 25 + t(apply(mat.delta_z, 1, cumsum)) 
mat.z <- t(cbind(matrix(25, 100), mat.z1))
Time2 <- c(0, Time)
pred <- mean(mat.z[(N + 1), ])
# 95% range.
rangetop <- pred + qnorm(0.975) * var(mat.z[(N + 1), ])^0.5
rangedown <- pred - qnorm(0.975) * var(mat.z[(N + 1), ])^0.5

Graphically.

Code
matplot(Time2, mat.z, type = "l", lty = 1, col = rgb(0, 0, 1, 0.3),
     ylab = "z in the texbook, it could be a stock price",
     xlab = "Time")
abline(h = pred, lty = 2, col = "red")
abline(v = 0, lty = 2)
lines(Time2, seq(25, pred, length.out = N + 1), lty = 2)
points(Time2[N + 1], pred, col = "red", cex = 2, pch = 16)
points(0, 25, col = "black", cex = 2, pch = 16)
legend("topleft", legend = c(round(pred, 2)), bg = "white", 
       text.col = "red")
abline(h = rangetop, lty = 2)
abline(h = rangedown, lty = 2)

Figure 4.2: 100 Wiener process 10,000 steps, initial price at 25, red is the mean at \(T\).

Let’s confirm this 95% confidence interval.

Code
sum(mat.z[(N + 1), ] > rangetop)
[1] 3
Code
sum(mat.z[(N + 1), ] < rangedown)
[1] 2

This means that 5 paths are outside this 95% confidence interval. Confidence intervals are then correct.

Code
summary(mat.z[(N + 1), ])
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
-155.23  -29.14   23.33   25.95   72.21  310.30 

A potential problem is that \(z\) values can be negative. This is problematic when we are interested to model stock prices.

Let’s explore deeper the stochastic process below.

Code
set.seed(1)
delta_z <- rnorm(N, 0, 1) * dt^0.5 
z <- 25 + cumsum(delta_z)
### A stochastic process
par(mfrow = c(2, 2), mai = c(0.4, 0.4, 0.4, 0.4))
par(pty = "s")
plot(Time, z, type = "l", xlim = c(0, TT), ylim = c(22.5, 26),
     main = "Full 5-year period", ylab = "z", xlab = "Time")
plot(Time, delta_z, type = "h", xlim = c(0, TT),
     main = "Full 5-year period", 
     ylab = expression(paste(Delta,z)), xlab = "Time")
abline(h = 0, lty = 2, col = "orange", lwd = 2)
plot(Time, z, type = "l", xlim = c(0, 1/12), ylim = c(24.9, 25.35),
     main = "Zoom: first month only", xlab = "Time")
plot(Time, delta_z, type = "h", xlim = c(0, 1/12), 
     col = ifelse(delta_z < 0, "red", "blue"), ylim = c(-0.055, 0.055),
     main = "Zoom: first month only", 
     ylab = expression(paste(Delta,z)), xlab = "Time")

Figure 4.3: A stochastic process with zoom.

These processes are driven by a random component. The lower panel is revealing because we can see how the random component goes from positive to negative without any pattern, it is entirely random. Now, let’s explore the role of \(\Delta t\).

Code
par(mfrow = c(2, 2), mai = c(0.4, 0.4, 0.4, 0.4))
par(pty = "s")
plot(Time[(seq(from = 1, to = N, length.out = 10))],
     z[(seq(from = 1, to = N, length.out = 10))], type = "l",
     ylim = c(22.5, 26), main = expression(paste("Large ",Delta,t)),
     ylab = "z")
abline(v = 0, lty = 2)
abline(v = 5, lty = 2)
plot(Time[(seq(from = 1, to = N, length.out = 100))],
     z[(seq(from = 1, to = N, length.out = 100))], type = "l", 
     ylim = c(22.5, 26), ylab = "z", 
     main = expression(paste("Small ",Delta,t)))
abline(v = 0, lty = 2)
abline(v = 5 , lty = 2)
plot(Time[(seq(from = 1, to = N, length.out = 1000))],
     z[(seq(from = 1, to = N, length.out = 1000))], type = "l", 
     ylim = c(22.5, 26), ylab = "z", 
     main = expression(paste("Even smaller ",Delta,t)))
abline(v = 0, lty = 2)
abline(v = 5 , lty = 2)
plot(Time, z, type = "l", ylim = c(22.5, 26), # length 10,000
     main = expression(paste("True process ",Delta,t, " ",
                             "tends ","to ","zero")))
abline(v = 0, lty = 2)
abline(v = 5, lty = 2)

Figure 4.4: Wiener process, the role of \(\Delta t\).

The \(\Delta t\) value determines how frequent are the changes in \(z\).

A few properties of stochastic processes.

Code
mean(delta_z) # should be zero
[1] -0.0001461726
Code
var(delta_z)^0.5 # should be dt^.5
[1] 0.02263698
Code
dt^0.5
[1] 0.02236068
Code
var(delta_z) # should be dt
[1] 0.0005124328
Code
dt
[1] 5e-04

4.2 The generalized Wiener process.

Now let’s analyze the case of the generalized Wiener process.

\(\Delta x = a \Delta t + b \epsilon \sqrt{\Delta t}\). Assume \(a=1.5\) and \(b=0.3\):

Code
# Figure 14.2
TT <- 50
N <- 200
dt <- TT / N
Time <- seq(from = 0, to = TT, by = dt)
set.seed(123)
delta_z <- 1.5 * rnorm(N, 0, 1) * dt^0.5
delta_x <- (0.3 * dt) + delta_z
x <- c(0, cumsum(delta_x))
z <- c(0, cumsum(delta_z))

Graphically:

Code
plot(Time, x, type = "l", lwd = 4, ylim = c(-7, 22))
lines(Time, z, col = "red", lwd = 4)
lines(Time, 0.3 * Time, col = "blue", lwd = 4)
abline(0, 0)
abline(v = 0)
legend("topleft", legend = c("Generalized Wiener process", 
                             "Drift", "Basic Wiener process"),
       col = c("black", "blue", "red"), lwd = 3, 
       bg = "white", cex = 0.8)

Figure 4.5: Generalized Wiener process.

Here, the generalized Wiener process \(\Delta x\) is decomposed into the drift \(a \Delta t\) and the basic Wiener process \(b \epsilon \sqrt{\Delta t}\). A zoom of the same plot.

Code
# Figure 14.2 Zoom
plot(Time, x, type = "b", ylim = c(-0.6, 1), xlim = c(0, 1), lwd = 4)
lines(Time, z, col = "red", lwd = 4, type = "b")
lines(Time, 0.3 * Time, col = "blue", lwd = 4, type = "b")
abline(0, 0)
abline(v = 0)
abline(v = dt, lty = 2)
abline(v = dt * 2, lty = 2)
abline(v = dt * 3, lty = 2)
abline(v = dt * 4, lty = 2)
#points(dt, 0, pch = 1, col = "blue", lwd = 2)
#points(dt * 2, 0, pch = 1, col = "blue", lwd = 2)
#points(dt * 3, 0, pch = 1, col = "blue", lwd = 2)
#points(dt * 4, 0, pch = 1, col = "blue", lwd = 2)
legend("topleft", legend = c("Generalized Wiener process", 
                           "Drift", "Basic Wiener process"),
       col = c("black", "blue", "red"), lty = 1, 
       bg = "white", lwd = 2)

Figure 4.6: Generalized Wiener process, zoom view.

This is a nice visual representation of: generalized = drift + Wiener.

See how the process changes when we consider a lower value of \(a\).

Code
set.seed(123)
delta_xlow <- (0.15 * dt) + delta_z
xlow <- c(0, cumsum(delta_xlow))
# Now plot.
plot(Time, xlow, type = "l", ylim = c(-7, 22), lwd = 4, ylab = "x")
lines(Time, x, lwd = 2, col = "grey")
lines(Time, z, lwd = 4, col = "red")
lines(Time, 0.15 * Time, lwd = 4, col = "blue")
lines(Time, 0.3 * Time, lty = 2, lwd = 2, col = "grey")
abline(0, 0)
abline(v = 0)
legend("topleft", legend = c("Original generalized Wiener process", 
    "Original drift", "New generalized Wiener process", "New drift",
    "Basic Wiener process"), lty = c(1, 2, 1, 1, 1), 
    bg = "white", lwd = 2,
    col = c("grey", "grey", "black", "blue", "red"), cex = 0.85)

Figure 4.7: Generalized Wiener process, lower a.

And a higher value of \(b\).

Code
set.seed(123)
delta_zhigh <- 3 * rnorm(N, 0, 1) * dt^0.5
delta_xhigh <- (0.3 * dt) + delta_zhigh
xhigh <- c(0, cumsum(delta_xhigh))
zhigh <- c(0, cumsum(delta_zhigh))
plot(Time, xhigh, type = "l", ylim = c(-7, 22), lwd = 4, ylab = "x")
lines(Time, x, lwd = 2, col = "grey")
lines(Time, zhigh, lwd = 4, col = "red")
lines(Time, z, lwd = 2, col = "grey")
lines(Time, 0.3 * Time, lwd = 4, col = "blue")
abline(0, 0)
abline(v = 0)
legend("topleft", legend = c("New generalized Wiener process", 
      "Original generalized Wiener process", "Drift" ,
      "New basic Wiener process", "Original basic Wiener process"), 
      lty = 1, bg = "white", lwd = 2,
      col = c("black", "grey", "blue", "red", "grey"), cex = 0.75)

Figure 4.8: Generalized Wiener process, higher b.

Note the changes are now more pronounced.

This is a replication of Hull (2015), table 14.1. Simulation of stock price when \(\mu= 0.15\) and \(\sigma = 0.30\) during 1-week periods.

Code
# Table 14.1
set.seed(19256)
delta_S <- rep(0, 10)
S <- rep(100, 10)
epsilon <- rep(0, 10)
for(i in 1:10){
  epsilon[i] <- rnorm(1, 0, 1)
  delta_S[i] <- 0.15 * (1 / 52) * S[i] + 0.3 * ((1 / 52)^0.5) * 
    epsilon[i] * S[i]
  S[i+1] <- S[i] + delta_S[i]
}
epsilon <- c(epsilon, NA)
delta_S <- c(delta_S, NA)
results <- data.frame(S, epsilon, delta_S)
results
          S    epsilon    delta_S
1  100.0000  0.8268625  3.7284174
2  103.7284  0.4957988  2.4387684
3  106.1672 -0.9313823 -3.8074983
4  102.3597 -0.1495925 -0.3417592
5  102.0179  0.7310239  3.3968957
6  105.4148 -0.6111472 -2.3761181
7  103.0387  1.2490868  5.6516490
8  108.6904 -0.6243389 -2.5096009
9  106.1808  0.6995349  3.3964067
10 109.5772  0.3612549  1.9629353
11 111.5401         NA         NA

It is interesting to note that in Hull (2015) the final simulated price is $111.54 and here it is $111.5401. Is this a pure coincidence?

A correlated process.

\(\Delta x_1 = a_1 \Delta t + b_1 \epsilon_1 \sqrt{\Delta t}\),

\(\Delta x_2 = a_2 \Delta t + b_2 \epsilon_2 \sqrt{\Delta t}\).

Where: \(\epsilon_1 = u\), and \(\epsilon_2=\rho u + \sqrt{1-\rho^{2}}v\).

Code
# Section 14.5
rho <- 0.8
TT <- 1
steps <- 1000
dt <- TT / steps
Time <- seq(from = dt, to = TT, by = dt)
set.seed(123)
e1 <- rnorm(steps, 0, 1)
e2 <- rho * e1 + ((1 - rho^2)^0.5) * rnorm(steps, 0, 1)
delta_z1 <- 1.5 * e1 * dt^0.5
delta_z2 <- 1.5 * e2 * dt^0.5
delta_x1 <- (0.3 * dt) + delta_z1
delta_x2 <- (0.3 * dt) + delta_z2
x1 <- cumsum(delta_x1)
x2 <- cumsum(delta_x2)
x <- c(x1, x2)

Assume \(a=1.5\), \(b=0.3\) and \(\rho=0.8\). Visually:

Code
plot(Time, x1, type = "l", ylim = c(min(x), max(x)), ylab = "x1 and x2")
lines(Time, x2, col = "red")
abline(0, 0)

Figure 4.9: Correlated processes x1 and x2.

The model of stock price behavior implies that a stock’s price at time \(T\), given its price today, is lognormally distributed. The following is similar to example 15.1 in Hull (2015). In particular, consider a stock with an initial price of $40, an expected return of 16% per annum, and a volatility of 20% per annum.

\(\frac{\Delta S}{S} = a \Delta t + b \epsilon \sqrt{\Delta t}\),

\(\frac{\Delta S}{S} = 0.16 \Delta t + 0.2 \epsilon \sqrt{\Delta t}\),

Assume \(\Delta t = \frac{1}{365}\): \(\frac{\Delta S}{S} = 0.16 \frac{1}{365} + 0.2 \epsilon \sqrt{\frac{1}{365}}\),

Calculate the constants: ::: {.cell}

Code
0.16/365
[1] 0.0004383562
Code
0.2*(1/365)^0.5
[1] 0.01046848

::: Finally: \(\Delta S = 0.0004383562 S + 0.01046848 S \epsilon\). The 100 simulated 6-months paths of the stock price \(S\) are:

Code
set.seed(10101010)
for(j in 1:100) {
  delta_S <- NULL 
  S <- rep(40, 180) # Now it is 40 for all days, the loop fill this out.
  epsilon <- NULL 
  for(i in 1:180) {
    epsilon[i] <- rnorm(1, 0, 1)
    delta_S[i] <- 0.16 * (1 / 365) * S[i] + 0.2 * ((1 / 365)^0.5) *
      epsilon[i] * S[i]
    S[i+1] <- S[i] + delta_S[i]
    }
  if (j==1) {
    plot(S, type = "l", ylim = c(30, 60), 
         ylab = expression(paste(S[t])), xlab = "t (days)")
    points(0, 40, pch = 19, col = "blue", cex = 3)
    abline(h = 10, col = "red", lwd = 3)
    }
  lines(S)
  }
abline(h = 32.55, lwd = 2, col = "red")
abline(h = 56.56, lwd = 2, col = "red")
abline(v = 181, lwd = 2, col = "red")

Figure 4.10: A geometric Brownian motion simulation: 100 stock price paths.

The code above can be problematic because we are not collecting all paths in an object. An alternative code is the following. Again, this is similar to example 15.1 in Hull (2015).

Code
S0 <- 40
nDays <- 360
mu <- 0.16
sig <- 0.2
TT <- 0.5
mean.logST <- log(S0)+(mu - (sig^2 / 2)) * TT
variance.logST <- sig^2 * TT
lower.ST <- exp(mean.logST + qnorm(0.025) * variance.logST^0.5)
upper.ST <- exp(mean.logST + qnorm(0.975) * variance.logST^0.5)
set.seed(3)
nSim <- 1000
SP_sim <- matrix(0, nrow = 180, ncol = nSim)
for(i in 1:nSim){
  SVec <- rep(0, 180)
  SVec[1] <- S0
  for(j in 2:180){
    DeltaS <- mu * SVec[j - 1] * (1 / nDays) + sig * SVec[j - 1] *
      rnorm(1) * (1 / nDays)^0.5
    SVec[j] <- SVec[j - 1] + DeltaS
  }
  SP_sim[, i] <- SVec
}

Now, let’s plot the results.

Code
matplot(SP_sim, type = 'l', col = rgb(0, 0, 1 ,0.3), lty = 1,
        ylab = expression(paste(S[t])),
        xlab = "Time")
points(0, S0, pch = 19, cex = 3)
abline(h = lower.ST)
abline(h = upper.ST)
abline(v = 180)

Figure 4.11: A geometric Brownian motion simulation: 1000 stock price paths.

Let’s verify that the there is a 95% probability that the stock price in 6 months will lie between 32.55 and 56.56.

Code
1 - (sum(SP_sim[180, ] < lower.ST) + sum(SP_sim[180, ] > upper.ST)) / 1000
[1] 0.944
Code
S0 <- 40
mu <- 0.16
sig <- 0.2
TT <- 0.5
mean.logST <- log(S0)+(mu - (sig^2 / 2)) * TT
variance.logST <- sig^2 * TT
lower.ST <- exp(mean.logST + qnorm(0.025) * variance.logST^0.5)
upper.ST <- exp(mean.logST + qnorm(0.975) * variance.logST^0.5)
lower.ST
[1] 32.51491
Code
upper.ST
[1] 56.6029

Nice.

This is how a stock price evolves according to the Black-Scholes formula.

5 Value at risk.

Value at Risk (VaR) is an attempt to provide a single number summarizing the total risk in a portfolio of financial assets. Here, we develop and extend the example called Investment in Four Stock Indices in Chapters 22 and 23 of Hull (2015). The database used in this example dataR.txt is available here.

Suppose that we want to calculate VaR for a portfolio using a one-day time horizon, a 99% confidence level, and 501 days of data, from Monday August 6, 2006 to Thursday September 25, 2008.

5.1 Prepare the data.

Load and prepare the database.

Code
df <- read.table("dataR.txt", sep = "", header = TRUE) 

Take a look of the database.

Code
head(df)
      DJIA FTSE100 USDGBP   CAC40 EURUSD   Nikkei YENUSD
1 11219.38  5828.8 1.9098 4956.34 0.7776 15154.06 115.00
2 11173.59  5818.1 1.9072 4967.95 0.7789 15464.66 115.08
3 11076.18  5860.5 1.9086 5025.15 0.7762 15656.59 115.17
4 11124.37  5823.4 1.8918 4976.64 0.7828 15630.91 115.41
5 11088.02  5820.1 1.8970 4985.52 0.7833 15565.02 116.07
6 11097.87  5870.9 1.8923 5046.93 0.7847 15857.11 116.45

These are four international stock indices: Dow Jones Industrial Average (DJIA) in the US, the FTSE 100 in the UK, the CAC 40 in France, and the Nikkei 225 in Japan together with the corresponding exchange rates.

Because we are considering a US investor in this example, the value of the FTSE 100, CAC 40, and Nikkei 225 must be measured in US dollars. The adjusted US dollar equivalent of stock indices are calculated as:

\(FTSE_{USD}=FTSE_{GBP}\times\frac{USD}{GBP}\).

\(CAC_{USD}=\frac{CAC_{EUR}}{\frac{EUR}{USD}}\).

\(Nikkei_{USD}=\frac{Nikkei_{JPY}}{\frac{JPY}{USD}}\).

Code
df.Adj <- df %>%
  mutate(Day = c(0:501)) %>%
  mutate(AFTSE100 = FTSE100 * USDGBP) %>%
  mutate(ACAC40 = CAC40 / EURUSD) %>%
  mutate(ANikkei = Nikkei / YENUSD)

The following is the US dollar equivalent of stock indices for historical simulation. This is the same as Hull (2015), Table 22.2:

Code
df.Adj %>%
  select(Day, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  slice(1, 2, 3, 4, 500, 501)
  Day     DJIA  AFTSE100   ACAC40  ANikkei
1   0 11219.38 11131.842 6373.894 131.7744
2   1 11173.59 11096.280 6378.162 134.3818
3   2 11076.18 11185.350 6474.040 135.9433
4   3 11124.37 11016.708 6357.486 135.4381
5 499 10825.17  9438.580 6033.935 114.2604
6 500 11022.06  9599.898 6200.396 112.8221

The number of observations is 501 as we start on day zero. The A before the index name stands for adjusted. A graphical representation of the table above:

Code
df.Adj %>%
  mutate(ANikkei = ANikkei*100) %>%
  mutate(obs = c(1:502)) %>%
  select(obs, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  filter(obs < 502) %>%
  gather(IX, Adj, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  mutate(IX = as.factor(IX)) %>%
  ggplot(aes(x = obs, y = Adj, col = IX)) +
  geom_line(size = 1) +
  labs(y = "US dollar equivalent of stock indices", x = "") +
  theme(legend.position = "bottom")

Figure 5.1: Adjusted index values, Nikkei = Nikkei \(\times\) 100.

The time-series starts on Monday August 6, 2006 and ends on Thursday September 25, 2008. These are 501 daily past observations. In the plot below, we also show the value of Friday September 26, 2008 as a dot, this is observation 502 and we call it tomorrow. This observation 502 is not included in the VaR estimation as it represents an unknown future value, but it will be useful for us to evaluate our estimations.

Code
tomorrow <- df.Adj %>%
  mutate(ANikkei = ANikkei*100) %>%
  mutate(obs = c(1:502)) %>%
  select(obs, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  filter(obs == 502) %>%
  gather(IX, Adj, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  mutate(IX = as.factor(IX))

df.Adj %>%
  mutate(ANikkei = ANikkei*100) %>%
  mutate(obs = c(1:502)) %>%
  select(obs, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  filter(obs > 400) %>%
  filter(obs < 502) %>%
  gather(IX, Adj, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  mutate(IX = as.factor(IX)) %>%
  ggplot(aes(x = obs, y = Adj, col = IX)) +
  geom_line(size = 1) +
  geom_point(aes(obs, Adj), size = 2, alpha = 0.4, data = tomorrow) +
  labs(y = "US dollar equivalent of stock indices", x = "") +
  theme(legend.position = "bottom")

Figure 5.2: Adjusted index values, Nikkei = Nikkei \(\times\) 100, zoom view.

The stock indices values of Friday September 26, 2008 are:

Code
tomorrow
  obs       IX       Adj
1 502     DJIA 11143.130
2 502 AFTSE100  9379.123
3 502   ACAC40  6081.478
4 502  ANikkei 11216.788

5.2 Historical simulation.

The historical simulation method requires scenarios of the possible stock indices values for Friday September 26, 2008. In particular, we constructed 500 scenarios for Friday September 26, 2008 with 501 past observations. The historical simulation assumes that the stock index value of Friday September 26 2008 depends on the value of Thursday September 25 2008 times a percentage change (which could be positive or negative). The percentage changes are calculated as follows. The first percentage change is from Monday August 6 2006 to Tuesday August 7 2006. The second percentage change is from Tuesday August 7 2006 to Wednesday 8 2006, and so on. In sum, we assume that tomorrow may behave as the past 500 days.

Take the adjusted values as a reference:

Code
df.Adj %>%
  select(Day, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  slice(1, 2, 3, 4, 500, 501)
  Day     DJIA  AFTSE100   ACAC40  ANikkei
1   0 11219.38 11131.842 6373.894 131.7744
2   1 11173.59 11096.280 6378.162 134.3818
3   2 11076.18 11185.350 6474.040 135.9433
4   3 11124.37 11016.708 6357.486 135.4381
5 499 10825.17  9438.580 6033.935 114.2604
6 500 11022.06  9599.898 6200.396 112.8221

The first three Dow Jones scenarios \(SDJIA\) are:

\(SDJIA_1 = 11022.06 \times \frac{11173.59}{11219.38} \rightarrow SDJIA_1 = 10977.08\).

\(SDJIA_2 = 11022.06 \times \frac{11076.18}{11173.59} \rightarrow SDJIA_2 = 10925.97\)

\(SDJIA_3 = 11022.06 \times \frac{11124.37}{11076.18} \rightarrow SDJIA_3 = 11070.01\)

The three Dow Jones possible values according to the historical approach for September 26, 2008 are: 10977.08, 10925.97 and 11070.01. We know the actual value is 11143.130, but this is only for evaluation purposes.

Scenarios generated for September 26, 2008:

Code
df.Adj.S <- df.Adj %>%
  mutate(obs = c(0:501)) %>%
  mutate(SDJIA = DJIA[501] * (DJIA/lag(DJIA))) %>%
  mutate(SFTSE100 = AFTSE100[501] * (AFTSE100/lag(AFTSE100))) %>%
  mutate(SCAC40 = ACAC40[501] * (ACAC40/lag(ACAC40))) %>%
  mutate(SNikkei = ANikkei[501] * (ANikkei/lag(ANikkei)))

df.Adj.S %>%
  select(obs, SDJIA, SFTSE100, SCAC40, SNikkei) %>%
  slice(2, 3, 4, 500, 501)
  obs    SDJIA SFTSE100   SCAC40  SNikkei
1   1 10977.08 9569.230 6204.547 115.0545
2   2 10925.97 9676.957 6293.603 114.1331
3   3 11070.01 9455.160 6088.768 112.4028
4 499 10831.43 9383.488 6051.936 113.8498
5 500 11222.53 9763.974 6371.450 111.4019

This is similar to Table 22.3, Hull (2015). Let’s see the scenarios above in a plot, including the actual values for September 26, 2008:

Code
levels(tomorrow$IX) <- list(SCAC40 = "ACAC40", SDJIA = "DJIA", 
                         SFTSE100 = "AFTSE100", SNikkei = "ANikkei")

df.Adj.S %>%
  mutate(SNikkei = SNikkei*100) %>%
  mutate(obs = c(1:502)) %>%
  select(obs, SDJIA, SFTSE100, SCAC40, SNikkei) %>%
  filter(obs < 502) %>%
  gather(IX, S, SDJIA, SFTSE100, SCAC40, SNikkei) %>%
  mutate(IX = as.factor(IX)) %>%
  ggplot(aes(x = IX, fill = IX)) +
  geom_violin(aes(IX, S), draw_quantiles = 0.5) +
  geom_point(aes(IX, Adj), size = 3,
             data = tomorrow, show.legend = FALSE) +
  labs(y = "Scenarios generated for September 26, 2008", x = "") +
  theme(legend.position = "none")

According to the example, an investor in the United States owns, on September 25, 2008, a portfolio worth $10 million consisting of 40% in the DJIA, 30% in the FTSE, 10% in the CAC, and 20% in the Nikkei. We can calculate the correspondent 500 scenarios of the possible portfolio value \(p\) for September 26, 2008:

\(p_1 = 4000 \frac{SDJIA_1}{DJIA_{sept.25.2008}} + 3000 \frac{SFTSE_1}{FTSE_{sept.25.2008}} + 1000 \frac{SCAC_1}{CAC_{sept.25.2008}} + 2000 \frac{SNikkei_1}{Nikkei_{sept.25.2008}}\),

\(p_1 = 4000 \frac{10977.08}{11022.06} + 3000 \frac{9569.230}{9599.898} + 1000 \frac{6204.547}{6200.396} + 2000 \frac{115.0545}{112.8221}\),

\(p_1 = \$10,014.334\).

\(p_2 = 4000 \frac{SDJIA_2}{DJIA_{sept.25.2008}} + 3000 \frac{SFTSE_2}{FTSE_{sept.25.2008}} + 1000 \frac{SCAC_2}{CAC_{sept.25.2008}} + 2000 \frac{SNikkei_2}{Nikkei_{sept.25.2008}}\),

\(p_2 = 4000 \frac{10925.97}{11022.06} + 3000 \frac{9676.957}{9599.898} + 1000 \frac{6293.603}{6200.396} + 2000 \frac{114.1331}{112.8221}\),

\(p_2 = \$10,027.481\).

And so on.

Code
df.Adj.S <- df.Adj.S %>%
  mutate(p = (4000 * SDJIA / DJIA[501]) + # Investment portfolio value
             (3000 * SFTSE100 / AFTSE100[501]) + 
             (1000 * SCAC40 / ACAC40[501]) + 
             (2000 * SNikkei / ANikkei[501])) %>%
  mutate(l = 10000 - p) # Losses l

df.Adj.S %>%
  select(obs, SDJIA, SFTSE100, SCAC40, SNikkei, p, l) %>%
  slice(2, 3, 4, 500, 501)
  obs    SDJIA SFTSE100   SCAC40  SNikkei         p          l
1   1 10977.08 9569.230 6204.547 115.0545 10014.334  -14.33385
2   2 10925.97 9676.957 6293.603 114.1331 10027.481  -27.48131
3   3 11070.01 9455.160 6088.768 112.4028  9946.736   53.26406
4 499 10831.43 9383.488 6051.936 113.8498  9857.465  142.53549
5 500 11222.53 9763.974 6371.450 111.4019 10126.439 -126.43897

The \(l\) stands for portfolio losses, it is \(l = \$10,000 - p\). Note that a negative loss represents a portfolio gain. Values above are exactly the same as in Hull (2015) Table 22.3.

Visually.

Code
l <- df.Adj.S %>%
  mutate(obs = c(0:501)) %>%
  select(obs, l) %>%
  filter(obs > 0 & obs < 501) %>%
  select(l) %>%
  unlist()

l.tomorrow <- df.Adj.S %>%
  mutate(obs = c(0:501)) %>%
  select(obs, l) %>%
  filter(obs == 501) %>%
  select(l) %>%
  unlist()
Code
plot(l, type = "h", lwd = 2, 
     xlab = "500 scenarios (sorted by historical date)",
     ylab = "Portfolio gains (-) and losses (+)", 
     col = ifelse(l < 0, "blue", "red"))
points(501, l.tomorrow, pch = 19, cex = 2)
abline(0, 0)
abline(v = 0)
lines(seq(0, max(l, na.rm = TRUE), length.out = 500), lty = 2)
lines(seq(0, min(l, na.rm = TRUE), length.out = 500), lty = 2)
abline(h = 253.385, lwd = 2, col = "green")
legend("bottomleft", legend = c("Gains", "Losses", "Historic VaR: 253.385"),
       col = c("blue", "red", "green"), lty = 1, bg = "white", lwd = 2)

Figure 5.3: Today is September 25, 2008. What is the worst that can happen to my portfolio tomorrow?

Now, a histogram of losses.

Code
hist(l, 15, xlab = "Portfolio losses (+) and gains (-)", main = NULL)
points(l.tomorrow, 1, pch = 19, cex = 2)
abline(v = 253.385, col = "red", lwd = 2)
legend("topleft", legend = c("Historic VaR: 253.385"), col = "red", 
       bg = "white", lwd = 2)

Figure 5.4: Histogram of losses for the scenarios considered between September 25 and 26, 2008: a histogram.

The point refers to what really happened, and the red line is the VaR following a historic approach. Now, the same information but now using a density plot.

Code
densi <- density(l, na.rm = TRUE)
plot(densi, main = "", xlab = "Portfolio losses (+) and gains (-)")
polygon(densi, col = "grey", border = "black")
points(l.tomorrow, 0, pch = 19, cex = 2)
abline(v = 253.385, col = "red", lwd = 2)
legend("topleft", legend = c("Historic VaR: 253.385"), col = "red", 
       bg = "white", lwd = 2)

Figure 5.5: Histogram of losses for the scenarios considered between September 25 and 26, 2008: a density plot.

Here is a histogram that looks like the one in Hull (2015).

Code
hist(l, 15, axes = F, lty = "blank", col = "grey", main = "",
     xlab = "Portfolio losses (+) and gains (-)")
axis(1, pos = 0)
axis(2, pos = 0, las = 1)

Figure 5.6: Figure 22.3. Histogram of losses for the scenarios considered between September 25 and 26, 2008.

A table for the losses ranked. In particular, losses ranked from highest to lowest for 500 scenarios (first 15 values).

Code
Table22.4 <- data.frame(scenario.number = order(l, decreasing = TRUE),
                        `loss in positive values` = 
                          l[order(l, decreasing = TRUE)])
head(Table22.4, 15)
     scenario.number loss.in.positive.values
l494             494                477.8410
l339             339                345.4351
l349             349                282.2038
l329             329                277.0413
l487             487                253.3850
l227             227                217.9740
l131             131                202.2555
l238             238                201.3892
l473             473                191.2691
l306             306                191.0497
l477             477                185.1269
l495             495                184.4496
l376             376                182.7072
l237             237                180.1048
l365             365                172.2237

And the last values (not shown in Hull).

Code
tail(Table22.4)
     scenario.number loss.in.positive.values
l395             395               -224.5146
l489             489               -284.9247
l341             341               -307.9301
l377             377               -316.4893
l379             379               -333.0218
l497             497               -555.7954

Note that one of the highest gains happens in scenario 497, almost at the end.

Code
VaR.hist <- 253.3850    
Results <- data.frame(VaR.method = "Historical simulation, sub-optimal weights", 
                      VaR = VaR.hist)
Results
                                  VaR.method     VaR
1 Historical simulation, sub-optimal weights 253.385

We call sub-optimal weights because the allocation of 40% in the DJIA, 30% in the FTSE, 10% in the CAC, and 20% in the Nikkei is arbitrary. The VaR according to the historic approach is the highest 5th loss because the top 1% of 500 scenarios is the 5th scenario. We can view this in a barplot.

Code
barplot(l[order(l, decreasing = TRUE)], 
        names.arg = order(l, decreasing = TRUE), ylim = c(0, 500), 
        xlim = c(0, 20), las = 2, cex.names = 1, ylab = "Porfolio loss ($000s)",
        col = "red", xlab = "Scenario number (ranked)")
abline(h = 253.385, lwd = 3)
abline(h = 0, lwd = 3)
abline(h = l.tomorrow, lwd = 3, col = "green")
legend("topright", legend = c("Historic VaR: 253.385", 
       "What really happened"), col = c("black", "green"), 
       bg = "white", lwd = 2)

Figure 5.7: Extract of table 22.4 in a graph VaR historical approach 1 percent 253.385 What really happen in green.

The top 1% can be also estimated by implementing the quantile() function. In theory, this should be equivalent to taking the highest 5th loss, however when we implement the quantile() function leads to a different result.

Code
VaR.hist.quantile <- quantile(l, 0.99)
VaR.hist.quantile
     99% 
218.3281 

The reason is that the quantile() function is continuous, and the historic simulation approach is discrete. With 500 discrete values we can take the highest 5th loss in the table above to represent the top 1% as we count: \(0.002 + 0.002 + 0.002 + 0.002 + 0.002 = 0.01\). However, if we want to fit 500 values in a continuous framework we have to start from 0, so from 0 to 100% we end up with different breaks as: \(0 + 0.002004008 + 0.002004008 + 0.002004008 + 0.002004008 = 0.008016032\). Then, in order to match the discrete and continuous approach we have to set 0.8016032% instead of 1%:

Code
quantile(l, 1-(0.008016032))
99.1984% 
 253.385 
Code
quantile(l, 0.991983968)
99.1984% 
 253.385 

Which is the same as the historical approach VaR. If we use the quantile() function at 1% we end up with:

Code
quantile(l, 0.99)
     99% 
218.3281 

A graphical approach:

Code
plot(seq(from = 0.902, to = 1, by = 0.002), 
     quantile(l, seq(from = 0.902, to = 1, by = 0.002)), cex = 1.5,
     xlab = "Confidence level (vertical line at 99%)", 
     ylab = "Portfolio VaR")
abline(v = 0.99, lty = 2, lwd = 2)
abline(h = quantile(l, 0.99), col = "blue", lty = 2, lwd = 2)
abline(h = Table22.4[5, 2], col = "red", lty = 2, lwd = 2)
legend("topleft", legend = c("99% level", "Historic VaR: 253.385", 
                             "99% quantile VaR: 218.3281"),
       col = c("black", "red", "blue"), lty = 2, bg = "white", lwd = 2)

Figure 5.8: A quantile approach.
Code
ci <- seq(0, 1, length.out = 500)
ind <- c(1:500)
tail(data.frame("quantile function" = quantile(l, ci), 
                "scenario number" = order(l),
                "sort losses" = sort(l)))
             quantile.function scenario.number sort.losses
98.9979960%           217.9740             227    217.9740
99.1983968%           253.3850             487    253.3850
99.3987976%           277.0413             329    277.0413
99.5991984%           282.2038             349    282.2038
99.7995992%           345.4351             339    345.4351
100.0000000%          477.8410             494    477.8410

Summary of results.

Code
Results.updated <- Results %>%
  add_row(VaR.method = 
            "Historical approximate quantile(), sub-optimal weights", 
          VaR = VaR.hist.quantile) %>%
  arrange(VaR.method)
Results.updated
                                              VaR.method      VaR
1 Historical approximate quantile(), sub-optimal weights 218.3281
2             Historical simulation, sub-optimal weights 253.3850

5.3 Model building approach.

The drawback of this method is that it assumes a normal distribution and we know \(l\) is not normal. The advantage of this method is its simplicity. Transform adjusted indices to returns.

Code
R <- df.Adj %>%
  select(Day, DJIA, AFTSE100, ACAC40, ANikkei) %>%
  mutate(RDJIA = (DJIA-lag(DJIA))/lag(DJIA)) %>%
  mutate(RFTSE100 = (AFTSE100-lag(AFTSE100))/lag(AFTSE100)) %>%
  mutate(RCAC40 = (ACAC40-lag(ACAC40))/lag(ACAC40)) %>%
  mutate(Nikkei = (ANikkei-lag(ANikkei))/lag(ANikkei)) %>%
  select(RDJIA, RFTSE100, RCAC40, Nikkei) %>%
  slice(-1)

R.tomorrow <- R %>%
  slice(501)

R <- R %>%
  slice(-501)

Replicate Hull’s table 23.5. This is the correlation matrix on September 25, 2008, calculated by giving equal weight to the last 500 daily returns.

Code
Rcor <- cor(R)
Table23.5 <- Rcor
Table23.5
               RDJIA  RFTSE100    RCAC40      Nikkei
RDJIA     1.00000000 0.4891059 0.4957096 -0.06189921
RFTSE100  0.48910594 1.0000000 0.9181083  0.20094221
RCAC40    0.49570963 0.9181083 1.0000000  0.21095096
Nikkei   -0.06189921 0.2009422 0.2109510  1.00000000

Replicate Hull’s table 23.6. This is the covariance matrix on September 25, 2008, calculated by giving equal weight to the last 500 daily returns.

Code
Rcov <- cov(R)
Table23.6 <- Rcov
Table23.6
                 RDJIA     RFTSE100       RCAC40        Nikkei
RDJIA     1.229524e-04 7.696591e-05 7.682514e-05 -9.493488e-06
RFTSE100  7.696591e-05 2.013973e-04 1.821076e-04  3.944302e-05
RCAC40    7.682514e-05 1.821076e-04 1.953506e-04  4.078130e-05
Nikkei   -9.493488e-06 3.944302e-05 4.078130e-05  1.913129e-04

This covariance matrix is quite similar to the one in Hull (2015). We are not taking round values this explains minor differences.

The variance of the portfolio losses ($000s) is:

Code
Rsd <- apply(R, 2, sd)
alpha <- c(4000, 3000, 1000, 2000)
(Pvar <- t(alpha) %*% Rcov %*% alpha) # equation in page 506 (chapter 22)
         [,1]
[1,] 8779.392

This is slightly different as in Hull (2015): 8,761.833. The standard deviation is the square root of this:

Code
(Psd <- Pvar^0.5)
         [,1]
[1,] 93.69841

As expected, this is a bit different as in Hull (2015) (93.60). The one-day 99% VaR in is therefore:

Code
(VaR.ew <- qnorm(0.99, 0, 1) * Psd)
         [,1]
[1,] 217.9751

In Hull (2015), this value is $217,757, which compares with $253,385, calculated using the historical simulation approach.

Code
Results.updated <- Results.updated %>%
  add_row(VaR.method = 
            "Model building equally weighted, sub-optimal weights", 
          VaR = VaR.ew) %>%
  arrange(VaR.method)
Results.updated
                                              VaR.method      VaR
1 Historical approximate quantile(), sub-optimal weights 218.3281
2             Historical simulation, sub-optimal weights 253.3850
3   Model building equally weighted, sub-optimal weights 217.9751

We are interested to compare the equally weighted approach with the EWMA (exponentially weighted moving average method).

Code
# Model building example -- EWMA
covEWMA <-
  function(factors, lambda = 0.96, return.cor = FALSE) {
    factor.names  = colnames(factors)
    t.factor      = nrow(factors)
    k.factor      = ncol(factors)
    factors       = as.matrix(factors)
    t.names       = rownames(factors)

    cov.f.ewma = array(,c(t.factor, k.factor, k.factor))
    cov.f = var(factors)  # unconditional variance as EWMA at time = 0
    FF = (factors[1, ] - mean(factors)) %*% t(factors[1,] - mean(factors))
    cov.f.ewma[1,,] = (1 - lambda) * FF  + lambda * cov.f
    for (i in 2:t.factor) {
    FF = (factors[i, ] - mean(factors)) %*% t(factors[i, ] - mean(factors))
    cov.f.ewma[i,,] = (1 - lambda) * FF  + lambda * cov.f.ewma[(i-1),,]
    }
    dimnames(cov.f.ewma) = list(t.names, factor.names, factor.names)

    if(return.cor) {
      cor.f.ewma = cov.f.ewma
      for (i in 1:dim(cor.f.ewma)[1]) {
        cor.f.ewma[i, , ] = cov2cor(cov.f.ewma[i, ,])
      }
      return(cor.f.ewma)
    } else {
      return(cov.f.ewma)
    }
  }

Let’s implement this approach to our problem. Covariance matrix on September 25, 2008, calculated using the EWMA method with \(\lambda=0.94\).

Code
Rdf <- as.data.frame(R)
C <- covEWMA(Rdf, lambda = 0.94)[500,,] 
Table23.7 <- C
Table23.7
                 RDJIA     RFTSE100       RCAC40        Nikkei
RDJIA     4.798561e-04 0.0004300820 0.0004255278 -3.988656e-05
RFTSE100  4.300820e-04 0.0010312613 0.0009629563  2.093380e-04
RCAC40    4.255278e-04 0.0009629563 0.0009535018  1.679592e-04
Nikkei   -3.988656e-05 0.0002093380 0.0001679592  2.538340e-04

The variance of portfolio losses is:

Code
(Pvar_EWMA <- t(alpha) %*% C %*% alpha)
         [,1]
[1,] 40977.52

In Hull (2015), this value is 40,995.765. The standard deviation is the square root of this, or:

Code
(Psd_EWMA <- Pvar_EWMA^0.5)
         [,1]
[1,] 202.4291

In Hull (2015), this value is 202.474. The one-day 99% VaR is therefore:

Code
(VaR.ewma <- qnorm(0.99, 0, 1) * Psd_EWMA)
         [,1]
[1,] 470.9204

In Hull (2015), this value is 471,025, over twice as high as the value given when returns are equally weighted.

Code
Results.updated <- Results.updated %>%
  add_row(VaR.method = "Model building EWMA, sub-optimal weights", 
          VaR = VaR.ewma) %>%
  arrange(VaR.method)
Results.updated
                                              VaR.method      VaR
1 Historical approximate quantile(), sub-optimal weights 218.3281
2             Historical simulation, sub-optimal weights 253.3850
3               Model building EWMA, sub-optimal weights 470.9204
4   Model building equally weighted, sub-optimal weights 217.9751

Let’s replicate Table 23.8. Volatilities (% per day) using equal weighting and EWMA.

Code
Table23.8 <- data.frame(Rsd * 100, diag(C)^0.5 * 100)
colnames(Table23.8) <- c("Equal weighting", "EWMA")
rownames(Table23.8) <- c("DJIA", "FTSE 100", "CAC 40", "Nikkei 225")
t(Table23.8)
                    DJIA FTSE 100   CAC 40 Nikkei 225
Equal weighting 1.108839 1.419145 1.397679   1.383159
EWMA            2.190562 3.211326 3.087882   1.593217

The estimated daily standard deviations are much higher when EWMA is used than when data are equally weighted. This is because volatilities were much higher during the period immediately preceding September 25, 2008, than during the rest of the 500 days covered by the data.

The estimated daily standard deviations are much higher when EWMA is used than when data are equally weighted. This is because volatilities were much higher during the period immediately preceding September 25, 2008, than during the rest of the 500 days covered by the data.

5.4 Optimal allocation.

The current sub-optimal portfolio weights are:

Code
W_hull <- alpha / 10000
W_hullt <- data.frame(W_hull)
rownames(W_hullt) <- c("DJIA", "FTSE 100", "CAC 40", "Nikkei 225")
t(W_hullt)
       DJIA FTSE 100 CAC 40 Nikkei 225
W_hull  0.4      0.3    0.1        0.2

Let’s calculate optimal weights.

Code
R <- R * 100
R.tomorrow <- R.tomorrow * 100
stocks<- c("DJIA", "FTSE100", "CAC40", "Nikkei")
sigma <- var(R)
sd <- diag(sigma)^0.5
E <- colMeans(R)
E.tomorrow <- colMeans(R.tomorrow)
ones <- abs(E / E)
a <- c(t(ones) %*% solve(sigma) %*% ones)
b <- c(t(ones) %*% solve(sigma) %*% E)
c <- c(t(E) %*% solve(sigma) %*% E)
d <- c(a * c - (b^2))
g <- c(solve(sigma) %*% (c * ones - b * E) / d)
h <- c(solve(sigma) %*% (a * E - b * ones) / d)
ER <- seq(from = -0.3, to = 0.3, by = 0.0001)
S <- ER
W <- matrix(0, nrow = length(ER), ncol = length(stocks))
for(i in 1:length(ER)){
  W[i,] <- g + h * ER[i]
  ER[i] <- W[i,] %*% E
  S[i] <- (t(W[i,]) %*% sigma %*% W[i, ])^0.5
  }
W_min <- (solve(sigma) %*% ones) / a
R_min <- c(t(W_min) %*% E)
R_min.realized <- c(t(W_min) %*% E.tomorrow)
S_min <- c(t(W_min) %*% sigma %*% W_min)^0.5
# Allocation to mimic the return of the CAC40 but with lower risk
W.cac40 <- g + h * E[3]
R_cac40 <- c(t(W.cac40) %*% E)
R_cac40.realized <- c(t(W.cac40) %*% E.tomorrow)
S_cac40 <- c(t(W.cac40) %*% sigma %*% W.cac40)^0.5
R_hull.realized = c(t(W_hull) %*% E.tomorrow)

Now we have four different portfolio weights alternatives. Two of them are optimal. Can we reduce the VaR by using optimal weights?

Code
W <- data.frame(W_hull, W_min, W.cac40, c(0.25, 0.25, 0.25, 0.25))
rownames(W) <- c("DJIA", "FTSE 100", "CAC 40", "Nikkei 225")
colnames(W) <- c("Hull, sub-optimal weights", 
                 "minimum variance", "CAC40 target return", 
                 "naive 1/N")
t(W)
                               DJIA    FTSE 100     CAC 40 Nikkei 225
Hull, sub-optimal weights 0.4000000  0.30000000 0.10000000  0.2000000
minimum variance          0.5592378  0.03793305 0.02279301  0.3800361
CAC40 target return       0.6075003 -0.40341894 0.45978629  0.3361323
naive 1/N                 0.2500000  0.25000000 0.25000000  0.2500000

In this Hull (2015) example we assume portfolio weights remains constant. However, we can rebalance portfolio weights. Here, we have the time evolution of minimum variance optimal weights when we consider 5 US industry portfolios as investment assets:

Code
embed_url("https://youtu.be/paH1-bDPsA4")

And a similar exercise but now with 10 US industry sorted portfolios:

Code
embed_url("https://youtu.be/DyjQbKm-NdE")

Let’s go back to the main Hull (2015) example and see the results graphically.

Code
W_hull <- alpha / 10000
R_hull <- c(t(W_hull) %*% E)
S_hull <- c(t(W_hull) %*% sigma %*% W_hull)^0.5

plot(S, ER, type = "l", lwd = 3, xlim = c(0.7, 1.6), 
     ylim = c(-0.022, 0.005),
     ylab = "Expected return", xlab = "Standard deviation")
points(diag(sigma)^0.5, E)
abline(0, R_hull / S_hull, lty = 3, col = "blue")
abline(0, R_min / S_min, lty = 3, col = "red")
points(S_min, R_min, col = "red", pch = 19, cex = 2)
abline(0, 0, lty = 3)
text(S_min, R_min, "Minimum
     variance", pos = 2, cex = 0.8)
text(sd, E, stocks, adj = -0.5, cex = 0.8)
points(S_hull, R_hull, col = "blue", pch = 19, cex = 2)
text(S_hull, R_hull, "Hull's 
     sub-optimal allocation", adj = -0.3, cex = 0.8, pos = 1)
legend("bottomleft", 
       legend = c("DJ=0.4,   FTSE=0.3,     CAC=0.1,     Nikkei=0.2",
                            "DJ=0.56, FTSE=0.038, CAC=0.022, Nikkei=0.38"),
       col = c("blue", "red"), pch = 19, bg = "white", cex = 1)

Figure 5.9: Hull’s allocation is sub-optimal, the red one is a better alternative, although with negative expected return.

The figure suggests that the (optimal) minimum variance alternative reduces the original portfolio risk. In fact, the minimum variance portfolio has a higher return and lower risk. This representation is an estimate of the future. We can evaluate what really happened by using the actual returns.

Code
(R_min.realized <- c(t(W_min) %*% E.tomorrow))
[1] 0.2629549
Code
(R_hull.realized = c(t(W_hull) %*% E.tomorrow))
[1] -0.5583249

This is interesting as it suggests that the sub-optimal allocation would lead to a realized return on Friday September 26, 2008 of -0.5583249%. On the other hand, following the optimal minimum variance allocation would lead to a portfolio gain of 0.2629549%. Hull’s sub-optimal portfolio allocation turns out to be problematic.

Code
W_hull <- alpha / 10000
R_hull <- c(t(W_hull) %*% E)
S_hull <- c(t(W_hull) %*% sigma %*% W_hull)^0.5

W_1N <- c(0.25,0.25,0.25,0.25)
R_1N <- c(t(W_1N) %*% E)
S_1N <- c(t(W_1N) %*% sigma %*% W_1N)^0.5

plot(S, ER, type = "l", lwd = 3, xlim = c(0.7, 1.6), 
     ylim = c(-0.022, 0.005),
     ylab = "Expected return", xlab = "Standard deviation")
points(diag(sigma)^0.5, E)
abline(0, R_hull / S_hull, lty = 3, col = "blue")
abline(0, R_min / S_min, lty = 3, col = "red")
abline(0, R_1N / S_1N, lty = 3, col = "purple")
points(S_min, R_min, col = "red", pch = 19, cex = 2)
points(S_1N, R_1N, col = "purple", pch = 19, cex = 2)
abline(0, 0, lty = 3)
text(S_min, R_min, "Minimum
     variance", pos = 2, cex = 0.8)
text(sd, E, stocks, adj = -0.5, cex = 0.8)
points(S_hull, R_hull, col = "blue", pch = 19, cex = 2)
text(S_hull, R_hull, "Hull's
     sub-optimal allocation", adj = -0.3, cex = 0.8, pos = 1)
text(S_1N, R_1N, "1/N alpha", adj = -0.3, cex = 0.8, pos = 3)
legend("bottomleft",
       legend = 
         c("Hull:    DJ=0.4,   FTSE=0.3,     CAC=0.1,     Nikkei=0.2",
"Minvar: DJ=0.56, FTSE=0.038, CAC=0.022, Nikkei=0.38",
"1/N:      DJ=0.25,  FTSE=0.25,  CAC=0.25,  Nikkei=0.25"),
       col = c("blue","red", "purple"), pch = 19, 
bg = "white", cex = 0.9)

Figure 5.10: Hull’s allocation is sub-optimal, the red one is a better alternative, although with negative expected return.

This Hull (2015) example is static. If we consider a dynamic approach, then optimal portfolio weights may change as new data is available. As a result, we end up with different mean-variance frontiers. This is the time evolution of the US efficient frontier assuming 5 industry sorted portfolio as investment assets:

Code
embed_url("https://youtu.be/Fq4UlUCneyI")

In general, as we increase the number of investment assets, the mean-variance frontier expands. Here is the case of 10 versus 12 US industry sorted portfolios as investment assets:

Code
embed_url("https://youtu.be/_GnJRviyYJI")

We can simulate 10,000 possible values for Friday September 26, 2008 given the mean and standard deviation estimates. The following histograms are generated as in this video:

Code
embed_url("https://youtu.be/P1Ky6-a8DE0")

First, the minimum variance portfolio:

Code
set.seed <- 1
M <- rnorm(10000, R_min, S_min)
hist(M, 100, xlim = c(-4, 4), ylim = c(0, 450), main = "")
abline(v = R_min.realized, col = "blue", lwd = 3)
abline(v = quantile(M, 0.025), lty = 2)
abline(v = quantile(M, 0.975), lty = 2)

Figure 5.11: Minimum variance recommendation and realized outcome in blue.

Then, the sub-optimal allocation:

Code
set.seed = 1
H = rnorm(10000, R_hull, S_hull)
hist(H, 100, xlim = c(-4, 4), ylim = c(0, 450), main = "")
abline(v = R_hull.realized, col = "blue", lwd = 3)
abline(v = quantile(H, 0.025), lty = 2)
abline(v = quantile(H, 0.975), lty = 2)

Figure 5.12: Hull sub-optimal recommendation and realized outcome.

And finally, the portfolio that targets the CAC 40 expected return:

Code
set.seed <- 1
Ca <- rnorm(10000, R_cac40, S_cac40)
hist(Ca, 100, xlim = c(-4, 4), ylim = c(0, 450), main = "")
abline(v = R_cac40.realized, col = "blue", lwd = 3)
abline(v = quantile(C, 0.025), lty = 2)
abline(v = quantile(C, 0.975), lty = 2)

Figure 5.13: CAC40 recommendation and realized outcome.

Let’s see the portfolio that targets the CAC 40 expected return in a mean-variance plot:

Code
plot(S, ER, type = "l", lwd = 3, xlim = c(0.7, 1.6),
     ylim = c(-0.022, 0.007), ylab = "Expected return",
     xlab = "Standard deviation")
points(diag(sigma)^0.5, E)
abline(0, 0, lty = 3)
points(S_cac40, R_cac40, col = "red", pch = 19, cex = 2)
points(0.68, R_cac40.realized, col = "red", pch = 15, cex = 2)
text(S_cac40, R_cac40, "CAC40
     expected return", pos = 2, cex = 0.8)
text(sd,E,stocks,adj=-0.5,cex=0.8)
W_hull = alpha / 10000
R_hull = c(t(W_hull) %*% E)
S_hull = c(t(W_hull) %*% sigma %*% W_hull)^0.5
points(S_hull, R_hull, col = "blue", pch = 19, cex = 2)
points(0.68, R_hull.realized, col = "blue", pch = 15, cex = 2)
abline(0,R_hull / S_hull, lty = 3, col = "blue")
abline(0,R_cac40 / S_cac40, lty = 3, col = "red")
abline(0,E[3] / sd[3], lty = 3)
abline(v = S_cac40, lty = 3)
text(S_hull, R_hull, "Hull's
     sub-optimal allocation", adj = -0.3, cex = 0.8, pos = 1)
legend("bottomleft", 
       legend = c("DJ=0.4,     FTSE=0.3,      CAC=0.1,     Nikkei=0.2",
                          "DJ=0.607, FTSE=-0.403, CAC=0.459, Nikkei=0.336"), col = c("blue", "red"), 
       pch = 19, bg = "white", cex = 0.9)

Figure 5.14: Estimation of a new optimal alpha: Replicate CAC40 ER.

These are the realized returns on Friday September 26, 2008.

Code
(R_min.realized)
[1] 0.2629549
Code
(R_hull.realized)
[1] -0.5583249
Code
(R_cac40.realized)
[1] 0.5183212

As stated before, Hull’s sub-optimal portfolio allocation turns out to be problematic. His recommendation would lead to a loss -0.558% and the optimal minimum variance would lead to a positive return 0.2629%, and the optimal portfolio that targets the CAC 40 expected return leads to an even higher return 0.518%.

5.5 Model building approach with optimal weights.

Now that we have two additional optimal portfolios, we can compute the corresponding VaR for the model building approach. First, the equally weighted case.

Code
# Model building approach, equally weighted.
alpha2 <- c(W_min * 10000)
alpha3 <- c(W.cac40 * 10000)
# This is the minimum variance optimal portfolio.
Pvar2 <- t(alpha2) %*% Rcov %*% alpha2
Psd2 <- Pvar2^0.5
VaR.ew.optimal <- qnorm(0.99, 0, 1) * Psd2
# This is optimal portfolio targeting the CAC 40 expected return.
Pvar3 <- t(alpha3) %*% Rcov %*% alpha3
Psd3 <- Pvar3^0.5
VaR.ew.optimal.cac <- qnorm(0.99, 0, 1) * Psd3

Summary of results:

Code
Results.updated <- Results.updated %>%
  add_row(VaR.method = 
        c("Model building equally weighted, minimum variance weights", 
          "Model building equally weighted, targeting CAC40"), 
          VaR = c(VaR.ew.optimal, VaR.ew.optimal.cac)) %>%
  arrange(VaR.method)
Results.updated
                                                 VaR.method      VaR
1    Historical approximate quantile(), sub-optimal weights 218.3281
2                Historical simulation, sub-optimal weights 253.3850
3                  Model building EWMA, sub-optimal weights 470.9204
4 Model building equally weighted, minimum variance weights 194.3892
5      Model building equally weighted, sub-optimal weights 217.9751
6          Model building equally weighted, targeting CAC40 203.7821

Second, the model building approach with EWMA.

Code
Pvar2_EWMA <- t(alpha2) %*% C %*% alpha2
Psd2_EWMA <- Pvar2_EWMA^0.5
VaR.ewma.optimal <- qnorm(0.99, 0, 1) * Psd2_EWMA
VaR.ewma.optimal
         [,1]
[1,] 338.2937
Code
Pvar3_EWMA <- t(alpha3) %*% C %*% alpha3
Psd3_EWMA <- Pvar3_EWMA^0.5
VaR.ewma.optimal.cac <- qnorm(0.99, 0, 1) * Psd3_EWMA
VaR.ewma.optimal.cac
         [,1]
[1,] 347.9537

Summary of results.

Code
Results.updated <- Results.updated %>%
  add_row(VaR.method = 
            c("Model building EWMA, minimum variance weights", 
              "Model building EWMA, targeting CAC40"), 
          VaR = c(VaR.ewma.optimal, VaR.ewma.optimal.cac)) %>%
  arrange(VaR.method)
Results.updated
                                                 VaR.method      VaR
1    Historical approximate quantile(), sub-optimal weights 218.3281
2                Historical simulation, sub-optimal weights 253.3850
3             Model building EWMA, minimum variance weights 338.2937
4                  Model building EWMA, sub-optimal weights 470.9204
5                      Model building EWMA, targeting CAC40 347.9537
6 Model building equally weighted, minimum variance weights 194.3892
7      Model building equally weighted, sub-optimal weights 217.9751
8          Model building equally weighted, targeting CAC40 203.7821

5.6 Historic approach with optimal weights.

Let’s go back to the historic approach. Now we can incorporate a comparison with the optimal weights.

Code
df.Adj.S <- df.Adj.S %>%
  mutate(p2 = (alpha2[1] * SDJIA / DJIA[501]) + 
             (alpha2[2] * SFTSE100 / AFTSE100[501]) + 
             (alpha2[3] * SCAC40 / ACAC40[501]) + 
             (alpha2[4] * SNikkei / ANikkei[501])) %>%
  mutate(l2 = 10000 - p2) %>% # Losses l
  mutate(p3 = (alpha3[1] * SDJIA / DJIA[501]) + 
             (alpha3[2] * SFTSE100 / AFTSE100[501]) + 
             (alpha3[3] * SCAC40 / ACAC40[501]) + 
             (alpha3[4] * SNikkei / ANikkei[501])) %>%
  mutate(l3 = 10000 - p3) # Losses l

Calculate the new portfolio losses for optimal weights.

Code
l2 <- df.Adj.S %>%
  filter(obs > 0 & obs < 501) %>%
  select(l2) %>%
  unlist()

l2.tomorrow <- df.Adj.S %>%
  filter(obs == 501) %>%
  select(l2) %>%
  unlist()

l3 <- df.Adj.S %>%
  filter(obs > 0 & obs < 501) %>%
  select(l3) %>%
  unlist()

l3.tomorrow <- df.Adj.S %>%
  filter(obs == 501) %>%
  select(l3) %>%
  unlist()

As a reference, this is the Hull (2015) original case.

Code
plot(l, type = "h", ylim = c(-600, 600), lwd = 2,
     xlab = "500 scenarios (sorted by historical date)",
ylab = "Portfolio gains (-) and losses (+)", 
col = ifelse(l < 0, "blue", "red"))
abline(0, 0)
abline(v = 0)
abline(h = max(l), lty = 2)
abline(h = min(l), lty = 2)

Figure 5.15: Losses with Hull’s suboptimal alpha DJ=0.4, FTSE=0.3, CAC=0.1, Nikkei=0.2.

The one-day 99% value at risk can be estimated as the fifth-worst loss.

Code
sort(l)[496]
   l487 
253.385 

Now, let’s see the historical approach but with minimum variance optimal weights.

Code
plot(l2, type = "h", ylim = c(-600, 600), lwd = 2,
     xlab = "500 scenarios (sorted by historical date)",
     ylab = "Portfolio gains (-) and losses (+)", 
     col = ifelse(l2 < 0, "blue", "red"))
abline(0, 0)
abline(v = 0)
abline(h = max(l2), lty = 2)
abline(h = min(l2), lty = 2)

Figure 5.16: Losses with optimal alpha DJ=0.56, FTSE=0.038, CAC=0.022, Nikkei=0.38.

See how the range of losses and gains is now reduced. The one-day 99% value at risk can be estimated as the fifth-worst loss.

Code
sort(l2)[496]
   l2487 
213.6898 

Let’s update our table of results.

Code
Results.updated <- Results.updated %>%
  add_row(VaR.method = "Historical simulation, minimum variance weights", 
          VaR = sort(l2)[496]) %>%
  arrange(VaR.method)
Results.updated
                                                 VaR.method      VaR
1    Historical approximate quantile(), sub-optimal weights 218.3281
2           Historical simulation, minimum variance weights 213.6898
3                Historical simulation, sub-optimal weights 253.3850
4             Model building EWMA, minimum variance weights 338.2937
5                  Model building EWMA, sub-optimal weights 470.9204
6                      Model building EWMA, targeting CAC40 347.9537
7 Model building equally weighted, minimum variance weights 194.3892
8      Model building equally weighted, sub-optimal weights 217.9751
9          Model building equally weighted, targeting CAC40 203.7821

And this is the optimal portfolio targeting the CAC 40 expected return.

Code
plot(l3, type = "h", ylim = c(-600, 600), lwd = 2,
     xlab = "500 scenarios (sorted by historical date)",
     ylab = "Portfolio gains (-) and losses (+)", 
     col = ifelse(l3 < 0, "blue", "red"))
abline(0, 0)
abline(v = 0)
abline(h = max(l3), lty = 2)
abline(h = min(l3), lty = 2)

Figure 5.17: Losses with CAC40 optimal alpha DJ=0.607, FTSE=-0.403, CAC=0.459, Nikkei=0.336.

Note that the range is reduced but allows for a higher gains. Let’s see these three together. The one-day 99% value at risk can be estimated as the fifth-worst loss.

Code
sort(l3)[496]
   l3494 
248.8909 

Let’s update the table of results.

Code
Results.updated <- Results.updated %>%
  add_row(VaR.method = "Historical simulation, targeting CAC40", 
          VaR = sort(l3)[496]) %>%
  arrange(VaR.method)
Results.updated
                                                  VaR.method      VaR
1     Historical approximate quantile(), sub-optimal weights 218.3281
2            Historical simulation, minimum variance weights 213.6898
3                 Historical simulation, sub-optimal weights 253.3850
4                     Historical simulation, targeting CAC40 248.8909
5              Model building EWMA, minimum variance weights 338.2937
6                   Model building EWMA, sub-optimal weights 470.9204
7                       Model building EWMA, targeting CAC40 347.9537
8  Model building equally weighted, minimum variance weights 194.3892
9       Model building equally weighted, sub-optimal weights 217.9751
10          Model building equally weighted, targeting CAC40 203.7821

Let’s see the three historical approaches together.

Code
par(mfrow=c(1, 3), oma = c(0, 0, 2, 0))
par(pty = "s")
plot(l, type = "h", ylim = c(-600, 600), lwd = 2, xlab = "",
     main = "Hull original case.", 
     ylab = "Portfolio gains (-) and losses (+)",
     col = ifelse(l < 0, "blue", "red"))
abline(0, 0)
abline(v = 0)
abline(h = max(l), lty = 2)
abline(h = min(l), lty = 2)
plot(l2, type = "h", ylim = c(-600, 600), lwd = 2,
     main = "Minimum variance.",
     xlab = "500 scenarios sorted by historical date.", ylab = "",
     col = ifelse(l2 < 0, "blue", "red"))
abline(0, 0)
abline(v = 0)
abline(h = max(l2), lty = 2)
abline(h = min(l2), lty = 2)
plot(l3, type = "h", ylim = c(-600, 600), lwd = 2, xlab = "",
     main = "Optimal targeting CAC 40.", ylab = "",
     col = ifelse(l3 < 0, "blue", "red"))
abline(0, 0)
abline(v = 0)
abline(h = max(l3), lty = 2)
abline(h = min(l3), lty = 2)

Figure 5.18: Hull’s suboptimal alpha (left); optimal alpha (center); optimal alpha CAC40 (right).

A quantile comparison.

Code
lq <- quantile(l, c(0.25, 0.5, 0.75))
lq2 <- quantile(l2, c(0.25, 0.5, 0.75))
lq3 <- quantile(l3, c(0.25, 0.5, 0.75))
ll <- data.frame(lq, lq2, lq3)
colours <- c("red", "orange", "blue")
Code
barplot(as.matrix(t(ll)), beside = TRUE, col = colours, 
        ylim = c(-60, 60), xlab = "Quantiles", 
        ylab = "Portfolio gains (-) and Losses (+)")
legend("topleft", c("Hull original","Optimal minvar",
                    "Replicating CAC40 ER"), cex = 1.3, bty = "n",
       fill = colours)

Figure 5.19: Quantile comparison of losses distribution.

This document took 4.21 minutes to compile in Quarto version 1.3.450, and R version 4.3.2 (2023-10-31 ucrt).

References.

Brealey, Richard A, Stewart C Myers, Franklin Allen, and Pitabas Mohanty. 2020. Principles of Corporate Finance. 13th ed. McGraw-Hill Education.
Hull, John C. 2015. Options, Futures, and Other Derivatives. 9th ed. Prentice Hall.
Knuth, Donald Ervin. 1984. “Literate Programming.” The Computer Journal 27 (2): 97–111.
R Core Team. 2024. R: A Language and Environment for Statistical Computing. Vienna, Austria: R Foundation for Statistical Computing. https://www.R-project.org/.