This vignette illustrates some ways to use the
AssetAllocation
package to backtest different asset
allocation strategies.
There are several alternatives to backtest systematic/quantitative investment strategies in R. The aim of this package is to provide a simplified way to backtest simple asset allocation rules. That is, with a few lines of code, the user can create and backtest simple static or dynamic (tactical) asset allocation strategies.
The package comes with a set of pre-loaded static and tactical
strategies, which are in the asset_allocations
object.
However, the user can easily create their own strategies, either by
choosing specific allocations in a static asset allocation, or by
creating their own custom function that implements a dynamic
strategy.
Within the context of the package, an asset allocation strategy is an
object of the type list
which contains the following
elements:
name
: an object of type character
with
the name of the strategy
tickers
: a vector of type character
containing the tickers of the assets to be used. These must either
correspond to the column names in the user-provided data to be used to
backtest the strategy, or to tickers in Yahoo Finance.
default_weights
: a vector of type
numeric
containing the default weights to invest in each
asset in decimals. The sum of the weights should be less than or equal
to one. Any amount not invested in the assets is automatically assumed
to be invested in the risk-free rate. If the rebalance function is
risk_parity
, this field should contain the risk budgets (in
decimals, the sum should equal one).
rebalance_frequency
: an object of type
character
which determines the rebalancing frequency.
Options are “days”, “weeks”, “months”, “quarters”, and “years”.
portfolio_rule_fn
: an object of type
character
containing the name of the rebalancing function
that determines allocations for the next period. A valid rebalancing
function takes as inputs a strategy, a rebalancing date, an
xts
matrix of prices, an xts
matrix of
returns, and an xts
vector of returns on a risk-free asset.
The function returns a vector of type numeric
with the same
number of elements as the object strat$tickers
. Some
specific cases that come with the package are:
static asset allocation strategies:
"constant_weights"
Ivy Portfolio: "tactical_ivy"
Dual Momentum: "tactical_DualMomentum"
Robust Asset Allocation: "tactical_RAA"
Adaptive Asset Allocation: "tactical_AAA"
Minimum variance: "min_variance"
Risk parity: "risk_parity"
A few comments:
The rebalancing function for tactical asset allocation strategies may contain specific choices regarding calculation of covariance matrices, look-back periods and so on. Consult the help for each rebalancing function for details.
Some rebalancing function require additional elements. For example, the “Dual Momentum” strategy requires the asset classes of each ticker. I’ve tried to make the package generic enough to allow users to backtest allocation rules with other assets, while at the same time maintaining a (hopefully) simple syntax.
To use the package, the user follows basically two steps:
Create a strategy with the elements described above (or choose one of the pre-loaded strategies)
Backtest the strategy by creating a new object using the function
backtest_allocation
and some data. The
backtest_allocation
function expects a strategy with the
elements described above, as well as an xts
matrix of
prices, an xts
matrix of returns, and an optional
xts
vector of returns on a risk-free asset. The user can
also provide an optional starting date. Importantly, the tickers in the
strategy should correspond to valid columns of the price and return
objects.
As defined above, an asset allocation strategy is a portfolio
comprised of a set of assets, with portfolios weights determined by a
specific rule, rebalanced at some frequency. The package comes with
several pre-loaded asset allocation strategies, which generally come
from published sources. These are in the object
asset_allocations
.
All of the pre-loaded asset allocation strategies are defined in
terms of exchange-traded funds, data for which are available in the
ETFs
data set. Users can type ?ETFs
to obtain
more information. The purpose of the pre-loaded strategies and data is
to demonstrate how to use the package. Users can test their own
strategies using their own data, or they can also specify their own
assets and have the package retrieve data automatically from Yahoo
Finance.
We load the package and inspect the available pre-loaded static (i.e., constant-weight) asset allocations:
library(AssetAllocation)
#> Registered S3 method overwritten by 'quantmod':
#> method from
#> as.zoo.data.frame zoo
library(PerformanceAnalytics)
#> Loading required package: xts
#> Loading required package: zoo
#>
#> Attaching package: 'zoo'
#> The following objects are masked from 'package:base':
#>
#> as.Date, as.Date.numeric
#>
#> Attaching package: 'PerformanceAnalytics'
#> The following object is masked from 'package:graphics':
#>
#> legend
names(asset_allocations$static)
#> [1] "us_60_40" "golden_butterfly" "rob_arnott" "globalAA"
#> [5] "permanent" "desert" "larry" "big_rocks"
#> [9] "sandwich" "balanced_tax" "balanced" "income_gr"
#> [13] "income_gr_tax" "con_income" "con_income_tax" "all_weather"
One of the pre-loaded static asset allocations is Ray Dalio’s All Weather Portfolio. The strategy invests 30% in U.S. stocks (represented by the SPY ETF), 40% in long-term U.S. Treasuries (TLT), 15% in intermediate-term U.S. Treasuries (IEF), 7.5% in gold (GLD), and 7.5% in commodities (DBC).
$static$all_weather
asset_allocations#> $name
#> [1] "All Weather Portfolio"
#>
#> $tickers
#> [1] "SPY" "TLT" "IEF" "GLD" "DBC"
#>
#> $default_weights
#> [1] 0.300 0.400 0.150 0.075 0.075
#>
#> $rebalance_frequency
#> [1] "month"
#>
#> $portfolio_rule_fn
#> [1] "constant_weights"
To backtest this strategy with the data in the ETFs
object, we simply do:
# define strategy
<- asset_allocations$static$all_weather
all_weather
# backtest strategy
<- backtest_allocation(all_weather, ETFs$Prices, ETFs$Returns, ETFs$risk_free) bt_all_weather
The output from backtest_allocation
contains the daily
returns of the strategy in the $returns
object. A
convenient way to visualize the results is by using the
charts.PerformanceSummary
function from the
PerformanceAnalytics
package:
# plot cumulative returns
charts.PerformanceSummary(bt_all_weather$returns,
main = all_weather$strat$name)
A basic set of performance statistics is provided in
$table_performance
:
# table with performance metrics
$table_performance
bt_all_weather#> All.Weather.Portfolio
#> Annualized Return 0.0707
#> Annualized Std Dev 0.0749
#> Annualized Sharpe (Rf=0.79%) 0.8338
#> daily downside risk 0.0033
#> Annualised downside risk 0.0528
#> Downside potential 0.0016
#> Omega 1.1618
#> Sortino ratio 0.0755
#> Upside potential 0.0018
#> Upside potential ratio 0.6946
#> Omega-sharpe ratio 0.1618
#> Semi Deviation 0.0034
#> Gain Deviation 0.0031
#> Loss Deviation 0.0036
#> Downside Deviation (MAR=210%) 0.0093
#> Downside Deviation (Rf=0.79%) 0.0033
#> Downside Deviation (0%) 0.0033
#> Maximum Drawdown 0.1640
#> Historical VaR (95%) -0.0074
#> Historical ES (95%) -0.0109
#> Modified VaR (95%) -0.0073
#> Modified ES (95%) -0.0137
The allocations over time are stored in $weights
. Of
course, for static, buy-and-hold asset allocations, the portfolio
weights always remains the same:
chart.StackedBar(bt_all_weather$rebalance_weights,
date.format = "%Y",
main = paste0("Allocations, ", all_weather$name))
As should be clear from the graph above, the weights that are stored
in $weights
are the weights on the rebalancing dates. Even
for a static, buy-and-hold strategy, the actual weights between
rebalancing dates will fluctuate.
The other pre-loaded static asset allocations may be tested analogously.
In this example, we create a custom strategy from scratch. The strategy invests equally in momentum (MTUM), value (VLUE), low volatility (USMV) and quality (QUAL) ETFs. We first set up this custom strategy as follows:
<- list(name = "EW Factors",
factors_EW tickers = c("MTUM", "VLUE", "USMV", "QUAL"),
default_weights = c(0.25, 0.25, 0.25, 0.25),
rebalance_frequency = "month",
portfolio_rule_fn = "constant_weights")
Next, we can automatically download data from Yahoo Finance using the
get_data_from_tickers
function:
<- get_data_from_tickers(factors_EW$tickers,
factor_ETFs_data starting_date = "2013-08-01")
Finally, we backtest the strategy and show the results:
# backtest the strategy
<- backtest_allocation(factors_EW,factor_ETFs_data$P, factor_ETFs_data$R)
bt_factors_EW
# plot returns
charts.PerformanceSummary(bt_factors_EW$returns,
main = bt_factors_EW$strat$name,
)
# table with performance metrics
$table_performance
bt_factors_EW#> EW.Factors
#> Annualized Return 0.1233
#> Annualized Std Dev 0.1731
#> Annualized Sharpe (Rf=0%) 0.7125
#> daily downside risk 0.0078
#> Annualised downside risk 0.1246
#> Downside potential 0.0032
#> Omega 1.1652
#> Sortino ratio 0.0664
#> Upside potential 0.0037
#> Upside potential ratio 0.5665
#> Omega-sharpe ratio 0.1652
#> Semi Deviation 0.0081
#> Gain Deviation 0.0077
#> Loss Deviation 0.0094
#> Downside Deviation (MAR=210%) 0.0125
#> Downside Deviation (Rf=0%) 0.0078
#> Downside Deviation (0%) 0.0078
#> Maximum Drawdown 0.3499
#> Historical VaR (95%) -0.0161
#> Historical ES (95%) -0.0266
#> Modified VaR (95%) -0.0151
#> Modified ES (95%) -0.0168
In this example, we test and compare four pre-loaded tactical asset allocation strategies: the Ivy Portfolio, the Robust Asset Allocation strategy, the Dual Momentum strategy, and the Adaptive Asset Allocation strategy. A brief description of each strategy (as well as appropriate references) is provided in the corresponding rebalancing functions.
# define strategies
<- asset_allocations$tactical$ivy
ivy <- asset_allocations$tactical$raa
raa <- asset_allocations$tactical$dual_mo
dual_mo <- asset_allocations$tactical$aaa
aaa
# run backtests
<- backtest_allocation(ivy, ETFs$Prices,ETFs$Returns, ETFs$risk_free)
bt_ivy <- backtest_allocation(raa, ETFs$Prices,ETFs$Returns, ETFs$risk_free)
bt_raa <- backtest_allocation(dual_mo, ETFs$Prices,ETFs$Returns, ETFs$risk_free)
bt_dual_mo <- backtest_allocation(aaa, ETFs$Prices,ETFs$Returns, ETFs$risk_free)
bt_aaa
<- merge.xts(bt_ivy$returns, bt_raa$returns, bt_dual_mo$returns, bt_aaa$returns)
ret_strats
# find index from which all strats are available
<- which.max(!is.na(rowSums(ret_strats)))
min_ind
charts.PerformanceSummary(ret_strats[min_ind:nrow(ret_strats)])
cbind(bt_ivy$table_performance,
$table_performance,
bt_raa$table_performance,
bt_dual_mo$table_performance)
bt_aaa#> Ivy RAA.Balanced Antonacci.s.Dual.Momentum
#> Annualized Return 0.0475 0.0454 0.0494
#> Annualized Std Dev 0.0777 0.0601 0.0896
#> Annualized Sharpe (Rf=0.53%) 0.5416 0.6259 0.4908
#> daily downside risk 0.0035 0.0028 0.0041
#> Annualised downside risk 0.0562 0.0441 0.0655
#> Downside potential 0.0015 0.0012 0.0016
#> Omega 1.1185 1.1279 1.1144
#> Sortino ratio 0.0496 0.0554 0.0453
#> Upside potential 0.0017 0.0014 0.0018
#> Upside potential ratio 0.6193 0.6066 0.6221
#> Omega-sharpe ratio 0.1185 0.1279 0.1144
#> Semi Deviation 0.0036 0.0028 0.0042
#> Gain Deviation 0.0034 0.0024 0.0039
#> Loss Deviation 0.0042 0.0032 0.0052
#> Downside Deviation (MAR=210%) 0.0094 0.0090 0.0098
#> Downside Deviation (Rf=0.53%) 0.0035 0.0028 0.0041
#> Downside Deviation (0%) 0.0035 0.0028 0.0041
#> Maximum Drawdown 0.1333 0.1103 0.1864
#> Historical VaR (95%) -0.0073 -0.0058 -0.0085
#> Historical ES (95%) -0.0126 -0.0096 -0.0141
#> Modified VaR (95%) -0.0078 -0.0065 -0.0088
#> Modified ES (95%) -0.0155 -0.0129 -0.0206
#> Adaptive.Asset.Allocation
#> Annualized Return 0.0838
#> Annualized Std Dev 0.0903
#> Annualized Sharpe (Rf=0.53%) 0.8624
#> daily downside risk 0.0041
#> Annualised downside risk 0.0649
#> Downside potential 0.0018
#> Omega 1.1745
#> Sortino ratio 0.0766
#> Upside potential 0.0021
#> Upside potential ratio 0.6611
#> Omega-sharpe ratio 0.1745
#> Semi Deviation 0.0042
#> Gain Deviation 0.0037
#> Loss Deviation 0.0046
#> Downside Deviation (MAR=210%) 0.0097
#> Downside Deviation (Rf=0.53%) 0.0041
#> Downside Deviation (0%) 0.0041
#> Maximum Drawdown 0.1435
#> Historical VaR (95%) -0.0092
#> Historical ES (95%) -0.0144
#> Modified VaR (95%) -0.0095
#> Modified ES (95%) -0.0177
Visualizing allocations for all strategies:
chart.StackedBar(bt_ivy$rebalance_weights,
date.format = "%Y",
main = paste0("Allocations, ", bt_ivy$strat$name))
chart.StackedBar(bt_raa$rebalance_weights,
date.format = "%Y",
main = paste0("Allocations, ", bt_raa$strat$name))
chart.StackedBar(bt_dual_mo$rebalance_weights,
date.format = "%Y",
main = paste0("Allocations, ", bt_dual_mo$strat$name))
chart.StackedBar(bt_aaa$rebalance_weights,
date.format = "%Y",
main = paste0("Allocations, ", bt_aaa$strat$name))
In this example, we create a strategy that uses the minimum variance portfolio rule using U.S. stocks and bonds. At each rebalancing date, this strategy uses optimization to determine the weights that yield the minimum variance possible.
# Minimum variance portfolio
<- list(name = "US MinVar",
us_mvp tickers = c("VTI",
"BND"),
default_weights = c(0.5,
0.5),
rebalance_frequency = "month",
portfolio_rule_fn = min_variance)
<- backtest_allocation(us_mvp,
bt_us_mvp $Prices,
ETFs$Returns,
ETFs$risk_free)
ETFs
charts.PerformanceSummary(bt_us_mvp$returns)
$table_performance
bt_us_mvp#> US.MinVar
#> Annualized Return 0.0347
#> Annualized Std Dev 0.0464
#> Annualized Sharpe (Rf=0.52%) 0.6323
#> daily downside risk 0.0021
#> Annualised downside risk 0.0335
#> Downside potential 0.0008
#> Omega 1.1551
#> Sortino ratio 0.0564
#> Upside potential 0.0009
#> Upside potential ratio 0.5341
#> Omega-sharpe ratio 0.1551
#> Semi Deviation 0.0022
#> Gain Deviation 0.0022
#> Loss Deviation 0.0027
#> Downside Deviation (MAR=210%) 0.0086
#> Downside Deviation (Rf=0.52%) 0.0021
#> Downside Deviation (0%) 0.0021
#> Maximum Drawdown 0.1249
#> Historical VaR (95%) -0.0035
#> Historical ES (95%) -0.0063
#> Modified VaR (95%) NA
#> Modified ES (95%) NA
As expected, this strategy would invest heavily in bonds:
chart.StackedBar(bt_us_mvp$rebalance_weights,
date.format = "%Y",
main = paste0("Allocations, ", us_mvp$name))
Finally, in this example, we test a risk parity portfolio inspired on the RPAR ETF. As described in the prospectus, this ETF targets the following risk allocations:
Risk parity is implemented in the risk_parity
rebalancing function, which considers as risk budgets the values in the
default_weights element of the strategy.1 Our “clone” RPAR
strategy uses the following ETFs:
TIPS: TIP
Global Equities
US equities: VTI
Non-U.S. Developed Markets Equities: EFA
Emerging Markets Equities: EEM
Commodities
Commodities: DBC
Gold: GLD
U.S. Treasuries: IEF
The risk budgets are set to 25% for each of the four categories above, and equally per asset within each category:
<- list(name = "US Risk Parity",
rp tickers = c("TIP",
"VTI", "EFA", "EEM",
"DBC", "GLD",
"IEF"),
default_weights = c(0.25,
0.25/3, 0.25/3, 0.25/3,
0.25/2, 0.25/2,
0.25),
rebalance_frequency = "month",
portfolio_rule_fn = "risk_parity")
<- backtest_allocation(rp,
bt_rp $Prices,
ETFs$Returns,
ETFs$risk_free) ETFs
As expected, the volatility is quite low, since 50% of the risk budget is allocated to fixed income:
charts.PerformanceSummary(bt_rp$returns)
$table_performance
bt_rp#> US.Risk.Parity
#> Annualized Return 0.0447
#> Annualized Std Dev 0.0532
#> Annualized Sharpe (Rf=0.79%) 0.6875
#> daily downside risk 0.0023
#> Annualised downside risk 0.0371
#> Downside potential 0.0011
#> Omega 1.1356
#> Sortino ratio 0.0633
#> Upside potential 0.0012
#> Upside potential ratio 0.7082
#> Omega-sharpe ratio 0.1356
#> Semi Deviation 0.0024
#> Gain Deviation 0.0023
#> Loss Deviation 0.0025
#> Downside Deviation (MAR=210%) 0.0088
#> Downside Deviation (Rf=0.79%) 0.0023
#> Downside Deviation (0%) 0.0023
#> Maximum Drawdown 0.1550
#> Historical VaR (95%) -0.0050
#> Historical ES (95%) -0.0078
#> Modified VaR (95%) -0.0048
#> Modified ES (95%) -0.0073
We can check the correlation with the actual RPAR ETF:
<- get_data_from_tickers("RPAR")
rpar <- merge.xts(bt_rp$returns, rpar$R, join = "right")
rp_compare <- na.omit(rp_compare)
rp_compare cor(rp_compare)
#> US.Risk.Parity RPAR
#> US.Risk.Parity 1.000000 0.888787
#> RPAR 0.888787 1.000000
Despite the high correlation, RPAR has about twice the volatility of our strategy. The reason is that the ETF can rely on leverage through futures:
table.AnnualizedReturns(rp_compare)
#> US.Risk.Parity RPAR
#> Annualized Return 0.0430 0.0905
#> Annualized Std Dev 0.0631 0.1225
#> Annualized Sharpe (Rf=0%) 0.6814 0.7394
We can rescale the clone to have the same ex-post volatility for an apples-to-apples comparison:
<- table.AnnualizedReturns(rp_compare)[2,2]/table.AnnualizedReturns(rp_compare)[2,1]
rp_rescale_factor
1] <- rp_compare[, 1] * rp_rescale_factor
rp_compare[,
charts.PerformanceSummary(rp_compare)
Creating your own static asset allocation strategies is
straightforward. Just add the tickers and set
portfolio_rule_fn = "constant_weights"
. To create your own
dynamic/tactical rebalancing functions, you can look at the provided
functions to get some ideas. One important thing to keep in mind is to
ensure that the rebalancing function only uses data until each
rebalancing date, in order to avoid look-ahead bias. Take a look at the
tactical rebalancing functions in the package to see one way to achieve
this.
The risk_parity
function itself uses
functions from the riskParityPortfolio
package.↩︎