knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.path = "man/figures/README-",
  out.width = "100%"
)

portopt

Tools for simple portfolio optimization:

portopt cannot handle nonlinear constraints on asset allocations, and it cannot find an optimal portfolio that satisfies more-complex objectives, such as value-at-risk criteria. For my purposes, so far, these are not necessary. portopt uses solve.QP from the package quadprog for minimum-variance optimization. Other solvers would be required for more-complex optimization.

I wrote portopt because I was looking for basic portfolio optimization tools in R. There are many available packages, but the package that looked like it would be most useful, PortfolioAnalytics, recently was removed from CRAN although I have installed it from github. It seems very powerful. fPortfolio also seems powerful. I understand the basics of how to use them when we are starting with raw data - historical data on asset returns - but I haven't figured out how to use them when we only want to use summary measures to construct an efficient frontier, by using already-available expected returns and standard deviations by asset classs, plus the correlation matrix. I am sure PortfolioAnalytics and fPortfolio can do that but I haven't figured it out. At some point I may abandon this package in favor of one of these two packages.

portopt data sets

portopt includes several data sets, each with expected returns, standard deviations, and a correlation matrix for multiple asset classes. These data sets are used in the examples below. Each data set is a list with two elements:

The data sets and their respective sources are:

The associated documents are in the "docs" folder of this project, on the github site

The examples below use these datasets.

Installation

Install as follows:

devtools::install_github("donboyd5/portopt")

Examples

Get basic information about a dataset

library("portopt")
library("plyr")
library("magrittr")
library("Matrix")
library("quadprog")
library("tidyverse")

stalebrink
aa.wts <- c(.25, .25, .2, .15, .1, .05) # asset-allocation weights
per(stalebrink$ersd$er, aa.wts) # portfolio expected return with these weights
psd(stalebrink$cormat, stalebrink$ersd$sd, aa.wts) # portfolio standard deviation with these weights

Get the minimum variance portfolio for a given expected return

# Create several minimum-variance portfolios

# no restrictions on asset allocation - shorting and leverage allowed
minvport(.09, stalebrink$ersd, stalebrink$cormat)$portfolio

# shorting and leverage NOT allowed:
minvport(.09, stalebrink$ersd, stalebrink$cormat, 0, 1)$portfolio

# shorting and leverage NOT allowed, 40% upper bound on real estate:
minvport(.09, stalebrink$ersd, stalebrink$cormat, 0, c(1, 1, 1, .4, 1, 1))$portfolio

Check whether correlation matrix is positive semi-definite

A correlation matrix must be positive semi-definite but sometimes matrices estimated from sample data are not, especially if the correlations are pairwise and do not all have the same number of observations. This could result in a negative variance, which is not allowed.

We can adjust an intended correlation matrix that is not proper (not positive semi-definite) to get the nearest proper matrix, using makePDcorr (which uses the function nearPD from the package Matrix). However, in the example below, the differences between the original rvk correlation matrix and the adjusted matrix appear quite large, which is disconcerting.

This requires investigation when time allows.

is.PSD(stalebrink$cormat) # good

is.PSD(rvk$cormat) # bad
cormat2 <- makePDcorr(rvk$cormat)
is.PSD(cormat2)
# compare:
rvk$cormat %>% round(., 2)
cormat2 %>% round(., 2)
(cormat2 - rvk$cormat) %>% round(., 2)

Get and graph the efficient frontier for Stalebrink assumptions

ds <- stalebrink

ef.nobound <- efrontier(seq(.00, .30, .0025), ds$ersd, ds$cormat)
ef.noshort <- efrontier(seq(.00, .30, .0025), ds$ersd, ds$cormat, 0, 1)

# create a data frame with just the data we want
efdf <- bind_rows(ef.nobound$efrontier %>% mutate(rule="no bounds on allocation"),
                  ef.noshort$efrontier %>% mutate(rule="no shorts or leverage")) %>%
  filter(type=="high", !is.na(per))

ggplot() +
  geom_line(data=efdf, aes(psd, per, colour=rule)) +
  scale_x_continuous(name="Standard deviation", breaks=seq(0, .5, .025), limits=c(0, .27), labels = scales::percent) +
  scale_y_continuous(name="Expected return", breaks=seq(0, .5, .01), limits=c(0, .16), labels = scales::percent) +
  ggtitle("Efficient frontier for the Stalebrink capital market assumptions", subtitle="With and without bounds on asset allocations") +
  # now add the asset-class points
  geom_point(data=ef.nobound$ersd, aes(x=sd, y=er)) +
  geom_text(data=ef.nobound$ersd, aes(x=sd, y=er, label=class), nudge_y = +.003) +
  geom_hline(yintercept = 0) +
  geom_vline(xintercept = 0) +
  geom_hline(yintercept = .075, linetype="dashed")

Repeat for the Horizon 2017 10-year assumptions

ds <- horizon10year2017

ef.nobound <- efrontier(seq(.00, .30, .0025), ds$ersd, ds$cormat)
ef.noshort <- efrontier(seq(.00, .30, .0025), ds$ersd, ds$cormat, 0, 1)

# create a data frame with just the data we want
efdf <- bind_rows(ef.nobound$efrontier %>% mutate(rule="no bounds on allocation"),
                  ef.noshort$efrontier %>% mutate(rule="no shorts or leverage")) %>%
  filter(type=="high", !is.na(per))

ggplot() +
  geom_line(data=efdf, aes(psd, per, colour=rule)) +
  scale_x_continuous(name="Standard deviation", breaks=seq(0, .5, .025), limits=c(0, .27), labels = scales::percent) +
  scale_y_continuous(name="Expected return", breaks=seq(0, .5, .01), limits=c(0, .16), labels = scales::percent) +
  ggtitle("Efficient frontier for the Horizon 2017 average 10-year capital market assumptions", subtitle="With and without bounds on asset allocations") +
  # now add the asset-class points
  geom_point(data=ef.nobound$ersd, aes(x=sd, y=er)) +
  geom_text(data=ef.nobound$ersd, aes(x=sd, y=er, label=class), nudge_y = +.003) +
  geom_hline(yintercept = 0) +
  geom_vline(xintercept = 0) +
  geom_hline(yintercept = .075, linetype="dashed")

Asset weights for the Stalebrink assumtions

ds <- stalebrink

ef.nobound <- efrontier(seq(.00, .30, .0025), ds$ersd, ds$cormat)
ef.noshort <- efrontier(seq(.00, .30, .0025), ds$ersd, ds$cormat, 0, 1)

# Examine asset class weights for different classes
ef.noshort$weights %>%
  filter(class %in% c("stocks", "cash", "bonds.dom")) %>%
  ggplot(aes(er.target, asset.weight, colour=class)) +
  geom_line() +
  ggtitle("Stalebrink weights with no shorting or leverage")

# Compare weights for an asset class, optimized with and without bounds
aclass <- "stocks"
wts <- bind_rows(ef.nobound$weights %>% mutate(rule="no bounds on allocation"),
                 ef.noshort$weights %>% mutate(rule="no shorts or leverage")) %>%
       filter(class==aclass)

wts %>%
       ggplot(aes(er.target, asset.weight, colour=rule)) +
       geom_line() +
       geom_hline(yintercept=0) +
       scale_x_continuous(breaks=seq(0, 1, .05)) +
       ggtitle(paste0(aclass, " weights, Stalebrink assumptions"))

Compare the Stalebrink and Horizon 2017 no-shorts/no-leverage frontiers

sb <- efrontier(seq(.00, .30, .0025), stalebrink$ersd, stalebrink$cormat, 0, 1)
hzn <- efrontier(seq(.00, .30, .0025), horizon10year2017$ersd, horizon10year2017$cormat, 0, 1)

# create a data frame with just the data we want
efdf <- bind_rows(sb$efrontier %>% mutate(data="Stalebrink"),
                  hzn$efrontier %>% mutate(data="Horizon")) %>%
  filter(type=="high", !is.na(per))

ggplot() +
  geom_line(data=efdf, aes(psd, per, colour=data)) +
  scale_x_continuous(name="Standard deviation", breaks=seq(0, .5, .025), limits=c(0, .27), labels = scales::percent) +
  scale_y_continuous(name="Expected return", breaks=seq(0, .5, .01), limits=c(0, .16), labels = scales::percent) +
  ggtitle("Efficient frontiers for the Stalebrink and Horizon 2017 average 10-year capital market assumptions",
          subtitle="No shorting or leverage allowed") +
  geom_hline(yintercept = 0) +
  geom_vline(xintercept = 0) +
  geom_hline(yintercept = .075, linetype="dashed")

Stalebrink with limits on asset allocations

# stalebrink$ersd$class # "stocks"     "bonds.dom"  "bonds.intl" "re"         "alts"       "cash"
lims.lb <- c(.3, .1, .1, .1, .1, .1)
lims.ub <- c(.8, .4, .4, .3, .3, .3)
sb <- efrontier(seq(.00, .30, .0025), stalebrink$ersd, stalebrink$cormat, 0, 1)
sb2 <- efrontier(seq(.00, .30, .0025), stalebrink$ersd, stalebrink$cormat, lims.lb, lims.ub)

# create a data frame with just the data we want
efdf <- bind_rows(sb$efrontier %>% mutate(data="Stalebrink"),
                  sb2$efrontier %>% mutate(data="Stalebrink limits")) %>%
  filter(type=="high", !is.na(per))

ggplot() +
  geom_line(data=efdf, aes(psd, per, colour=data)) +
  scale_x_continuous(name="Standard deviation", breaks=seq(0, .5, .025), limits=c(0, .27), labels = scales::percent) +
  scale_y_continuous(name="Expected return", breaks=seq(0, .5, .01), limits=c(0, .16), labels = scales::percent) +
  ggtitle("Efficient frontiers for two Stalebrink rules",
          subtitle="No shorting or leverage allowed") +
  geom_hline(yintercept = 0) +
  geom_vline(xintercept = 0) +
  geom_hline(yintercept = .075, linetype="dashed")


donboyd5/portopt documentation built on May 20, 2019, 2:58 p.m.