Quantitative Analytics: Optimal Portfolio Allocation
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
Introduction:
The literature in portfolio optimisation has been around for decades. In this post I cover a number of traditional portfolio optimisation models. The general aim is to select a portfolio of assets out of a set of all possible portfolios being considered with a defined objective function.
The data:
The data is collected using the tidyquant()
package’s tq_get()
function. I then convert the daily asset prices to daily log returns using the periodReturn
function from the quantmod()
package. Next I construct lists
of 6 months worth of daily returns using the rolling_origin()
function from the rsample()
package. The objective is to compute on a rolling basis the 6 month mean returns mus
and the 6 month covariance matrices Sigmas
on the training sets (i.e. 6 months) and apply them on the test sets (i.e. 1 month later) – monthly rebalancing.
Just as with the returns data, the same is applied to the monthly prices data.
start_date <- "2013-01-01" end_date <- "2017-08-31" symbols <- c("AAPL", "ABBV", "A", "APD", "AA", "CF", "NVDA", "HOG", "WMT", "AMZN" #,"MSFT", "F", "INTC", "ADBE", "AMG", "AKAM", "ALB", "ALK" ) portfolio_prices <- tq_get( symbols, from = start_date, to = end_date, ) %>% select(symbol, date, adjusted) portfolio_monthly_returns <- portfolio_prices %>% group_by(symbol) %>% tq_transmute( select = adjusted, mutate_fun = periodReturn, period = "daily", type = "log", ) %>% pivot_wider(names_from = symbol, values_from = daily.returns) %>% mutate(year_month = yearmonth(date)) %>% nest(-year_month) %>% rolling_origin( initial = 6, assess = 1, skip = 0, cumulative = FALSE) portfolio_monthly_prices <- portfolio_prices %>% pivot_wider(names_from = symbol, values_from = adjusted) %>% mutate(year_month = yearmonth(date)) %>% nest(-year_month) %>% rolling_origin( initial = 6, assess = 1, skip = 0, cumulative = FALSE) ListNamesDates <- map(portfolio_monthly_returns$splits, ~assessment(.x) %>% select(year_month)) %>% plyr::ldply(., data.frame) %>% pull(year_month) ######### # Goal is to define the mean and covariance matrix on the training sets and apply them on the test sets - monthly rebalancing mus <- map(portfolio_monthly_returns$splits, ~ analysis(.x) %>% unnest(data) %>% select(-year_month, -date) %>% colMeans) %>% setNames(c(ListNamesDates)) Sigmas <- map(portfolio_monthly_returns$splits, ~ analysis(.x) %>% unnest() %>% tk_xts(., date_var = date) %>% cov(.)) %>% setNames(c(ListNamesDates))
Price and Returns data
The returns data for the first split
looks like the following:
AAPL | ABBV | A | APD | AA | CF | NVDA | HOG | WMT | AMZN |
---|---|---|---|---|---|---|---|---|---|
0.000000000 | 0.000000000 | 0.000000000 | 0.0000000000 | 0.000000000 | 0.000000000 | 0.0000000000 | 0.000000000 | 0.0000000000 | 0.0000000000 |
-0.012702708 | -0.008291331 | 0.003575412 | -0.0035002417 | 0.008859253 | -0.004740037 | 0.0007860485 | -0.020819530 | -0.0063746748 | 0.0045367882 |
-0.028249939 | -0.012713395 | 0.019555411 | 0.0133515376 | 0.020731797 | 0.022152033 | 0.0324601827 | -0.005529864 | 0.0037720098 | 0.0025886574 |
-0.005899428 | 0.002033327 | -0.007259372 | -0.0009230752 | -0.017429792 | -0.003753345 | -0.0293228286 | 0.006958715 | -0.0096029606 | 0.0352948721 |
0.002687379 | -0.022004791 | -0.008022622 | 0.0018451781 | 0.000000000 | -0.014769161 | -0.0221704307 | -0.003063936 | 0.0027737202 | -0.0077780140 |
-0.015752246 | 0.005620792 | 0.026649604 | 0.0133906716 | -0.002200109 | 0.034376623 | -0.0226730363 | 0.038724890 | -0.0002916231 | -0.0001126237 |
The statistics
The Mus
(mean returns) data looks like:
|
The Sigmas
(covariance matrix) data looks like:
|
Comparing Portfolio Optimisation
Global Minimum Variance Portfolio
The global minimum-variance portfolio \(w^{gmv}\) is a portfolio of assets with gives us the lowest possible return variance or portfolio volatility. Volatility here is used as a replacement for risk, thus with less variance in volatility correlates to less risk in an asset. The portfolio focuses only on risk and ignores expected returns.
The objective function is;
\[\begin{eqnarray} \min_{w} w^{T} \Sigma w \\ \text{subject to } 1^{T}w = 1 \\ w \ge 0 \end{eqnarray}\]
Since \(\Sigma\) is unknown we can estimate it as \(\hat{\Sigma}\) with the covariance matrix. In which the convex solution becomes:
\[W = \dfrac{1}{1^{T}\hat{\Sigma}^{-1}1}\hat{\Sigma}^{-1}1\] The objective is that we want to find the optimial weights from the model such that our risk is minimised.
The below problem consists of our \(Minimisation\) problem \(\min_{w} w^{T} \Sigma w\). The quad_form
function takes the quadratic form \(x^T Px\) where \(x\) is a vector and \(P\) is a matrix or in our case \(w\) is our weights vector and \(\Sigma\) is the covariance matrix for \(A_{1}, \dots, A_{5}\). The constraints
correspond to \(1^{T}w = 1\) in which we cannot assign negative weights to our assets and that we invest all our capital in the portfolio.
We can use the Disciplined Convex Programming (CVXR
) package in R, which;
Analyses the problem,
Verifies the convexity,
Converts the problem into canonical form,
Solves the problem.
We want to find the optimial weights from the model such that our risk is minimised. We can do this by solving the optimisation problem, bind the lists into a single data frame and use ggplot2
to plot the rolling one month out of sample optimal portfolio weights – based on the previous 6 months rolled mus
and Sigmas
.
# 1) Function: Global Minimum Variance Portfolio GMVPportolioFunction <- function(Sigma) { w <- Variable(nrow(Sigma)) problem <- Problem( # Initialise the problem Minimize( # minimse or maximise objective function quad_form( w, Sigma # Model inputs, the number of weights to consider and the covariance matrix ) ), constraints = list( w >= 0, # First model constraint sum(w) == 1)) # Second model constrain Solution <- solve(problem, solver="SCS") # Solves the problem return(as.vector(Solution$getValue(w))) } # 1a) Portfolio GMVP GMVPPortfolio <- map( .x = Sigmas, GMVPportolioFunction) # 1b) Portfolio GMVP GMVPPortfolioWeights <- setNames(GMVPPortfolio, ListNamesDates) %>% map(., ~setNames(., c(symbols))) %>% map_dfr(., ~bind_rows(.), .id = "date") %>% mutate(date = as.Date(paste(date, "01"), format = "%Y %b %d")) GMVPPortfolioWeights %>% head() %>% kable() %>% kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
date | AAPL | ABBV | A | APD | AA | CF | NVDA | HOG | WMT | AMZN |
---|---|---|---|---|---|---|---|---|---|---|
2013-07-01 | 0.0600841 | 0.0335474 | 0.0000000 | 0.1487694 | 0.1258906 | 0.0727919 | 0.0095632 | 0.0000000 | 0.5208891 | 0.0284642 |
2013-08-01 | 0.1046817 | 0.0404000 | -0.0000009 | 0.0917764 | 0.1592917 | 0.0299732 | 0.0364528 | 0.0000000 | 0.5374240 | 0.0000007 |
2013-09-01 | 0.1183895 | 0.0000005 | 0.0000006 | 0.0844453 | 0.1683525 | 0.0246108 | 0.0635568 | 0.0000002 | 0.5406430 | 0.0000004 |
2013-10-01 | 0.0975699 | 0.0000000 | 0.0024138 | 0.0742436 | 0.1166938 | 0.0431301 | 0.0968247 | 0.0000000 | 0.5691240 | 0.0000000 |
2013-11-01 | 0.1480922 | 0.0000003 | 0.0620406 | 0.0449223 | -0.0000003 | 0.0279872 | 0.1613034 | 0.0376023 | 0.5014186 | 0.0166322 |
2013-12-01 | 0.1135468 | 0.0000007 | 0.0678773 | 0.0560224 | -0.0000005 | 0.0062812 | 0.1195510 | 0.0779063 | 0.5588149 | 0.0000001 |
# 1c) Portfolio GMVP GMVPPortfolioWeights %>% select(date, everything()) %>% pivot_longer(cols = 2:ncol(.), values_to = "weights") %>% ggplot(aes(fill = name, y = weights, x = date)) + geom_bar(position = "stack", stat = "identity", width = 100) + scale_fill_viridis(option = "magma", discrete = TRUE, name = "Asset") + theme_bw() + ggtitle("GMVP Rolling Portfolio Adjustments") + xlab("Date") + ylab("Weights")
Markowitz Portfolio
The Markowitz Mean-Variance Portfolio is constructed as follows:
\[\begin{eqnarray} \max_{w} \mu^{T}w – \lambda w^{T}\Sigma w \\ \text{subject to } 1^{T}w = 1 \\ w \ge 0 \end{eqnarray}\]
We can set different risk parameters by adjusting \(\lambda\) and see how the returns are affected. This can be done by running multiple optimisation problems on the data with different \(\lambda\) values. Higher \(\lambda\) values places emphasis on the right hand side of the equation and thus the more risk adverse the investor is.
# 2) Function: Markowitz Portfolio MarkowitzportolioFunction <- function(mu, Sigma, lmd) { w <- Variable(nrow(Sigma)) problem <- Problem( Maximize(t(mu) %*% w - lmd*quad_form(w, Sigma)), constraints = list( w >= 0, sum(w) == 1) ) Solution <- solve(problem) return(as.vector(Solution$getValue(w))) } # 2a) Portfolio Markowitz lmd <- c(0.25, 0.5, 0.75) MarkowitzPortfolio <- map(lmd, ~map2( .x = mus, .y = Sigmas, MarkowitzportolioFunction, lmd = .x)) # 2b) Portfolio Markowitz MarkowitzPortfolioWeights <- setNames(MarkowitzPortfolio, lmd) %>% map(., ~setNames(., c(ListNamesDates))) %>% map(., ~map(., ~setNames(., c(symbols)))) %>% map(., ~map_dfr(., ~bind_rows(.), .id = "date")) %>% map_dfr(., ~bind_rows(.), .id = "lambda") %>% mutate(date = as.Date(paste(date, "01"), format = "%Y %b %d")) MarkowitzPortfolioWeights %>% head() %>% kable() %>% kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
lambda | date | AAPL | ABBV | A | APD | AA | CF | NVDA | HOG | WMT | AMZN |
---|---|---|---|---|---|---|---|---|---|---|---|
0.25 | 2013-07-01 | 0 | 1.0000000 | 0.000000 | 0.0000000 | 0 | 0 | 0.0000000 | 0.0000000 | 0 | 0 |
0.25 | 2013-08-01 | 0 | 0.2938168 | 0.000000 | 0.7061832 | 0 | 0 | 0.0000000 | 0.0000000 | 0 | 0 |
0.25 | 2013-09-01 | 0 | 0.0000000 | 0.000000 | 1.0000000 | 0 | 0 | 0.0000000 | 0.0000000 | 0 | 0 |
0.25 | 2013-10-01 | 0 | 0.0000000 | 0.000001 | 0.9999984 | 0 | 0 | 0.0000004 | 0.0000001 | 0 | 0 |
0.25 | 2013-11-01 | 0 | 0.0000000 | 0.000000 | 0.0000000 | 0 | 0 | 0.0000000 | 0.0000000 | 0 | 1 |
0.25 | 2013-12-01 | 0 | 0.0000000 | 0.000000 | 0.0000000 | 0 | 0 | 0.0000000 | 0.0000000 | 0 | 1 |
We can see how the risk and return is affected by the changes in the \(\lambda\) values in the following plot. As the \(\lambda\) values increase the less risk we take on but also we assume less returns. Low values of \(\lambda\) makes us invest in a signle asset with the weight on asset \(A_{5}\), increasing the \(\lambda\) values increases the weights in other assets \(A_{i}\) spreading our risk.
# 2c) Portfolio Markowitz MarkowitzPortfolioWeights %>% select(date, lambda, everything()) %>% pivot_longer(cols = 3:ncol(.), values_to = "weights") %>% ggplot(aes(fill = name, y = weights, x = date)) + geom_bar(position = "stack", stat = "identity", width = 100) + facet_wrap(~lambda, ncol = 1) + scale_fill_viridis(option = "magma", discrete = TRUE, name = "Asset") + theme_bw() + ggtitle("Markowitz Rolling Portfolio Adjustments", subtitle = "For different Lambda Risk Values") + xlab("Date") + ylab("Weights")
Max Sharpe Ratio
Note: I originally made this post a few months back and when I went back to it some of the parts for the Max-Sharpe Ratio portfolio did not work as before – I think this is due to the recent update in the tidyr
package. I made some minor adjustments, however this part of the post is incorrect (just look at the portfolio weights plot). I hightlight in the code when I made the minor adjustments – when I have the opportunity to go back through the code more carefully I will –
# 2) Function: Max Sharpe Ratio #NOTE::: When we have all negative mus for a given month we obtain an NA value - the function # cannot find a maximum when all mus are negative compare the MaxSharpePortfolio$`2015 Sep` and # MaxSharpePortfolio$`2015 Oct` along with the mus$`2015 Sep` and mus$`2015 Oct` we invests 99% of the portfolio # in Sept in ABBV since its positive. In Oct ABBV mu is negative and we cannot invest at all. # Increasing the universe of Assets may overcome this problem. MaxSharpeportolioFunction <- function(mu, Sigma) { w <- Variable(nrow(Sigma)) problem <- Problem( Minimize(quad_form(w, Sigma)), constraints = list( w >= 0, t(mu) %*% w == 1) ) Solution <- solve(problem, solver="SCS") return(as.vector(Solution$getValue(w)/sum(Solution$getValue(w)))) } # 3a) Portfolio Max Sharpe Ratio MaxSharpePortfolio <- purrr::map2(.x = mus, .y = Sigmas, .f = ~MaxSharpeportolioFunction(.x, .y)) # 3b) Portfolio Max Sharpe Ratio MaxSharpePortfolioWeights <- setNames(MaxSharpePortfolio, ListNamesDates) %>% as_tibble(.) %>% # Made an adjustment here map(., ~setNames(., c(symbols))) %>% map_dfr(., ~bind_rows(.), .id = "date") %>% mutate(date = as.Date(paste(date, "01"), format = "%Y %b %d")) MaxSharpePortfolioWeights %>% head() %>% kable() %>% kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
date | AAPL | ABBV | A | APD | AA | CF | NVDA | HOG | WMT | AMZN |
---|---|---|---|---|---|---|---|---|---|---|
2013-07-01 | 0.0009088 | 0.1532283 | 0.0371040 | 0.2332344 | -0.1010233 | 0.0126946 | 0.2083815 | -0.0262255 | 0.5086221 | -0.0269248 |
2013-08-01 | 0.0109744 | 0.1485344 | -0.0156878 | 0.3292178 | -0.0080488 | 0.0051372 | 0.1146053 | 0.0091278 | 0.4086911 | -0.0025515 |
2013-09-01 | 0.1217206 | 0.1246626 | 0.0087004 | 0.3421645 | 0.0000000 | 0.0000000 | 0.2414533 | 0.0002820 | 0.1610166 | 0.0000000 |
2013-10-01 | 0.0270424 | 0.0000000 | 0.2268394 | 0.3427932 | 0.0000000 | 0.0000000 | 0.2653166 | 0.1283026 | 0.0000000 | 0.0097059 |
2013-11-01 | 0.2061379 | 0.0000000 | 0.1890218 | 0.2234739 | 0.0000000 | 0.0533254 | 0.0000000 | 0.0000000 | 0.0000000 | 0.3280410 |
2013-12-01 | 0.2252115 | 0.0000000 | 0.0306323 | 0.0724210 | 0.0000000 | 0.0325004 | 0.0000000 | 0.1907158 | 0.1192395 | 0.3292796 |
# 3c) Portfolio Max Sharpe Ratio MaxSharpePortfolioWeights %>% select(date, everything()) %>% # Made an adjustment here pivot_longer(cols = 2:ncol(.), values_to = "weights") %>% ggplot(aes(fill = name, y = weights, x = date)) + geom_bar(position = "stack", stat = "identity", width = 100) + scale_fill_viridis(option = "magma", discrete = TRUE, name = "Asset") + theme_bw() + ggtitle("Maximum Sharpe Ratio Rolling Portfolio Adjustments") + xlab("Date") + ylab("Weights")
Mu Quintile Portfolio
# 1a) mu_quintiles <- map(mus, ~ntile(., 5) %>% setNames(c(symbols))) %>% map(., ~sort(., decreasing = TRUE)) # 2) mu_diag_sigma <- map2( .x = mus, .y = Sigmas, ~.x / diag(.y)) mu_diag_sigma_quintiles <- mu_diag_sigma %>% map(., ~ntile(., 5)) %>% map(., ~setNames(., c(symbols))) %>% map(., ~sort(., decreasing = TRUE)) # 1b) rank the quintiles by mu quintiles_mu <- map2( .x = mus, .y = mu_quintiles, ~bind_rows(.x, .y)) quintiles_mu_weights <- map(quintiles_mu, ~ .x %>% mutate_all(~ case_when(. %in% c(5, 1) ~ .5, TRUE ~ 0)) %>% pmap_dfr(., ~ {v1 <- c(...) v2 <- if(sum(v1) > 1) replace(v1, v1 != 0, 1/sum(v1!=0)) else v1 as.list(v2)})) quintiles_mu_weights <- map(quintiles_mu_weights, ~.x[-1,]) %>% map2(.x = quintiles_mu, .y = ., ~bind_rows(.x, .y)) QuintileMuWeights <- map(quintiles_mu_weights, ~.x[3, ]) %>% # The weights are stored in row 3. map_dfr(., ~bind_rows(.), .id = "date") %>% mutate(date = as.Date(paste(date, "01"), format = "%Y %b %d")) QuintileMuWeights %>% head() %>% kable() %>% kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
date | AAPL | ABBV | A | APD | AA | CF | NVDA | HOG | WMT | AMZN |
---|---|---|---|---|---|---|---|---|---|---|
2013-07-01 | 0.25 | 0.25 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 | 0 | 0.00 | 0.00 |
2013-08-01 | 0.00 | 0.25 | 0.00 | 0.25 | 0.25 | 0.25 | 0.00 | 0 | 0.00 | 0.00 |
2013-09-01 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 | 0.25 | 0.25 | 0 | 0.00 | 0.00 |
2013-10-01 | 0.00 | 0.00 | 0.25 | 0.25 | 0.25 | 0.00 | 0.00 | 0 | 0.25 | 0.00 |
2013-11-01 | 0.00 | 0.25 | 0.00 | 0.25 | 0.00 | 0.00 | 0.00 | 0 | 0.25 | 0.25 |
2013-12-01 | 0.25 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.25 | 0 | 0.25 | 0.25 |
QuintileMuWeights %>% select(date, everything()) %>% pivot_longer(cols = 2:ncol(.), values_to = "weights") %>% ggplot(aes(fill = name, y = weights, x = date)) + geom_bar(position = "stack", stat = "identity", width = 100) + scale_fill_viridis(option = "magma", discrete = TRUE, name = "Asset") + theme_bw() + ggtitle("Quintile MU Rolling Portfolio Adjustments") + xlab("Date") + ylab("Weights")
Mu / Diag(Sigma) Portfolio
# 2a) mu_diag_sigma <- map2( .x = mus, .y = Sigmas, ~.x / diag(.y)) mu_diag_sigma_quintiles <- mu_diag_sigma %>% map(., ~ntile(., 5)) %>% map(., ~setNames(., c(symbols))) %>% map(., ~sort(., decreasing = TRUE)) # 2b) rank the quintiles by mu / diag(Sigma) quintiles_mu_diag_sigma <- map2( .x = mu_diag_sigma, .y = mu_diag_sigma_quintiles, ~bind_rows(.x, .y)) quintiles_mu_diag_sigma_weights <- map(quintiles_mu_diag_sigma, ~ .x %>% mutate_all(~ case_when(. %in% c(5, 1) ~ .5, TRUE ~ 0)) %>% pmap_dfr(., ~ {v1 <- c(...) v2 <- if(sum(v1) > 1) replace(v1, v1 != 0, 1/sum(v1!=0)) else v1 as.list(v2)})) quintiles_mu_diag_sigma_weights <- map(quintiles_mu_diag_sigma_weights, ~.x[-1,]) %>% map2(.x = quintiles_mu_diag_sigma, .y = ., ~bind_rows(.x, .y)) QuintileMuDiagSigmaWeights <- map(quintiles_mu_diag_sigma_weights, ~.x[3, ]) %>% # The weights are stored in row 3. map_dfr(., ~bind_rows(.), .id = "date") %>% mutate(date = as.Date(paste(date, "01"), format = "%Y %b %d")) QuintileMuDiagSigmaWeights %>% head() %>% kable() %>% kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
date | AAPL | ABBV | A | APD | AA | CF | NVDA | HOG | WMT | AMZN |
---|---|---|---|---|---|---|---|---|---|---|
2013-07-01 | 0.25 | 0.00 | 0.00 | 0.25 | 0.25 | 0.00 | 0.00 | 0.00 | 0.25 | 0.00 |
2013-08-01 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 | 0.25 | 0.00 | 0.00 | 0.25 | 0.00 |
2013-09-01 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 | 0.25 | 0.25 | 0.00 | 0.00 | 0.00 |
2013-10-01 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 | 0.00 | 0.25 | 0.00 | 0.25 | 0.00 |
2013-11-01 | 0.00 | 0.25 | 0.25 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 |
2013-12-01 | 0.00 | 0.00 | 0.00 | 0.00 | 0.25 | 0.00 | 0.25 | 0.25 | 0.25 | 0.00 |
QuintileMuDiagSigmaWeights %>% select(date, everything()) %>% pivot_longer(cols = 2:ncol(.), values_to = "weights") %>% ggplot(aes(fill = name, y = weights, x = date)) + geom_bar(position = "stack", stat = "identity", width = 100) + scale_fill_viridis(option = "magma", discrete = TRUE, name = "Asset") + theme_bw() + ggtitle("Quintile MU Diag Sigma Rolling Portfolio Adjustments") + xlab("Date") + ylab("Weights")
Mu / Diag(sqrt(Sigma)) Portfolio
# 3a) mu_diag_sqrt_sigma <- map2( .x = mus, .y = Sigmas, ~.x / sqrt(diag(.y))) mu_diag_sqrt_sigma_quintiles <- mu_diag_sqrt_sigma %>% map(., ~ntile(., 5)) %>% map(., ~setNames(., c(symbols))) %>% map(., ~sort(., decreasing = TRUE)) # 3b) rank the quintiles by mu / sqrt(diag(sigma)) quintiles_mu_diag_sqrt_sigma <- map2( .x = mu_diag_sqrt_sigma, .y = mu_diag_sqrt_sigma_quintiles, ~bind_rows(.x, .y)) quintiles_mu_diag_sqrt_sigma_weights <- map(quintiles_mu_diag_sqrt_sigma, ~ .x %>% mutate_all(~ case_when(. %in% c(5, 1) ~ .5, TRUE ~ 0)) %>% pmap_dfr(., ~ {v1 <- c(...) v2 <- if(sum(v1) > 1) replace(v1, v1 != 0, 1/sum(v1!=0)) else v1 as.list(v2)})) quintiles_mu_diag_sqrt_sigma_weights <- map(quintiles_mu_diag_sqrt_sigma_weights, ~.x[-1,]) %>% map2(.x = quintiles_mu_diag_sqrt_sigma, .y = ., ~bind_rows(.x, .y)) QuintileMuDiagSqrtSigmaWeights <- map(quintiles_mu_diag_sqrt_sigma_weights, ~.x[3, ]) %>% # The weights are stored in row 3. map_dfr(., ~bind_rows(.), .id = "date") %>% mutate(date = as.Date(paste(date, "01"), format = "%Y %b %d")) QuintileMuDiagSqrtSigmaWeights %>% head() %>% kable() %>% kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
date | AAPL | ABBV | A | APD | AA | CF | NVDA | HOG | WMT | AMZN |
---|---|---|---|---|---|---|---|---|---|---|
2013-07-01 | 0.25 | 0.25 | 0.00 | 0.00 | 0.00 | 0.25 | 0.00 | 0.00 | 0.25 | 0.00 |
2013-08-01 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 | 0.25 | 0.00 | 0.00 | 0.25 | 0.00 |
2013-09-01 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 | 0.25 | 0.25 | 0.00 | 0.00 | 0.00 |
2013-10-01 | 0.00 | 0.00 | 0.25 | 0.25 | 0.25 | 0.00 | 0.00 | 0.00 | 0.25 | 0.00 |
2013-11-01 | 0.00 | 0.25 | 0.00 | 0.25 | 0.00 | 0.00 | 0.00 | 0.00 | 0.25 | 0.25 |
2013-12-01 | 0.00 | 0.00 | 0.00 | 0.00 | 0.25 | 0.00 | 0.25 | 0.25 | 0.00 | 0.25 |
QuintileMuDiagSqrtSigmaWeights %>% select(date, everything()) %>% pivot_longer(cols = 2:ncol(.), values_to = "weights") %>% ggplot(aes(fill = name, y = weights, x = date)) + geom_bar(position = "stack", stat = "identity", width = 100) + scale_fill_viridis(option = "magma", discrete = TRUE, name = "Asset") + theme_bw() + ggtitle("Quintile MU Diag Sqrt(Sigma) Rolling Portfolio Adjustments") + xlab("Date") + ylab("Weights")
Analysis of Results
There are 8 different portfolio optimisation models. Putting all the data together, it’s easier to inspect the results.
############################# Assess the Returns ################################## MonthlyReturns <- map(portfolio_monthly_prices$splits, ~assessment(.x)) %>% map(., ~unnest(.x, data) %>% select(-year_month) %>% tk_xts(date_var = date) %>% lapply(., periodReturn, period = "monthly")) %>% setNames(., ListNamesDates) %>% map(., ~data.frame(.)) %>% bind_rows(., .id = "date") %>% mutate(date = as.Date(paste(date, "01"), format = "%Y %b %d")) %>% tk_xts(., date_var = date) ############# Assess the Monthly Portfolio Returns Depeninding on Weights ######### # 1d) TS_GMVPPortfolioWeights <- GMVPPortfolioWeights %>% tk_xts(., date_var = date) PortRets_GMVP <- Return.portfolio(MonthlyReturns, weights = TS_GMVPPortfolioWeights) %>% setNames(c("GMVP")) # 2d) TS_MarkowitzPortfolioWeights <- MarkowitzPortfolioWeights %>% group_split(lambda) %>% setNames(c(lmd)) %>% map(., ~tk_xts(., select = -lambda, date_var = date)) MarkowitzWeightedPortfolioReturnsFunction <- function(lmd){ WeightedReturns = Return.portfolio(MonthlyReturns, weights = TS_MarkowitzPortfolioWeights[[lmd]]) return(fortify.zoo(WeightedReturns)) } PortRets_Markowitz <- lapply(seq(1:length(lmd)), MarkowitzWeightedPortfolioReturnsFunction) %>% setNames(c(lmd)) %>% bind_rows(., .id = "lmd") %>% #column_to_rownames("Index") %>% #select(-matches("Index")) %>% #rownames_to_column("Index") %>% mutate(Index = as.Date(Index), lmd = as.numeric(lmd)) %>% pivot_wider(names_from = lmd, values_from = portfolio.returns) %>% tk_xts(date_var = Index) %>% setNames(c(paste("Markowitz Lambda", lmd))) # 3d) TS_MaxSharpePortfolioWeights <- MaxSharpePortfolioWeights %>% tk_xts(., date_var = date) PortRets_MaxSharpe <- Return.portfolio(MonthlyReturns, weights = TS_MaxSharpePortfolioWeights) %>% setNames(c("MaxSharpe")) # 4) quintile mu TS_QuintileMuWeights <- QuintileMuWeights %>% tk_xts(., date_var = date) PortRets_Mu_Quintiles <- Return.portfolio(MonthlyReturns, weights = TS_QuintileMuWeights) %>% setNames(c("Mu_Quintiles")) # 4) quintile mu diag sigma TS_QuintileMuDiagSigmaWeights <- QuintileMuDiagSigmaWeights %>% tk_xts(., date_var = date) PortRets_MuDiagSigma_Quintiles <- Return.portfolio(MonthlyReturns, weights = TS_QuintileMuDiagSigmaWeights) %>% setNames(c("MuDiagSigma_Quintiles")) # 4) Quintile mu diga sqrt sigma TS_QuintileMuDiagSqrtSigmaWeights <- QuintileMuDiagSqrtSigmaWeights %>% tk_xts(., date_var = date) PortRets_MuDiagSqrtSigma_Quintiles <- Return.portfolio(MonthlyReturns, weights = TS_QuintileMuDiagSqrtSigmaWeights) %>% setNames(c("MuDiagSqrtSigma_Quintiles")) #####################################################################################
Looking at the plots the Global Minimum Variance Portfolio shows the lowest volatility in the portfolio returns. The Max Sharpe portfolio is clearly wrong here (see, previous point on Max Sharpe Ratio section). The annaulised performance metrics are also displayed at the bottom for each portfolio over the period.
########## Assess the Annualised Returns based on portfolio weights ################## AllReturns <- cbind(PortRets_GMVP, PortRets_Markowitz, PortRets_MaxSharpe, PortRets_Mu_Quintiles, PortRets_MuDiagSigma_Quintiles, PortRets_MuDiagSqrtSigma_Quintiles) library(data.table) AllReturns %>% data.table(keep.rownames = TRUE) %>% as_tibble() %>% pivot_longer(cols = 2:ncol(.)) %>% ggplot(aes(x = index, y = value, color = name)) + geom_line() + facet_wrap(~name, ncol = 2) + ggtitle("Portfolio Returns", subtitle = "With portfolio optimised weights") + xlab("Returns") + ylab("Date") + theme_tq()
AnnualMetrics <- bind_cols(table.AnnualizedReturns(PortRets_GMVP) %>% rownames_to_column("Metric"), table.AnnualizedReturns(PortRets_Markowitz), table.AnnualizedReturns(PortRets_MaxSharpe), table.AnnualizedReturns(PortRets_Mu_Quintiles), table.AnnualizedReturns(PortRets_MuDiagSigma_Quintiles), table.AnnualizedReturns(PortRets_MuDiagSqrtSigma_Quintiles) ) AnnualMetrics %>% kable() %>% kable_styling(bootstrap_options = c("striped", "hover", "condensed", "responsive"))
Metric | GMVP | Markowitz Lambda 0.25 | Markowitz Lambda 0.5 | Markowitz Lambda 0.75 | MaxSharpe | Mu_Quintiles | MuDiagSigma_Quintiles | MuDiagSqrtSigma_Quintiles |
---|---|---|---|---|---|---|---|---|
Annualized Return | 0.1009 | 0.8441 | 0.8142 | 0.7712 | 0.1450 | 0.1939 | 0.2590 | 0.2691 |
Annualized Std Dev | 0.1127 | 0.3617 | 0.3516 | 0.3353 | 0.1473 | 0.1800 | 0.1485 | 0.1535 |
Annualized Sharpe (Rf=0%) | 0.8951 | 2.3340 | 2.3157 | 2.2999 | 0.9845 | 1.0774 | 1.7437 | 1.7529 |
Plotting the cumulative returns shows that the Markowitz lambda portfolios have the highest returns over the period, however they also have the highest standard deviation over the same period.
chart.CumReturns(AllReturns, main = "Weighted Returns by Objective Function and Risk Tolerance", wealth.index = TRUE, legend.loc = "topleft", colorset = rich6equal)
charts.PerformanceSummary(AllReturns, main = "Performance for Different Weighted Assets", wealth.index = TRUE, colorset = rich6equal)
Additional
(Some additional test data I leave here for future reference)
data <- data.frame( A_1 = c(2.3, 0.93, 0.62, 0.74, -0.23), A_2 = c(0.93, 1.40, 0.2, 0.56, 0.26), A_3 = c(0.62, 0.2, 1.80, 0.78, -0.27), A_4 = c(0.74, 0.56, 0.78, 3.4, -0.56), A_5 = c(-0.23, 0.26, -0.27, -0.56, 2.60), Security = c(1, 2, 3, 4, 5), R_i = c(15.1, 12.5, 14.7, 9.02, 17.68) ) Sigma <- data[, 1:5] w <- Variable(nrow(data)) mu <- data[, 7] n_samples = 10 lambdas <- 10^seq(-2, 3, length.out = n_samples) ret_data <- rep(0, n_samples) risk_data <- rep(0, n_samples) w_data <- matrix(0, nrow = n_samples, ncol = nrow(data)) for(i in seq_along(lambdas)) { lambda <- lambdas[i] problem <- Problem( Maximize( t(mu) %*% w - lambda*quad_form(w, Sigma) ), constraints = list(w >= 0, sum(w) == 1)) solution <- solve(problem) w_data[i,] <- solution$getValue(w) risk_data[i] <- solution$getValue(sqrt(quad_form(w, Sigma))) ret_data[i] <- solution$getValue(t(mu) %*% w) } ggplot() + geom_line(mapping = aes(x = risk_data, y = ret_data), color = "blue") + geom_point(mapping = aes(x = sqrt(diag(as.matrix(Sigma))), y = mu), color = "red")
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.