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

Source

R Markdown

References

Boyd, S., E. Busseti, S. Diamond, R. N. Kahn, K. Koh, P. Nystrup, and J. Speth. 2017. “Multi-Period Trading via Convex Optimization.” Foundations and Trends in Optimization.
Lobo, M. S., M. Fazel, and S. Boyd. 2007. “Portfolio Optimization with Linear and Fixed Transaction Costs.” Annals of Operations Research 152 (1): 341–65.
Markowitz, H. M. 1952. “Portfolio Selection.” Journal of Finance 7 (1): 77–91.
Roy, A. D. 1952. “Safety First and the Holding of Assets.” Econometrica 20 (3): 431–49.