# basic finance
library(xts) # to manipulate time series of stock data
library(PerformanceAnalytics) # to compute performance measures
library(portfolioBacktest) # to conduct backtests
library(pob) # book package with financial data
# plotting
library(ggplot2) # for nice plots
library(reshape2) # to reshape data
library(patchwork) # for combining plots
library(latex2exp) # for latex symbols with TeX()
# optimization
library(CVXR)
library(nloptr)
library(alabama)
Portfolio Optimization
Chapter 10: Portfolios with Alternative Risk Measures
R code
R code examples for Chapter 10 of the book:
Daniel P. Palomar (2024). Portfolio Optimization: Theory and Application. Cambridge University Press.
Loading packages
The following packages are used in the examples:
Downside risk portfolios
First, let’s define the downside risk portfolio (as well as the two benchmarks MVP and GMVP):
library(CVXR)
<- function(mu, Sigma, lambda = 1, ub = Inf) {
design_MVP <- Variable(nrow(Sigma))
w <- Problem(Maximize(t(mu) %*% w - (lambda/2)*quad_form(w, Sigma)),
prob constraints = list(w >= 0, sum(w) == 1, w <= ub))
<- solve(prob)
result <- as.vector(result$getValue(w))
w return(w)
}
<- function(Sigma) design_MVP(mu = rep(0, ncol(Sigma)), Sigma = Sigma)
design_GMVP
<- function(X, alpha = 2, lmd = Inf, tau = c("mu", "zero")) {
design_DRportfolio <- as.matrix(X)
X <- ncol(X)
N
# disaster level
<- match.arg(tau)
tau if (tau == "zero")
<- rep(0, N)
mu else
<- colMeans(X)
mu
# design
<- Variable(N)
w if (lmd == Inf)
<- Problem(Minimize(mean(pos(t(mu) %*% w - X %*% w)^alpha)),
prob constraints = list(w >= 0, sum(w) == 1))
else
<- Problem(Maximize(t(w) %*% mu - lmd * mean(pos(t(mu) %*% w - X %*% w)^alpha)),
prob constraints = list(w >= 0, sum(w) == 1))
if (alpha == 1)
<- solve(prob, solver = "GLPK") #installed_solvers()
result else
<- solve(prob)
result if (!result$status %in% c("optimal", "optimal_inaccurate"))
browser()
return(as.vector(result$getValue(w)))
}
Then, prepare all the portfolio instances for the backtest:
<- function(data, ...) {
EWP <- ncol(data[[1]])
N return(rep(1/N, N))
}
<- function(dataset, ...) {
GMVP <- diff(log(dataset$prices))[-1]
X <- cov(X)
Sigma design_GMVP(Sigma)
}
<- function(dataset, ...) {
MVP <- diff(log(dataset$prices))[-1]
X <- cov(X)
Sigma <- colMeans(X)
mu design_MVP(mu, Sigma, lambda = 100)
}
<- function(dataset, ...) {
DR_alpha1 <- diff(log(dataset$prices))[-1]
X design_DRportfolio(X, alpha = 1, tau = "zero")
}
<- function(dataset, ...) {
DR_alpha2 <- diff(log(dataset$prices))[-1]
X design_DRportfolio(X, alpha = 2, tau = "zero")
}
<- function(dataset, ...) {
DR_alpha2_approx <- diff(log(dataset$prices))[-1]
X <- 0
tau <- pmax(tau - X, 0)
Xpos <- cov(Xpos)
Sigma design_GMVP(Sigma)
}
<- function(dataset, ...) {
DR_alpha3 <- diff(log(dataset$prices))[-1]
X design_DRportfolio(X, alpha = 3, tau = "zero")
}
Single backtest
Portfolio allocation of different downside risk portfolios:
library(pob)
<- SP500_2015to2020$stocks["2019::",
stock_prices c("AAPL", "AMZN", "AMD", "GM", "GOOGL", "MGM", "MSFT", "QCOM", "TSCO", "UPS")]
# backtest
<- portfolioBacktest(list("GMVP" = GMVP,
bt "Min. downside risk (alpha = 1)" = DR_alpha1,
"Min. semivariance" = DR_alpha2,
"Min. semivariance (approx)" = DR_alpha2_approx,
"Min. downside risk (alpha = 3)" = DR_alpha3),
list(list("prices" = stock_prices)),
lookback = 12*21, optimize_every = 21, rebalance_every = 1)
Backtesting 5 portfolios over 1 datasets (periodicity = daily data)...
listPortfoliosWithFailures(bt)
data.frame(
"stocks" = names(stock_prices),
"GMVP" = as.numeric(bt$`GMVP`$data1$w_optimized[1, ]),
"Min. downside risk (alpha = 1)" = as.numeric(bt$`Min. downside risk (alpha = 1)`$data1$w_optimized[1, ]),
"Min. semivariance" = as.numeric(bt$`Min. semivariance`$data1$w_optimized[1, ]),
"Min. semivariance (approx)" = as.numeric(bt$`Min. semivariance (approx)`$data1$w_optimized[1, ]),
"Min. downside risk (alpha = 3)" = as.numeric(bt$`Min. downside risk (alpha = 3)`$data1$w_optimized[1, ]),
check.names = FALSE) |>
melt(id.vars = "stocks") |>
ggplot(aes(x = stocks, y = value, fill = variable)) +
geom_bar(stat = "identity", position = "dodge", color = "black", width = 0.8) +
labs(fill = "portfolios") +
ylab("weight") +
ggtitle("Portfolio weights")
<- backtestSummary(bt)
bt_summary_median summaryTable(bt_summary_median, type = "kable",
measures = c("Sharpe ratio", "Sortino ratio", "annual return", "annual volatility", "downside deviation", "CVaR", "max drawdown"))
Portfolio | Sharpe ratio | Sortino ratio | annual return | annual volatility | downside deviation | max drawdown |
---|---|---|---|---|---|---|
GMVP | 1.70 | 2.44 | 61% | 36% | 0.25 | 27% |
Min. downside risk (alpha = 1) | 1.73 | 2.51 | 66% | 38% | 0.26 | 26% |
Min. semivariance | 1.78 | 2.58 | 67% | 38% | 0.26 | 27% |
Min. semivariance (approx) | 1.75 | 2.51 | 64% | 37% | 0.25 | 27% |
Min. downside risk (alpha = 3) | 1.78 | 2.60 | 67% | 37% | 0.26 | 27% |
Backtest performance of different downside risk portfolios:
<- backtestChartCumReturn(bt) +
p1 scale_x_date(date_breaks = "6 months", date_labels = "%b %Y", date_minor_breaks = "1 week") +
theme(legend.title = element_text()) + labs(color = "portfolios", linetype = "portfolios") +
ggtitle("Cumulative return")
<- backtestChartDrawdown(bt) +
p2 scale_x_date(date_breaks = "6 months", date_labels = "%b %Y", date_minor_breaks = "1 week") +
theme(legend.title = element_text()) + labs(color = "portfolios", linetype = "portfolios") +
ggtitle("Drawdown")
/ p2 + plot_layout(guides = "collect") p1
Multiple backtests
Backtest performance of different downside risk portfolios (\(N=50\)) over 50 datasets:
library(pob)
# resample data
set.seed(42)
<- financialDataResample(list("prices" = SP500_2015to2020$stocks),
stock_prices_resampled num_datasets = 50, N_sample = 50, T_sample = 252*2)
50 datasets resampled (with N = 50 instruments and length T = 504) from the original data between 2015-01-05 and 2020-09-22.
# perform multiple backtest on the resampled data
<- portfolioBacktest(list("GMVP" = GMVP,
bt "Min. downside risk (alpha = 1)" = DR_alpha1,
"Min. semivariance" = DR_alpha2,
"Min. semivariance (approx)" = DR_alpha2_approx,
"Min. downside risk (alpha = 3)" = DR_alpha3),
dataset_list = stock_prices_resampled,
lookback = 12*21, optimize_every = 21, rebalance_every = 1,
paral_datasets = 6, show_progress_bar = TRUE)
Backtesting 5 portfolios over 50 datasets (periodicity = daily data)...
Backtesting function "GMVP " (1/5)
Backtesting function "Min. downside risk (alpha = 1)" (2/5)
Backtesting function "Min. semivariance" (3/5)
Backtesting function "Min. semivariance (approx)" (4/5)
Backtesting function "Min. downside risk (alpha = 3)" (5/5)
listPortfoliosWithFailures(bt)
<- backtestSummary(bt)
bt_summary_median summaryTable(bt_summary_median, type = "kable", measures = c("Sharpe ratio", "CVaR", "max drawdown", "cpu time"))
Portfolio | Sharpe ratio | max drawdown | cpu time |
---|---|---|---|
GMVP | 1.38 | 9% | 0.17 |
Min. downside risk (alpha = 1) | 1.45 | 9% | 0.19 |
Min. semivariance | 1.43 | 9% | 0.16 |
Min. semivariance (approx) | 1.48 | 9% | 0.18 |
Min. downside risk (alpha = 3) | 1.43 | 9% | 0.27 |
<- backtestBoxPlot(bt, "Sharpe ratio") + coord_flip(ylim = c(0, 2.5)) +
p1 scale_x_discrete(labels = c("GMVP", TeX("Min. downside risk ($\\alpha = 1$)"), "Min. semivariance",
"Min. semivariance (approx)", TeX("Min. downside risk ($\\alpha$ = 3)")),
limits = rev(names(bt)))
<- backtestBoxPlot(bt, "max drawdown") + coord_flip(ylim = c(0, 0.2)) +
p2 scale_x_discrete(labels = c("GMVP", TeX("Min. downside risk ($\\alpha = 1$)"), "Min. semivariance",
"Min. semivariance (approx)", TeX("Min. downside risk ($\\alpha$ = 3)")),
limits = rev(names(bt)))
/ p2 p1
Tail portfolios
First, let’s define the worst-case portfolio, the CVaR portfolio, and the EVaR portfolio:
library(CVXR)
library(nloptr)
library(alabama)
<- function(X, lmd = 5) {
portolioWorstCase <- nrow(X)
T <- ncol(X)
N <- as.matrix(X)
X <- colMeans(X)
mu
# design (https://cvxr.rbind.io/cvxr_functions/)
<- Variable(N)
w <- Problem(Maximize(t(w) %*% mu - lmd * max_entries(- X %*% w)),
prob constraints = list(w >= 0, sum(w) == 1))
<- solve(prob, solver = "GLPK") #installed_solvers()
result return(as.vector(result$getValue(w)))
}
<- function(X, lmd = 5, alpha = 0.95) {
portolioCVaR <- nrow(X)
T <- ncol(X)
N <- as.matrix(X)
X <- colMeans(X)
mu
<- Variable(N)
w <- Variable(1)
tau <- Variable(T)
u <- Problem(Maximize( t(w) %*% mu - lmd*(tau + mean(u)/(1-alpha)) ),
prob constraints = list(u >= 0, u >= -X %*% w - tau,
>= 0, sum(w) == 1))
w tryCatch(result <- solve(prob, solver = "GLPK"), #installed_solvers()
error = function(e) geterrmessage())
return(as.vector(result$getValue(w)))
}#w <- CVaR_alpha95(stock_prices_resampled$`dataset 1`)
<- function(X, lmd = 1000, alpha = 0.95) {
portolioEVaR_CVXR <- nrow(X)
T <- ncol(X)
N <- as.matrix(X)
X <- colMeans(X)
mu
# CVXR
<- Variable(N)
w <- Variable(1)
s <- Variable(1)
t <- Variable(T)
u
<- Problem(Maximize( t(w) %*% mu - lmd*( s - t*log((1-alpha)*T) ) ),
prob constraints = list(CVXR:::ExpCone(- X %*% w - s, t*rep(1,T), u),
>= sum(u),
t >= 0, sum(w) == 1))
w
<- solve(prob, num_iter = 200, verbose = FALSE)
result
return(as.vector(result$getValue(w)))
}
# https://docs.mosek.com/modeling-cookbook/expo.html
<- function(X, lmd = Inf, alpha = 0.95) {
portolioEVaR_nloptr <- nrow(X)
T <- ncol(X)
N <- as.matrix(X)
X <- colMeans(X)
mu
#
# via general solver nloptr
#
<- rep(1/N, N)
w0 <- 1
t0
<- function(x) {
fn_min <- x[1:N]
w <- x[N+1]
t -( t(w) %*% mu - lmd*t*log(mean(exp(- X %*% (w/t)))/(1-alpha)) )
}<- function(x) {
h_eq <- x[1:N]
w <- x[N+1]
t sum(w) - 1
}<- function(x) {
h_eq_jac <- x[1:N]
w <- x[N+1]
t rbind(c(rep(1, N), 0))
}
<- nloptr::slsqp(x0 = c(w0, t0),
res fn = fn_min, # gr =
lower = rep(0, N + 1),
heq = h_eq, heqjac = h_eq_jac,
control = list(xtol_rel = 1e-6)) # check_derivatives = TRUE
<- res$par[1:N]
w_nloptr <- res$par[N+1]
t_nloptr
return(w_nloptr)
}
Then, prepare all the portfolio instances for the backtest:
<- function(dataset, ...) {
CVaR_alpha90 <- diff(log(dataset$prices))[-1]
X portolioCVaR(X, lmd = 10000, alpha = 0.90)
}<- function(dataset, ...) {
CVaR_alpha95 <- diff(log(dataset$prices))[-1]
X portolioCVaR(X, lmd = 10000, alpha = 0.95)
}<- function(dataset, ...) {
CVaR_alpha99 <- diff(log(dataset$prices))[-1]
X portolioCVaR(X, lmd = 10000, alpha = 0.99)
}
<- function(dataset, ...) {
EVaR_alpha90 <- diff(log(dataset$prices))[-1]
X portolioEVaR_nloptr(X, lmd = 10000, alpha = 0.90)
}<- function(dataset, ...) {
EVaR_alpha95 <- diff(log(dataset$prices))[-1]
X portolioEVaR_nloptr(X, lmd = 10000, alpha = 0.95)
}<- function(dataset, ...) {
EVaR_alpha99 <- diff(log(dataset$prices))[-1]
X portolioEVaR_nloptr(X, lmd = 10000, alpha = 0.99)
}
<- function(dataset, ...) {
WorstCase <- diff(log(dataset$prices))[-1]
X portolioWorstCase(X, lmd = 10000)
}
Single backtest
Portfolio allocation of different tail portfolios:
library(pob)
<- SP500_2015to2020$stocks["2019::",
stock_prices c("AAPL", "AMZN", "AMD", "GM", "GOOGL", "MGM", "MSFT", "QCOM", "TSCO", "UPS")]
# backtest
<- portfolioBacktest(list("GMVP" = GMVP,
bt "Min. CVaR 90%" = CVaR_alpha90,
"Min. EVaR 90%" = EVaR_alpha90,
"Min. Worst-Case" = WorstCase),
list(list("prices" = stock_prices)),
lookback = 12*21, optimize_every = 21, rebalance_every = 1)
Backtesting 4 portfolios over 1 datasets (periodicity = daily data)...
listPortfoliosWithFailures(bt)
data.frame(
"stocks" = names(stock_prices),
"GMVP" = as.numeric(bt$`GMVP`$data1$w_optimized[1, ]),
"Min. CVaR 90%" = as.numeric(bt$`Min. CVaR 90%`$data1$w_optimized[1, ]),
"Min. EVaR 90%" = as.numeric(bt$`Min. EVaR 90%`$data1$w_optimized[1, ]),
"Min. Worst-Case" = as.numeric(bt$`Min. Worst-Case`$data1$w_optimized[1, ]),
check.names = FALSE) |>
melt(id.vars = "stocks") |>
ggplot(aes(x = stocks, y = value, fill = variable)) +
geom_bar(stat = "identity", position = "dodge", color = "black", width = 0.8) +
labs(fill = "portfolios") +
ylab("weight") +
ggtitle("Portfolio weights")
<- backtestSummary(bt)
bt_summary_median summaryTable(bt_summary_median, type = "kable",
measures = c("Sharpe ratio", "Sortino ratio", "annual return", "annual volatility", "downside deviation", "CVaR", "max drawdown"))
Portfolio | Sharpe ratio | Sortino ratio | annual return | annual volatility | downside deviation | max drawdown |
---|---|---|---|---|---|---|
GMVP | 1.70 | 2.44 | 61% | 36% | 0.25 | 27% |
Min. CVaR 90% | 1.59 | 2.28 | 59% | 37% | 0.26 | 28% |
Min. EVaR 90% | 1.57 | 2.27 | 59% | 38% | 0.26 | 28% |
Min. Worst-Case | 1.24 | 1.79 | 48% | 38% | 0.27 | 33% |
Backtest performance of different tail portfolios:
<- backtestChartCumReturn(bt) +
p1 scale_x_date(date_breaks = "6 months", date_labels = "%b %Y", date_minor_breaks = "1 week") +
theme(legend.title = element_text()) + labs(color = "portfolios", linetype = "portfolios") +
ggtitle("Cumulative return")
<- backtestChartDrawdown(bt) +
p2 scale_x_date(date_breaks = "6 months", date_labels = "%b %Y", date_minor_breaks = "1 week") +
theme(legend.title = element_text()) + labs(color = "portfolios", linetype = "portfolios") +
ggtitle("Drawdown")
/ p2 + plot_layout(guides = "collect") p1
Multiple backtests
Backtest performance of CVaR and EVaR portfolios (\(N=10\)) over 50 datasets:
library(pob)
# resample data
set.seed(42)
<- financialDataResample(list("prices" = SP500_2015to2020$stocks),
stock_prices_resampled num_datasets = 50, N_sample = 10, T_sample = 252*2)
50 datasets resampled (with N = 10 instruments and length T = 504) from the original data between 2015-01-05 and 2020-09-22.
# perform multiple backtest on the resampled data
<- portfolioBacktest(list("GMVP" = GMVP,
bt "Min. CVaR 90%" = CVaR_alpha90,
"Min. EVaR 90%" = EVaR_alpha90,
"Min. CVaR 95%" = CVaR_alpha95,
"Min. EVaR 95%" = EVaR_alpha95,
"Min. CVaR 99%" = CVaR_alpha99,
"Min. EVaR 99%" = EVaR_alpha99,
"Min. Worst-Case" = WorstCase),
dataset_list = stock_prices_resampled,
lookback = 12*21, optimize_every = 21, rebalance_every = 1,
paral_datasets = 6, show_progress_bar = TRUE)
Backtesting 8 portfolios over 50 datasets (periodicity = daily data)...
Backtesting function "GMVP " (1/8)
Backtesting function "Min. CVaR 90% " (2/8)
Backtesting function "Min. EVaR 90% " (3/8)
Backtesting function "Min. CVaR 95% " (4/8)
Backtesting function "Min. EVaR 95% " (5/8)
Backtesting function "Min. CVaR 99% " (6/8)
Backtesting function "Min. EVaR 99% " (7/8)
Backtesting function "Min. Worst-Case" (8/8)
listPortfoliosWithFailures(bt)
<- backtestSummary(bt)
bt_summary_median summaryTable(bt_summary_median, type = "kable", measures = c("Sharpe ratio", "CVaR", "max drawdown", "cpu time"))
Portfolio | Sharpe ratio | max drawdown | cpu time |
---|---|---|---|
GMVP | 0.99 | 12% | 0.14 |
Min. CVaR 90% | 0.92 | 11% | 0.17 |
Min. EVaR 90% | 1.00 | 13% | 0.03 |
Min. CVaR 95% | 1.02 | 11% | 0.17 |
Min. EVaR 95% | 1.03 | 13% | 0.03 |
Min. CVaR 99% | 0.95 | 12% | 0.17 |
Min. EVaR 99% | 0.91 | 13% | 0.35 |
Min. Worst-Case | 1.01 | 12% | 0.13 |
<- backtestBoxPlot(bt, "Sharpe ratio") + coord_flip(ylim = c(-0.5, 2.5))
p1 <- backtestBoxPlot(bt, "max drawdown") + coord_flip(ylim = c(0, 0.3))
p2 / p2 p1
Drawdown portfolios
First, let’s define the max-drawdown portfolio, ave-drawdown portfolio, and CVaR-drawdown portfolio:
library(CVXR)
<- function(X, lmd = 5) {
portfolioMaxDD <- nrow(X)
T <- ncol(X)
N <- as.matrix(X)
X <- apply(X, MARGIN = 2, FUN = cumsum)
X_cum <- colMeans(X)
mu
# variables
<- Variable(N)
w <- Variable(1)
s <- Variable(T)
u # problem
<- Problem(Maximize(t(w) %*% mu - lmd*s),
prob constraints = list(w >= 0, sum(w) == 1,
<= X_cum %*% w + s,
u >= X_cum %*% w,
u -1] >= u[-T]))
u[<- solve(prob, solver = "GLPK") #installed_solvers()
result return(as.vector(result$getValue(w)))
}
<- function(X, lmd = 5) {
portfolioAveDD <- nrow(X)
T <- ncol(X)
N <- as.matrix(X)
X <- apply(X, MARGIN = 2, FUN = cumsum)
X_cum <- colMeans(X)
mu
# variables
<- Variable(N)
w <- Variable(1)
s <- Variable(T)
u # problem
<- Problem(Maximize(t(w) %*% mu- lmd*s),
prob constraints = list(w >= 0, sum(w) == 1,
mean(u) <= mean(X_cum %*% w) + s,
>= X_cum %*% w,
u -1] >= u[-T]))
u[<- solve(prob, solver = "GLPK") #installed_solvers()
result return(as.vector(result$getValue(w)))
}#w <- portfolioAveDD(X = diff(log(stock_prices_resampled$`dataset 1`$prices))[-1])
<- function(X, lmd = 5, alpha = 0.95) {
portfolioCVaRDD <- nrow(X)
T <- ncol(X)
N <- as.matrix(X)
X <- apply(X, MARGIN = 2, FUN = cumsum)
X_cum <- colMeans(X)
mu
# variables
<- Variable(N)
w <- Variable(1)
tau <- Variable(1)
s <- Variable(T)
z <- Variable(T)
u # problem
<- Problem(Maximize(t(w) %*% mu - lmd*s),
prob constraints = list(w >= 0, sum(w) == 1,
>= tau + mean(z)/(1-alpha),
s >= 0, z >= u - X_cum %*% w - tau,
z >= X_cum %*% w,
u -1] >= u[-T]))
u[<- solve(prob, solver = "GLPK") #installed_solvers()
result return(as.vector(result$getValue(w)))
}
Then, prepare all the portfolio instances for the backtest:
<- function(dataset, ...) {
MaxDD <- diff(log(dataset$prices))[-1]
X portfolioMaxDD(X, lmd = 1000)
}
<- function(dataset, ...) {
AveDD <- diff(log(dataset$prices))[-1]
X portfolioAveDD(X, lmd = 1000)
}
<- function(dataset, ...) {
CVaRDD_alpha90 <- diff(log(dataset$prices))[-1]
X portfolioCVaRDD(X, lmd = 1000, alpha = 0.90)
}
<- function(dataset, ...) {
CVaRDD_alpha95 <- diff(log(dataset$prices))[-1]
X portfolioCVaRDD(X, lmd = 1000, alpha = 0.95)
}
<- function(dataset, ...) {
CVaRDD_alpha99 <- diff(log(dataset$prices))[-1]
X portfolioCVaRDD(X, lmd = 1000, alpha = 0.99)
}
Single backtest
Portfolio allocation of different drawdown portfolios:
library(pob)
<- SP500_2015to2020$stocks["2019::",
stock_prices c("AAPL", "AMZN", "AMD", "GM", "GOOGL", "MGM", "MSFT", "QCOM", "TSCO", "UPS")]
# backtest
<- portfolioBacktest(list("GMVP" = GMVP,
bt "Min. Max-DD" = MaxDD,
"Min. Ave-DD" = AveDD,
"Min. CVaR-DD 90%" = CVaRDD_alpha90),
list(list("prices" = stock_prices)),
lookback = 12*21, optimize_every = 21, rebalance_every = 1)
Backtesting 4 portfolios over 1 datasets (periodicity = daily data)...
listPortfoliosWithFailures(bt)
data.frame(
"stocks" = names(stock_prices),
"GMVP" = as.numeric(bt$`GMVP`$data1$w_optimized[1, ]),
"Min. Max-DD" = as.numeric(bt$`Min. Max-DD`$data1$w_optimized[1, ]),
"Min. Ave-DD" = as.numeric(bt$`Min. Ave-DD`$data1$w_optimized[1, ]),
"Min. CVaR-DD 90%" = as.numeric(bt$`Min. CVaR-DD 90%`$data1$w_optimized[1, ]),
check.names = FALSE) |>
melt(id.vars = "stocks") |>
ggplot(aes(x = stocks, y = value, fill = variable)) +
geom_bar(stat = "identity", position = "dodge", color = "black", width = 0.8) +
labs(fill = "portfolios") +
ylab("weight") +
ggtitle("Portfolio weights")
<- backtestSummary(bt)
bt_summary_median summaryTable(bt_summary_median, type = "kable",
measures = c("Sharpe ratio", "Sortino ratio", "annual return", "annual volatility", "downside deviation", "CVaR", "max drawdown"))
Portfolio | Sharpe ratio | Sortino ratio | annual return | annual volatility | downside deviation | max drawdown |
---|---|---|---|---|---|---|
GMVP | 1.70 | 2.44 | 61% | 36% | 0.25 | 27% |
Min. Max-DD | 1.81 | 2.55 | 73% | 41% | 0.29 | 32% |
Min. Ave-DD | 1.61 | 2.37 | 72% | 45% | 0.31 | 27% |
Min. CVaR-DD 90% | 1.80 | 2.65 | 82% | 45% | 0.31 | 29% |
Backtest performance of different tail portfolios:
<- backtestChartCumReturn(bt) +
p1 scale_x_date(date_breaks = "6 months", date_labels = "%b %Y", date_minor_breaks = "1 week") +
theme(legend.title = element_text()) + labs(color = "portfolios", linetype = "portfolios") +
ggtitle("Cumulative return")
<- backtestChartDrawdown(bt) +
p2 scale_x_date(date_breaks = "6 months", date_labels = "%b %Y", date_minor_breaks = "1 week") +
theme(legend.title = element_text()) + labs(color = "portfolios", linetype = "portfolios") +
ggtitle("Drawdown")
/ p2 + plot_layout(guides = "collect") p1
Multiple backtests
Backtest performance of drawdown portfolios (\(N=10\)) over 50 datasets:
library(pob)
# resample data
set.seed(42)
<- financialDataResample(list("prices" = SP500_2015to2020$stocks),
stock_prices_resampled num_datasets = 50, N_sample = 10, T_sample = 252*2)
50 datasets resampled (with N = 10 instruments and length T = 504) from the original data between 2015-01-05 and 2020-09-22.
# perform multiple backtest on the resampled data
<- portfolioBacktest(list("GMVP" = GMVP,
bt "Min. Max-DD" = MaxDD,
"Min. Ave-DD" = AveDD,
"Min. CVaR-DD 90%" = CVaRDD_alpha90,
"Min. CVaR-DD 95%" = CVaRDD_alpha95,
"Min. CVaR-DD 99%" = CVaRDD_alpha99),
dataset_list = stock_prices_resampled,
lookback = 12*21, optimize_every = 21, rebalance_every = 1,
paral_datasets = 6, show_progress_bar = TRUE)
Backtesting 6 portfolios over 50 datasets (periodicity = daily data)...
Backtesting function "GMVP " (1/6)
Backtesting function "Min. Max-DD " (2/6)
Backtesting function "Min. Ave-DD " (3/6)
Backtesting function "Min. CVaR-DD 90%" (4/6)
Backtesting function "Min. CVaR-DD 95%" (5/6)
Backtesting function "Min. CVaR-DD 99%" (6/6)
listPortfoliosWithFailures(bt)
<- backtestSummary(bt)
bt_summary_median summaryTable(bt_summary_median, type = "kable", measures = c("Sharpe ratio", "CVaR", "max drawdown", "cpu time"))
Portfolio | Sharpe ratio | max drawdown | cpu time |
---|---|---|---|
GMVP | 0.99 | 12% | 0.14 |
Min. Max-DD | 0.96 | 13% | 0.17 |
Min. Ave-DD | 0.98 | 12% | 0.19 |
Min. CVaR-DD 90% | 1.00 | 12% | 0.26 |
Min. CVaR-DD 95% | 0.88 | 12% | 0.26 |
Min. CVaR-DD 99% | 1.01 | 12% | 0.26 |
<- backtestBoxPlot(bt, "Sharpe ratio") + coord_flip(ylim = c(-0.5, 2.5))
p1 <- backtestBoxPlot(bt, "max drawdown") + coord_flip(ylim = c(0, 0.3))
p2 / p2 p1