Skip to content
All library documents

Optimizing a Multi-Asset Portfolio of Long Call Options

Article Quant Q&A · Author: Lisa Ann

Summary

The document proposes allocating a limited budget across long call options on three correlated assets: an equity fund, government bonds, and gold. Its workflow estimates a covariance matrix with a multivariate GARCH model, uses that matrix to simulate correlated geometric Brownian motion paths, reprices calls at the simulated horizon, and optimizes portfolio weights using a modified Sharpe measure based on the mean return and losses in the lower tail. It frames the investment case around realized volatility exceeding the volatility embedded in option prices, while recognizing that option returns are nonlinear.

The text supplies a detailed R example, but no reported performance or validation results. It explicitly notes that actual option prices should be used; its illustration instead estimates prices with a Black–Scholes–Merton formula and GARCH volatility. The proposed setup has material modeling limits: geometric Brownian motion and forecast volatility may not represent changing distributions, and terminal repricing assumptions may not capture market behavior. The objective’s downside-risk denominator and risk-free-rate adjustment also merit careful review. The resulting weights are therefore an illustration of an optimization workflow, not evidence of a reliable allocation strategy.

Key ideas

  • The example allocates capital among calls on correlated equity, bond, and gold underlyings.
  • A multivariate GARCH forecast supplies covariance estimates for correlated price simulations.
  • The proposed objective optimizes a modified Sharpe measure using average returns and lower-tail losses.
  • Long calls have nonlinear payoffs, so underlying returns alone do not describe portfolio outcomes.
  • Model assumptions and estimated option prices limit the interpretation of the example weights.

Tags

Full text
# Call options portfolio: what would the underlyings' moments to be maximized?


# Call options portfolio: what would the underlyings' moments to be maximized?












Let you have only three underlyings, like SPY, TLT and GLD, and you want to buy $n_{1}$ Call options on SPY, $n_{2}$ Call options on TLT and $n_{3}$ Call options on GLD... with a limited budget, that is obvious.

You know those three underlyings, $S_{i}$ where $i=1,2,3$, are correlated as explained by $P$ correlation matrix and $V$ annualized covariance matrix (in case, with implied volatilities in main diagonal).

Being moneyness and maturity equal for each Call option written on each underlying, my goal is to find the value of $n_{1}, n_{2}$ and $n_{3}$ in order to make often good profits over time.

What was my step-by-step proceedings in such a case?

- Calculating pseudo-square root matrix of $V$, that is $\Sigma$, via eigenvalue decomposition;

- setting an horizon of 30 days, that is $T=30/365=0.082$ years;

- simulating 10,000 correlated multi-dimensional GBM stochastic processes with risk free rate drift, $\Sigma$ diffusion matrix and $T$ terminal time;

- calculating arithmetic returns separately for each underlying as $R_{T}=\frac{S_{T}}{S_{0}}-1$.

If I had bought the underlying securities, I would have linear P&L and I could choose my favorite performance (risk) measure to be maximized (minimized), like Omega, Expected Shortfall, Tail Dependance, Drawdown etc.; but in this case I'm dealing with options, which have not linear P&L due to convexity (Gamma).

This means you're looking for realized volatility (dispersion of $S_{T}$) higher than implied volatility regardless of weighted returns trend, because Call options already defend you against drawdowns and tail risk.

Hence my question is: making $n_{1}, n_{2}$ and $n_{3}$ varying over time, what's the measure I should maximize or minimize to

- maximize my returns;

- maximize my Sharpe;

- making my return shape distribution the best possible

...buying only Call options on those underlying securities?

Here is the `R` reproducible code to maximize long Call options modified Sharpe ratio. In the real world book's options prices should be used, but in this example I've used BMS formula with GARCH(1,1) volatility to estimate them and their change over time.

Please, tell me if you think there are theoretical and/or practical mistakes:

```
# ======================================================== #
# Managing options portfolio risk                          #
# Stochastic optimization of several correlated securities #
# By Lisa Ann for Quantitative Finance @ StackExchange                                                  #
# ======================================================== #

# Contents:

# 1. Loading packages
# 2. Downloading datas and preparing environment
# 3. Estimating orthogonal GARCH(1,1) covariance matrix
# 4. Simulating stochastic n-dimensional diffusion processes with 'n' equations
# 5. Correlated geometric Brownian motions returns
# 6. Pricing options at the end of simulation
# 7. Optimization of portfolio modified Sharpe

# *********************************
# 1. Loading packages
# *********************************

library(DEoptim)
library(quantmod)
library(RQuantLib)
library(Rsolnp)
library(rugarch)

# 'yuima' package from R-Forge

install.packages("yuima", repos="http://R-Forge.R-project.org")
library(yuima)

# You can download 'rmgarch' package from:
# https://rmgarch.googlecode.com/files/rmgarch_1.00-4.zip

library(rmgarch)

# *********************************
# 2. Downloading datas and preparing environment
# *********************************

# We will start with few assets which represent main asset classes: SPY for 
# equity, TLT for Government bonds and GLD for gold. We will add more assets
# later on

tickers <- c('SPY', 'TLT', 'GLD')
getSymbols(tickers, from = '1950-01-01')

# 3-Month London Interbank Offered Rate (LIBOR), based on U.S. Dollar

getSymbols('USD3MTD156N', src = 'FRED')

# Merging close-to-close arithmetic returns

xinit <- as.vector(tail(na.omit(merge(Cl(SPY), Cl(TLT), Cl(GLD))), 1))
data <- na.omit(merge(ClCl(SPY), ClCl(TLT), ClCl(GLD)))
xyplot(data)

# Risk free rate proxy

r.f <- as.numeric(tail(USD3MTD156N, 1) / 100)

# *********************************
# 3. Estimating orthogonal GARCH(1,1) covariance matrix
# *********************************

# We have to set our time horizon

n.ahead <- 30

# Covariance matrix forecast

spec <- gogarchspec(mean.model = list(model = 'constant'), 
                    variance.model = list(model = 'apARCH',
                                          garchOrder = c(1,1)),
                    distribution.model = 'manig')
fit <- gogarchfit(spec = spec, data = data)
forecast <- gogarchforecast(fit = fit, n.ahead = n.ahead)
V <- rcov(forecast)[[1]][,,n.ahead]
colnames(V) <- rownames(V) <- tickers
V

# *********************************
# 4. Simulating stochastic n-dimensional diffusion processes with 'n' equations
# *********************************

# Eigenvalue decomposition of coavariance matrix:
# a is the matrix for which we want to square root, then
# a.eig <- eigen(a)
# a.sqrt <- a.eig$vectors %*% diag(sqrt(a.eig$values)) %*% solve(a.eig$vectors)

s.eig <- eigen(V)
sigma.ch <- s.eig$vectors %*% diag(sqrt(s.eig$values)) %*% t(s.eig$vectors)
colnames(sigma.ch) <- rownames(sigma.ch) <- tickers
sigma.ch

# Stochastic differential equations

n <- ncol(data)
drift <- paste('r.f * x', 1:n, sep = '')
diffusion <- matrix(NA, n, n)

for(i in 1:n){
  for(j in 1:n){
    diffusion[i,j] <- paste(sigma.ch[i,j], ' * x', j, sep = '')
  }
}

object <- setModel(drift = drift, diffusion = diffusion, 
         solve.variable = paste('x', 1:n, sep = ''))

# Sampling grid properties: 'n.ahead' days and number of intervals

sampling <- setSampling(Terminal = n.ahead / 365, n = n.ahead * 1)

# Plotting simulations to check SDEs behaviour follows covariance matrix

plot(simulate(object = object, xinit = xinit, sampling = sampling), lwd = 2,
     type = 'o', col = 1:n)

# *********************************
# 5. Correlated geometric Brownian motions
# *********************************

# Starting values

S.0 <- matrix(xinit, nrow = 10000, ncol = n, byrow = TRUE)

# Terminal values

S.T <- matrix(NA, nrow = 10000, ncol = n)

for(i in 1:10000){
  for(j in 1:n){
    sim <- simulate(object = object, xinit = xinit, sampling = sampling)
    S.T[i,j] <- tail(sim@data@zoo.data[[j]], 1)
  }
}

colnames(S.T) <- tickers
par(mfrow = c(n,n))

for(j in 1:n){
  hist(S.T[,j], xlab = tickers[j], freq = FALSE,
       main = paste('Probability of', tickers[j], 'being at', 
                    expression(S(T)), n.ahead, 'periods from today'))
  lines(density(S.T[,j]), lwd = 2)
}

par(mfrow = c(1,1))

# *********************************
# 6. Pricing options at the end of simulation
# *********************************

# Now we have to price Call options at time 'T' to see true performances
# Estimating volatility of underlyings

spec <- ugarchspec(variance.model = list(model = 'sGARCH', garchOrder = c(1,1)),
                   mean.model = list(armaOrder = c(0,0)),
                   distribution.model = 'norm')

# Function to estimate underlying volatility

hv <- function(x, t, n.ahead){
  fit <- ugarchfit(spec = spec, data = x)
  forc <- ugarchforecast(fit, n.ahead = n.ahead)
  sigma <- forc@forecast$sigmaFor[t]
  return(sigma)
}

# Volatilities at '0' and at 'T'

sigma.0 <- apply(X = data, MARGIN = 2, FUN = hv, t = 1, 
                 n.ahead = n.ahead) * sqrt(252)
sigma.0.vec <- matrix(sigma.0, nrow = nrow(S.0), ncol = n, byrow = TRUE)
sigma.T <- apply(X = data, MARGIN = 2, FUN = hv, t = n.ahead, 
                 n.ahead = n.ahead) * sqrt(252)
sigma.T.vec <- matrix(sigma.T, nrow = nrow(S.T), ncol = n, byrow = TRUE)
barplot(rbind(sigma.0, sigma.T), beside = TRUE, legend.text = TRUE,
        ylab = 'Annualized Volatility')

# Options prices at 'S.0'
# We consider (ATM + 1) strikes for Call options

strike <- ceiling(S.0) + 1
call.0 <- matrix(NA, nrow = nrow(S.0), ncol = n)

for(i in 1:nrow(S.0)){
  for(j in 1:n){
    call.0[i,j] <- EuropeanOption(type = 'call', underlying = S.0[i,j], 
                                  strike = strike[i,j], dividendYield = r.f, 
                                  riskFreeRate = r.f, 
                                  maturity = (n.ahead * 2) / 365,
                                  volatility = sigma.0.vec[i,j])$value  
  }
}

# Options prices at 'S.T'

call.T <- matrix(NA, nrow = nrow(S.T), ncol = n)

for(i in 1:nrow(S.T)){
  for(j in 1:n){
    call.T[i,j] <- EuropeanOption(type = 'call', underlying = S.T[i,j], 
                                  strike = strike[i,j], dividendYield = r.f, 
                                  riskFreeRate = r.f, maturity = n.ahead / 365,
                                  volatility = sigma.T.vec[i,j])$value    
  }
}

# Call buyer returns

call.r <- call.T / call.0 - 1

# *********************************
# 7. Optimization of portfolio modified Sharpe
# *********************************

# Call weighted returns function

call.w <- function(w){
  ret <- call.r %*% w
  mu <- mean(ret)
  q <- quantile(ret, probs = .05)
  risk <- mean(ret[ret < q])
  return(- (mu - r.f) / risk)
}

# Optimization

par <- DEoptim(fn = call.w, lower = rep(10, n), upper = rep(100, n),
               control = DEoptim.control(strategy = 6, 
                                         itermax = 1000))$optim$bestmem
par / sum(par)
round(solnp(pars = runif(3, 0, 1), fun = call.w, eqfun = function(w){sum(w)},
            eqB = 1, LB = rep(0, n), UB = rep(1, n), 
            control = list(innter.iter = 10000))$pars, 4)
```

At the end of optimization, my budget is allocated on SPY, TLT and GLD according to optmal weights.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.