Portfolio Analysis in R | A 60/40 US Stock/Bond Portfolio

How’s that rebalancing strategy working out for ‘ya? Results will vary, of course, depending on when we run the analysis, the architecture of the strategy, and a number of other variables. Deciding if the results are satisfying or disappointing could be due to any number of factors, such as the choice of assets, the rebalancing rules, or the selection of ETFs to implement the strategy. But whether you’re running a backtest to kick the tires on a rebalancing idea or monitoring an existing portfolio in real time, keeping an eye on risk and return–and understanding what’s driving results–is critical. The good news is that this essential task is relatively easy thanks to R, the data analysis software. As a brief illustration, let’s kick the tires on a simple 60%/40% US stock/bond strategy with a couple of ETFs.

The recent upgrade of the PerformanceAnalytics package in particular is a major leap forward for dissecting the finer points of rebalancing strategies. As an investor/journalist/consultant who once upon a time ran portfolio analytics in Excel, I can now see that shifting to R is the equivalent of trading in the horse and buggy for a private jet in one’s econometric travels in finance and economics. The combination of running a spectrum of sophisticated analytics quickly and with minimal brain damage with regards to data management is a huge plus with R. To understand why, I’ll be writing a series of posts going forward that explore the possibilities of portfolio analytics in R. But the 1,000-mile journey begins with the first step.

As always, we need data. To start our test, let’s download the daily price history for two ETFs to populate our 60/40 portfolio: SPDR S&P 500 ETF (SPY) and the iShares Core US Aggregate Bond (AGG).

[code language=”r”]

# rebalancing analysis for 60/40 US stock/bond portfolio

# load packages
library(quantmod)
library(tseries)
library(PerformanceAnalytics)

# download prices
spy <-get.hist.quote(instrument="spy",start="2003-12-31",quote="AdjClose",compression="d")
agg <-get.hist.quote(instrument="agg",start="2003-12-31",quote="AdjClose",compression="d")

# choose asset weights
w = c(0.6,0.4) # 60% / 40%

# merge price histories into one dataset
# calculate 1-day % returns
# and label columns
portfolio.prices <-as.xts(merge(spy,agg))
portfolio.returns <-na.omit(ROC(portfolio.prices,1,"discrete"))
colnames(portfolio.returns) <-c("spy","agg")

[/code]

Using the Return.portfolio function in the PerformanceAnalytics package, the next step is generating portfolio results for two strategies that begin with a 60/40 asset allocation on Dec. 31, 2003. The first portfolio is simply a buy-and-hold strategy; the second is rebalancing back to 60/40 weights every Dec. 31.

[code language=”r”]
# calculate portfolio total returns
# rebalanced portfolio
portfolio.rebal <-Return.portfolio(portfolio.returns,
rebalance_on="years",
weights=w,wealth.index=TRUE,verbose=TRUE)

# buy and hold portfolio/no rebalancing
portfolio.bh <-Return.portfolio(portfolio.returns,
weights=w,wealth.index=TRUE,verbose=TRUE)

# merge portfolio returns into one dataset
# label columns
portfolios.2 <-cbind(portfolio.rebal$returns,portfolio.bh$returns)
colnames(portfolios.2) <-c("rebalanced","buy and hold")

[/code]

An obvious way to begin: compare the investment results of the two strategies. Here’s how a $1 investment in each of the portfolios compared from the end of 2003 through Jan. 16, 2015. Note the modestly higher performance in the rebalanced strategy.

[code language=”r”]
chart.CumReturns(portfolios.2,
wealth.index=TRUE,
legend.loc="bottomright",
main="Growth of $1 investment",
ylab="$")
[/code]

graph.a.19jan2015

For a quick comparison of risk and return, we might run the table.AnnualizedReturns function, which shows that the rebalanced strategy has a modest performance edge.

[code language=”r”]
# Compare return/risk
table.AnnualizedReturns(portfolios.2)

rebalanced buy and hold
Annualized Return 0.0695 0.0653
Annualized Std Dev 0.1112 0.1118
Annualized Sharpe (Rf=0%) 0.6250 0.5840
[/code]

Another perspective on risk is looking at drawdown (DD), which measures the peak-to-trough declines. Here’s how the top-five drawdowns stack up for the sample period. It’s interesting to note that the rebalanced portfolio’s worst drawdown was slightly shorter and less painful vs. the buy-and-hold strategy’s deepest DD.

[code language=”r”]
# Review biggest drawdowns

table.Drawdowns((portfolio.bh$returns)) # buy and hold drawdowns
From Trough To Depth Length To Trough Recovery
1 2007-10-10 2009-03-09 2011-01-27 -0.3417 832 355 477
2 2011-07-25 2011-08-08 2012-01-12 -0.0935 120 11 109
3 2007-07-20 2007-08-15 2007-09-19 -0.0570 43 19 24
4 2004-03-08 2004-05-10 2004-11-03 -0.0533 168 45 123
5 2012-04-03 2012-06-04 2012-07-27 -0.0502 81 43 38

table.Drawdowns((portfolio.rebal$returns)) # rebal drawdowns
From Trough To Depth Length To Trough Recovery
1 2007-10-10 2009-03-09 2010-11-02 -0.3371 773 355 418
2 2011-07-25 2011-10-03 2012-01-12 -0.0964 120 50 70
3 2012-04-03 2012-06-04 2012-07-27 -0.0536 81 43 38
4 2004-03-08 2004-05-10 2004-11-03 -0.0533 168 45 123
5 2007-07-20 2007-08-15 2007-09-18 -0.0524 42 19 23
[/code]

We can also run a battery of tests to model portfolio results for a deeper look into what going on under the performance hood. For instance, let’s consider the distribution of the rebalanced portfolio’s returns. By using the qqnorm function, we can visually inspect how a portfolio’s performance compares with a normal distribution, a test that provides a quick summary of the incidence of fat tails. As you can see in the next chart below, the rebalanced portfolio suffers from a degree of fat tails (black circles). If the returns were normally distributed, the black circles would align with the red line. That’s what we see for most of the returns, but at the extremes there’s a clear divergence from the red line, which tells us that we should do some additional modeling/testing to investigate the potential for trouble related to fat tails.

[code language=”r”]
# Compare portfolio return distribution vs. normal distribution
qqnorm(portfolio.rebal$returns,main="Rebalanced Portfolio")
qqline(portfolio.rebal$returns,col="red")
[/code]

qqnorm.19jan2015

Another valuable metric is rolling one-year return, which offers some perspective on how a strategy evolves without the distracting short-term noise of daily volatility. By this standard, the two strategies look similar. Of course, it’s worth mentioning that the similarity in annual returns masks the fact that the rebalanced strategy ultimately delivered a slightly superior return through time.

[code language=”r”]
# Compare rolling 1-year returns
chart.RollingPerformance(portfolios.2,width=252,
legend.loc="bottomright",
main="Rolling 1yr % returns")
[/code]

one.yr.ret.19jan2015

The analytics above barely scratch the surface of what’s available in R. In future posts, I’ll continue to explore how we might enhance the 60/40 portfolio and run a series of quantitative tests to validate (or reject) the results. As we’ll see, there are a variety of improvements we can make that fall under two broad headings: adding asset classes and tweaking the rebalancing rules. The acid test, of course, is generating superior risk-adjusted returns that stand up to rigorous econometric tests, some of which I’ll outline in the weeks ahead. Easier said than done, although necessary if we’re intent on developing a high degree of confidence in a given strategy. But thanks to the broad and deep resources in R’s econometric toolkit, the odds are a bit higher than you might expect for juicing results relative to what we can easily tap into with a basic 60/40 strategy.

10 thoughts on “Portfolio Analysis in R | A 60/40 US Stock/Bond Portfolio

  1. Ilya Kipnis

    I…did not know about the table.AnnualizedReturns functionality, since I actually use something quite similar on my blog (I leave off standard deviation and substitute in max drawdown), but the drawdown table looks especially interesting!

    Do you do more posts in R in this type of style? Because this one is well done.

    -Ilya (author of quantStrat TradeR)

  2. Pingback: The Whole Street’s Daily Wrap for 1/19/2015 | The Whole Street

  3. James Picerno Post author

    Ilya,
    I do a lot of coding in R on macro and portfolio topics, but have only posted a handful of articles on CapitalSpectator.com over the years. But I’ll be writing more, in part to extend out this piece as an excuse to investigate/analyze how to use R to enhance a simple 60/40 portfolio. Thanks for reading!
    –JP

  4. Dave Bristor

    Excellent post! I really look forward to following your progress on this. Any thoughts to making some of it available via an RStudio app? Perhaps allowing user-provided choice of securities, weights, and rebalancing time periods? I could be interested in helping.

  5. James Picerno Post author

    Dave,
    Sounds like a job for Shiny R. In theory, great idea. In fact, I’ve been thinking about something along those lines. Now if I could only figure out how to invent the 30-hour day! 🙂
    –JP

  6. CaxM

    James – first thanks for such a well written piece. I’m a R novice and as a means of seeing its application for portf. management tried to simply tried to cut and paste your code as an test case. However i get this error whilst trying to chart portfolios.2:

    chart.CumReturns(portfolios.2,
    + wealth.index=TRUE,
    + legend.loc=”bottomright”,
    + main=”Growth of $1 investment”,
    + ylab=”$”)
    Error in checkData(R) :
    The data cannot be converted into a time series. If you are trying to pass in names from a data object with one column, you should use the form ‘data[rows, columns, drop = FALSE]’. Rownames should have standard date formats, such as ‘1985-03-15’.

    Portfolios.2 returns NULL also

    I’ve not changed anything within your code in anyway so I would, if you have the time, please enlighten me into what im doing wrong.

    Thanks

  7. James Picerno Post author

    Max, the code as shown should work fine. Double check that you’re copying and pasting without adding any characters. As a test, try charting the data at the most basic level:

    chart.CumReturns(portfolios.2)

    This should work; if not, you may have some issue specific to your computer. Good luck.

    –JP

  8. Pingback: Using an Allocation Table in R PerformanceAnalytics | KennethKlabunde

Comments are closed.