Portfolio Optimization
Introduction
In this example, we solve the Markowitz portfolio problem under various constraints (Markowitz 1952; Roy 1952; Lobo, Fazel, and Boyd 2007).
We have \(n\) assets or stocks in our portfolio and must determine the amount of money to invest in each. Let \(w_i\) denote the fraction of our budget invested in asset \(i = 1,\ldots,m\), and let \(r_i\) be the returns (, fractional change in price) over the period of interest. We model returns as a random vector \(r \in {\mathbf R}^n\) with known mean \({\mathop{\bf E{}}}[r] = \mu\) and covariance \({\mathop{\bf Var{}}}(r) = \Sigma\). Thus, given a portfolio \(w \in {\mathbf R}^n\), the overall return is \(R = r^Tw\).
Portfolio optimization involves a trade-off between the expected return \({\mathop{\bf E{}}}[R] = \mu^Tw\) and associated risk, which we take as the return variance \({\mathop{\bf Var{}}}(R) = w^T\Sigma w\). Initially, we consider only long portfolios, so our problem is \[ \begin{array}{ll} \underset{w}{\mbox{maximize}} & \mu^Tw - \gamma w^T\Sigma w \\ \mbox{subject to} & w \geq 0, \quad \sum_{i=1}^n w = 1 \end{array} \] where the objective is the risk-adjusted return and \(\gamma > 0\) is a risk aversion parameter.
Example
We construct the risk-return trade-off curve for \(n = 10\) assets and \(\mu\) and \(\Sigma^{1/2}\) drawn from a standard normal distribution.
## Problem data
set.seed(10)
n <- 10
SAMPLES <- 100
mu <- matrix(abs(rnorm(n)), nrow = n)
Sigma <- matrix(rnorm(n^2), nrow = n, ncol = n)
Sigma <- t(Sigma) %*% Sigma
## Form problem
w <- Variable(n)
ret <- t(mu) %*% w
risk <- quad_form(w, Sigma)
constraints <- list(w >= 0, sum(w) == 1)
## Risk aversion parameters
gammas <- 10^seq(-2, 3, length.out = SAMPLES)
ret_data <- rep(0, SAMPLES)
risk_data <- rep(0, SAMPLES)
w_data <- matrix(0, nrow = SAMPLES, ncol = n)
## Compute trade-off curve
for(i in seq_along(gammas)) {
gamma <- gammas[i]
objective <- ret - gamma * risk
prob <- Problem(Maximize(objective), constraints)
result <- solve(prob)
## Evaluate risk/return for current solution
risk_data[i] <- result$getValue(sqrt(risk))
ret_data[i] <- result$getValue(ret)
w_data[i,] <- result$getValue(w)
}
Note how we can obtain the risk and return by directly evaluating the value of the separate expressions:
result$getValue(risk)
result$getValue(ret)
The trade-off curve is shown below. The \(x\)-axis represents the standard deviation of the return. Red points indicate the result from investing the entire budget in a single asset. As \(\gamma\) increases, our portfolio becomes more diverse, reducing risk but also yielding a lower return.
cbPalette <- brewer.pal(n = 10, name = "Paired")
p1 <- ggplot() +
geom_line(mapping = aes(x = risk_data, y = ret_data), color = "blue") +
geom_point(mapping = aes(x = sqrt(diag(Sigma)), y = mu), color = "red")
markers_on <- c(10, 20, 30, 40)
nstr <- sprintf("gamma == %.2f", gammas[markers_on])
df <- data.frame(markers = markers_on, x = risk_data[markers_on],
y = ret_data[markers_on], labels = nstr)
p1 + geom_point(data = df, mapping = aes(x = x, y = y), color = "black") +
annotate("text", x = df$x + 0.2, y = df$y - 0.05, label = df$labels, parse = TRUE) +
labs(x = "Risk (Standard Deviation)", y = "Return")
We can also plot the fraction of budget invested in each asset.
w_df <- data.frame(paste0("grp", seq_len(ncol(w_data))),
t(w_data[markers_on,]))
names(w_df) <- c("grp", sprintf("gamma == %.2f", gammas[markers_on]))
tidyW <- gather(w_df, key = "gamma", value = "fraction", names(w_df)[-1], factor_key = TRUE)
ggplot(data = tidyW, mapping = aes(x = gamma, y = fraction)) +
geom_bar(mapping = aes(fill = grp), stat = "identity") +
scale_x_discrete(labels = parse(text = levels(tidyW$gamma))) +
scale_fill_manual(values = cbPalette) +
guides(fill = "none") +
labs(x = "Risk Aversion", y = "Fraction of Budget")
## Testthat Results: No output is good
Discussion
Many variations on the classical portfolio problem exist. For instance, we could allow long and short positions, but impose a leverage limit \(\|w\|_1 \leq L^{max}\) by changing
constr <- list(p_norm(w,1) <= Lmax, sum(w) == 1)
An alternative is to set a lower bound on the return and minimize just
the risk. To account for transaction costs, we could add a term to the
objective that penalizes deviations of \(w\) from the previous
portfolio. These extensions and more are described in
Boyd et al. (2017). The key takeaway is that all of these convex
problems can be easily solved in CVXR
with just a few alterations
to the code above.
Session Info
sessionInfo()
## R version 4.4.2 (2024-10-31)
## Platform: x86_64-apple-darwin20
## Running under: macOS Sequoia 15.1
##
## Matrix products: default
## BLAS: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRblas.0.dylib
## LAPACK: /Library/Frameworks/R.framework/Versions/4.4-x86_64/Resources/lib/libRlapack.dylib; LAPACK version 3.12.0
##
## locale:
## [1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
##
## time zone: America/Los_Angeles
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices datasets utils methods base
##
## other attached packages:
## [1] tidyr_1.3.1 RColorBrewer_1.1-3 ggplot2_3.5.1 CVXR_1.0-15
## [5] testthat_3.2.1.1 here_1.0.1
##
## loaded via a namespace (and not attached):
## [1] gmp_0.7-5 clarabel_0.9.0.1 sass_0.4.9 utf8_1.2.4
## [5] generics_0.1.3 slam_0.1-54 blogdown_1.19 lattice_0.22-6
## [9] digest_0.6.37 magrittr_2.0.3 evaluate_1.0.1 grid_4.4.2
## [13] bookdown_0.41 pkgload_1.4.0 fastmap_1.2.0 rprojroot_2.0.4
## [17] jsonlite_1.8.9 Matrix_1.7-1 brio_1.1.5 Rmosek_10.2.0
## [21] purrr_1.0.2 fansi_1.0.6 scales_1.3.0 codetools_0.2-20
## [25] jquerylib_0.1.4 cli_3.6.3 Rmpfr_0.9-5 rlang_1.1.4
## [29] Rglpk_0.6-5.1 bit64_4.5.2 munsell_0.5.1 withr_3.0.2
## [33] cachem_1.1.0 yaml_2.3.10 tools_4.4.2 osqp_0.6.3.3
## [37] Rcplex_0.3-6 rcbc_0.1.0.9001 dplyr_1.1.4 colorspace_2.1-1
## [41] gurobi_11.0-0 assertthat_0.2.1 vctrs_0.6.5 R6_2.5.1
## [45] lifecycle_1.0.4 bit_4.5.0 desc_1.4.3 cccp_0.3-1
## [49] pkgconfig_2.0.3 bslib_0.8.0 pillar_1.9.0 gtable_0.3.6
## [53] glue_1.8.0 Rcpp_1.0.13-1 highr_0.11 xfun_0.49
## [57] tibble_3.2.1 tidyselect_1.2.1 knitr_1.48 farver_2.1.2
## [61] htmltools_0.5.8.1 labeling_0.4.3 rmarkdown_2.29 compiler_4.4.2