Testing static asset allocations

Simple Buy-and-Hold Asset Allocations

In this Markdown, I test 16 simple ``buy-and-hold'' asset allocation strategies using ETF data. This post was inspired by a thread on Twitter which provided descriptions and some performance stats for these constant allocation portfolios.1 The allocations are the following (ETF names/asset classes are provided at the end):

  1. Golden Butterfly: SHY(20%), TLT (20%), VTI (20%), IWN (20%), GLD (20%)

  2. Rob Arnott Portfolio: BNDX (20%), LQD (20%), VEU (10%), VNQ (10%), SPY (10%), TLT (10%), TIP (10%), DBC(10%)

  3. Global Asset Allocation Portfolio: SPY (18%), EFA (13.5%) , EEM (4.5%) , LQD (19.8%) , BNDX (14.4%) , TLT (13.5%) , TIP (1.8%) , DBC (5%) , GLD (5%) , VNQ (4.5% )

  4. Permanent Portfolio: BIL (25%), GLD(25%), TLT (25%), SPY (25%)

  5. Desert Portfolio: IEF (60%), VTI(30%), GLD(10%)

  6. The Larry Portfolio: IWN (15%), DLS (7.5%), EEM (7.5%), IEF (70%)

  7. Big Rocks Portfolio: AGG (60%), SPY (6%), IWD (6%), IWM (6%), IWN (6%), EFV (4%), VNQ (4%), EFA (2%), SCZ (2%), DLS (2%), EEM (2%)

  8. Sandwich Portfolio: IEF (41%), SPY (20%), SCZ (10%), IWM (8%), EEM (6%), EFA (6%), VNQ (5%), BIL (4%)

  9. Balanced - Tax Aware Portfolio: AGG (38%), SPY (15%), BIL (15%), EFA (13%), IWM (5%), VNQ (5%), DBC (5%), EEM (4%)

  10. Balanced Portfolio: AGG (33%), SPY (15%), BIL (15%), EFA (13%), IWM (5%), VNQ (5%), DBC (5%), EEM (4%), TIP (2%), BNDX (2%), HYG (1%)

  11. Income With Growth Portfolio: AGG (37%), BIL (20%), TIP (10%), SPY (9%), EFA (8%), VNQ (5%), HYG (4%), BNDX (4%), IWM (2%), DBC (1%)

  12. Income Growth Tax Portfolio: AGG (55%), BIL (20%), SPY (9%), EFA (8%), VNQ (5%), IWM (2%), DBC (1%)

  13. Conservative Income Portfolio: AGG (40%), BIL (25%), TIP (18%), HYG (7%), VNQ (5%), BNDX (5%)

  14. Conservative Income Tax Portfolio: AGG (70%), BIL (25%), VNQ (5%)

  15. All Weather Portfolio: SPY (30%), TLT (40%), IEF (15%), GLD (7.5%), DBC (7.5%)

  16. United Stated 60/40 Portfolio: SPY (60%), IEF (40%)

These portfolios are simple to implement, as they involve only a few ETFs. However they require rebalancing to maintain the desired allocations. I assume rebalancing is done at the end of each month to facilitate the calculation of returns. I do not consider transaction costs.

Representing each strategy

The first step is to create an object with each strategy. I use a simple list with the tickers and weights:

# create each allocation strategy as a list of tickers and weights
golden_butterfly <- list(tickers = c("SHY", "TLT", "VTI", "IWN", "GLD"), 
                        weights = c(0.20, 0.20, 0.20, 0.20, 0.20))

rob_arnott <- list(tickers = c("BNDX", "LQD", "VEU", "VNQ", "VNQ", "SPY", "TLT", "TIP", "DBC"), 
                   weights = c(0.20, 0.20, 0.10, 0.10, 0.10, 0.10, 0.10, 0.10, 0.10))

globalAA <- list(tickers = c("SPY", "EFA", "EEM", "LQD", "BNDX", "TLT", "TIP", "DBC", "GLD", "VNQ"), 
                 weights = c(0.18, 0.135, 0.045, 0.18, 0.144, 0.135, 0.018, 0.05, 0.05, 0.045))

permanent <- list(tickers = c("BIL", "GLD", "TLT", "SPY"),
                  weights = c(0.25, 0.25, 0.25, 0.25))

desert <- list(tickers = c("IEF", "VTI", "GLD"), 
               weights = c(0.60, 0.30, 0.10))

larry <- list(tickers = c("IWN", "DLS", "EEM", "IEF"),
              weights = c(0.15, 0.075, 0.075, 0.70))

big_rocks <- list(tickers = c("AGG", "SPY", "IWD", "IWM", "IWN", "EFV", "VNQ", "EFA", "SCZ", "DLS", "EEM"), 
                  weights = c(0.60, 0.06, 0.06, 0.06, 0.06, 0.04,0.04, 0.02, 0.02, 0.02, 0.02))

sandwich <- list(tickers = c("IEF", "SPY", "SCZ", "IWM", "EEM", "EFA", "VNQ", "BIL"),
                  weights = c(0.41, 0.20, 0.10, 0.08, 0.06, 0.06, 0.05, 0.04))


balanced_tax <- list(tickers = c("AGG", "SPY", "BIL", "EFA", "IWM", "VNQ", "DBC", "EEM" ), 
                           weights = c(0.38, 0.15, 0.15, 0.13, 0.05, 0.05, 0.05, 0.04))

balanced <- list(tickers = c("AGG", "SPY", "BIL", "EFA", "IWM", "VNQ", "DBC", "EEM", "TIP", "BNDX", "HYG"), 
                 weights = c(0.33, 0.15, 0.15, 0.13, 0.05, 0.05, 0.05, 0.04, 0.02, 0.02, 0.01))

income_gr <- list(tickers = c("AGG", "BIL", "TIP", "SPY", "EFA", "VNQ", "HYG", "BNDX", "IWM", "DBC"), 
                      weights = c(0.37, 0.20, 0.10, 0.09, 0.08, 0.05, 0.04, 0.04, 0.02, 0.01))

income_gr_tax <- list(tickers = c("AGG", "BIL", "SPY", "EFA", "VNQ", "IWM", "DBC"), 
                          weights = c(0.55, 0.20, 0.09, 0.08, 0.05, 0.02, 0.01))

con_income  <- list(tickers = c("AGG", "BIL", "TIP", "HYG", "VNQ", "BNDX"), 
                             weights = c(0.40, 0.25, 0.18, 0.07, 0.05, 0.05))

con_income_tax  <- list(tickers = c("AGG", "BIL", "VNQ"), 
                    weights = c(0.70, 0.25, 0.05))

all_weather  <- list(tickers = c("SPY", "TLT", "IEF", "GLD", "DBC"), 
                     weights = c(0.30, 0.40, 0.15, 0.075, 0.075))

us_60_40 <- list(tickers = c("SPY", "IEF"), 
                  weights = c(0.60, 0.40))

Retrieving the data

To download the data, I use the getSymbols function from the quantmod package. I also load the PerformanceAnalytics package, which makes calculation of performance metrics trivial. I keep only end-of-month data and then calculate the monthly returns of all ETFs. I also download the yield on the 3-month T-bill from FRED and align it with the ETF data.

library(quantmod)
library(PerformanceAnalytics)

# get all the unique tickers
tickers <- unique(c(golden_butterfly$tickers,
                    rob_arnott$tickers,
                    globalAA$tickers,
                    permanent$tickers,
                    desert$tickers,
                    larry$tickers,
                    big_rocks$tickers,
                    sandwich$tickers,
                    balanced_tax$tickers,
                    balanced$tickers,
                    income_gr$tickers,
                    income_gr_tax$tickers,
                    con_income$tickers,
                    all_weather$tickers,
                    us_60_40$tickers))

# download prices for all tickers from Yahoo Finance
getSymbols(tickers, from = "2007-06-01", source = 'yahoo')
##  [1] "SHY"  "TLT"  "VTI"  "IWN"  "GLD"  "BNDX" "LQD"  "VEU"  "VNQ"  "SPY" 
## [11] "TIP"  "DBC"  "EFA"  "EEM"  "BIL"  "IEF"  "DLS"  "AGG"  "IWD"  "IWM" 
## [21] "EFV"  "SCZ"  "HYG"
# align all prices into one xts object
prices <- xts()
for (i in 1:length(tickers)){
  prices  <- merge.xts(prices, get(tickers[i])[,6])
}
colnames(prices) <- tickers

# keep only month ends - could do it daily but who's got time?
prices <- prices[endpoints(prices, on = "months"),]

#calculate returns
returns <- CalculateReturns(prices)

# download risk-free (3-month Tbill from FRED) and align with monthly frequency
getSymbols("DGS3MO", src = "FRED")
## [1] "DGS3MO"
tbill <- DGS3MO[index(returns)]/100/12

Calculating the returns of each strategy

I create a function to calculate the returns of each allocation strategy. Since the portfolios are rebalanced on a monthly basis, the monthly returns can be obtained simply by multiplying the weights by the corresponding ETF returns. I start calculation of returns from the date when all ETF returns are available.

calculate_strat_returns <- function(strat, returns){

  dates <- index(returns)
  
  # convention: start the backtest when data for all assets is available
  returns_strat <- returns[, strat$tickers]
  first_index <- which.max((!is.na(rowSums(returns_strat))))
  
  n_assets <- length(strat$tickers)
  weights <- rbind(matrix(NA, nrow = first_index - 1, ncol = n_assets), 
                   matrix(strat$weights, nrow = nrow(returns) - first_index + 1,
                          ncol = n_assets, byrow = TRUE))
  
  strat_returns <- xts(rowSums(weights * returns_strat), order.by = index(returns))
  return(strat_returns)
}

I then loop through each strategy and calculate their returns:

strats <- c("golden_butterfly",
            "rob_arnott",
            "globalAA",
            "permanent",
            "desert",
            "larry",
            "big_rocks",
            "sandwich",
            "balanced_tax",
            "balanced",
            "income_gr",
            "income_gr_tax",
            "con_income",
            "con_income_tax",
            "all_weather",
            "us_60_40")

# calculate returns of all strategies
strat_returns <- xts()
for (i in 1:length(strats)){
  this_strat <- calculate_strat_returns(get(strats[i]), returns)
  strat_returns <- merge.xts(strat_returns, 
                             this_strat)
}
colnames(strat_returns) <- strats

Performance of different strategies

Now I can calculate some performance metrics using functions from PerformanceAnalytics and display the results in a table. The annualized returns range from 2.63% for the Conservative Income strategy to 8.15% for the United Stated 60/40 strategy. The Desert portfolio produces the highest Sharpe ratio (1.04), while the Sandwich portfolio produces the lowest (0.33).

# calculate some statistics
table1 <- table.AnnualizedReturns(strat_returns, Rf = tbill)
table2 <- table.DownsideRiskRatio(strat_returns, MAR = mean(tbill))
table3 <- table.DownsideRisk(strat_returns, Rf = mean(tbill))

table_metrics <- rbind(table1,
                       table2[c("Annualised downside risk",
                                "Sortino ratio"), ],
                       table3[c("Historical VaR (95%)",
                                "Historical ES (95%)",
                                "Maximum Drawdown"),])

library(kableExtra)
kbl(t(table_metrics), caption =  "Performance metrics for buy & hold asset allocation strategies") %>%
  kable_classic() 
Performance metrics for buy & hold asset allocation strategies
Annualized ReturnAnnualized Std DevAnnualized Sharpe (Rf=0.68%)Annualised downside riskSortino ratioHistorical VaR (95%)Historical ES (95%)Maximum Drawdown
golden_butterfly0.07210.07950.81940.05070.3774-0.0289-0.05030.1663
rob_arnott0.06640.07610.77770.04930.3548-0.0302-0.04430.1194
globalAA0.06520.06770.85780.04170.4069-0.0266-0.03790.0930
permanent0.06610.06890.86040.04040.4269-0.0246-0.03710.1280
desert0.06780.05891.03370.03580.4890-0.0207-0.03460.1113
larry0.05070.06010.73050.03980.3232-0.0254-0.03930.1295
big_rocks0.05200.07690.60320.05260.2577-0.0298-0.05260.2249
sandwich0.06090.09160.60250.06270.2612-0.0383-0.06230.2895
balanced_tax0.04630.08010.49340.05590.2161-0.0347-0.05580.2704
balanced0.05470.06350.75040.04140.3393-0.0251-0.03730.1062
income_gr0.04240.04130.85640.02620.3927-0.0149-0.02380.0586
income_gr_tax0.04020.04940.67900.03300.2973-0.0181-0.03320.1437
con_income0.02630.02830.68610.01760.3220-0.0109-0.01550.0375
con_income_tax0.03130.03240.76050.01940.3664-0.0113-0.01860.0468
all_weather0.07400.07420.90360.04720.4138-0.0286-0.04510.1363
us_60_400.08060.08850.83170.05760.3754-0.0370-0.05800.2946

Next I plot the cumulative returns and drawdowns of the strategies. The drawdowns in 2007-2008 for most strategies are in the 10-20% range. The United Stated 60/40, which is considered by many as a good constant allocation benchmark, produces the highest drawdown at 29.5%.

# plot cumulative returns
chart.CumReturns(strat_returns, 
                 begin = "axis", 
                 legend.loc = "topleft")
# drawdowns
chart.Drawdown(strat_returns,
               legend.loc = "bottomright")

Finally, I zoom in on the recent period starting in 2020. Most strategies lose up to 10% during the first months of 2020 as the pandemic hits. Since most strategies have sizable allocations to bonds, it’s not surprising to see how they have all suffered since the end of 2021, as rates start to increase.

# plot cumulative returns
chart.CumReturns(strat_returns["2020/"], 
                 begin = "axis", 
                 legend.loc = "topleft")
chart.Drawdown(strat_returns["2020/"],
               legend.loc = "bottomright")

Conclusion

In this short R Markdown, I calculate returns and performance metrics of 16 popular static asset allocation strategies. These strategies are simple to implement using ETFs, and several of them have produced very decent risk-adjusted returns in the past. The million-dollar question is, what kind of allocation will perform well in the current (and unprecedented) environment?

Some interesting extensions: calculate returns on a daily basis and also for tactical asset allocation schemes, such as the Ivy portfolio.

List of ETFs

TICKERFund NameFund TypeGeographic Focus
AGGiShares Core US Aggregate Bond ETFBondUnited States of America
BILSPDR Bloomberg 1-3 Month T-Bill ETFBondUnited States of America
BNDXVanguard Total International Bond Index Fund;ETFBondGlobal Ex US
DBCInvesco DB Commodity Index Tracking FundCommodityUnited States of America
DLSWisdomTree International SmallCap Dividend FundEquityGlobal Ex US
EEMiShares MSCI Emerging Markets ETFEquityGlobal Emerging Markets
EFAiShares MSCI EAFE ETFEquityGlobal Ex US
EFViShares MSCI EAFE Value ETFEquityGlobal Ex US
GLDSPDR Gold SharesCommodityGlobal
HYGiShares iBoxx $ High Yield Corporate Bond ETFBondUnited States of America
IEFiShares 7-10 Year Treasury Bond ETFBondUnited States of America
IWDiShares Russell 1000 Value ETFEquityUnited States of America
IWMiShares Russell 2000 ETFEquityUnited States of America
IWNiShares Russell 2000 Value ETFEquityUnited States of America
LQDiShares iBoxx $ Inv Grade Corporate Bond ETFBondUnited States of America
SCZiShares MSCI EAFE Small-Cap ETFEquityGlobal Ex US
SHYiShares 1-3 Year Treasury Bond ETFBondUnited States of America
SPYSPDR S&P 500 ETF TrustEquityUnited States of America
TIPiShares TIPS Bond ETFBondUnited States of America
TLTiShares 20+ Year Treasury Bond ETFBondUnited States of America
VEUVanguard FTSE All-World ex US Index Fund;ETFEquityGlobal Ex US
VNQVanguard Real Estate Index Fund;ETFEquityUnited States of America
VTIVanguard Total Stock Market Index Fund;ETFEquityUnited States of America

  1. The thread was posted by @WifeyAlpha which is currently a locked account. ↩︎

Alexandre Rubesam (since 2021/04)
Alexandre Rubesam (since 2021/04)
Professor de Finanças
Próximo
Anterior

Relacionados