knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.path = "man/figures/README-", out.width = "100%" )
Tools for simple portfolio optimization:
Minimum variance portfolio for a given expected return, with no limits on allocations for individual asset classes other than that they must sum to 1. (That is, shorting and leverage are allowed.)
Minimum variance portfolio with lower and upper bounds on allocations for asset classes. It can also accommodate more-general linear constraints on asset classes. (For example, stocks plus bonds combined must be between 40% and 60% of the portfolio.)
Efficient frontier
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
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:
ersd
- a data frame with columns class (the asset class), er (expected return), and sd (standard deviation)cormat
- a correlation matrix for these assets. Thus, the number of rows and number of columns equal the number of asset classes. The row names and column names are the asset-class names.The data sets and their respective sources are:
stalebrink
-- Stalebrink, O. J. “Public Pension Funds and Assumed Rates of Return: An Empirical Examination of Public Sector Defined Benefit Pension Plans.” The American Review of Public Administration 44, no. 1 (January 1, 2014): 92–111. https://doi.org/10.1177/0275074012458826.
rvk
-- RVK. “Asset/Liability Study: Los Angeles Fire and Police Pension System.” RVK, Inc., October 2015.
horizon10year2017
-- “Survey of Capital Market Assumptions: 2017 Edition.” Horizon Actuarial Services, LLC, August 2017. http://www.horizonactuarial.com/uploads/3/0/4/9/30499196/horizon_cma_survey_2017_v0822.pdf.
The associated documents are in the "docs" folder of this project, on the github site
The examples below use these datasets.
Install as follows:
devtools::install_github("donboyd5/portopt")
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
# 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
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)
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")
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")
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"))
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$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")
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.