I’m sure we’ve all heard about diversified stock and bond portfolios. In its simplest, most diluted form, it can be comprised of the SPY and TLT etfs. The concept introduced by Logical Invest, in a Seeking Alpha article written by Frank Grossman (also see link here), essentially uses a walk-forward methodology of maximizing a modified Sharpe ratio, biased heavily in favor of the volatility rather than the returns. That is, it uses a 72-day moving window to maximize total returns between different weighting configurations of a SPY-TLT mix over the standard deviation raised to the power of 5/2. To put it into perspective, at a power of 1, this is the basic Sharpe ratio, and at a power of 0, just a momentum maximization algorithm.

The process for this strategy is simple: rebalance every month on some multiple of 5% between SPY and TLT that previously maximized the following quantity (returns/vol^2.5 on a 72-day window).

Here’s the code for obtaining the data and computing the necessary quantities:

require(quantmod) require(PerformanceAnalytics) getSymbols(c("SPY", "TLT"), from="1990-01-01") returns <- merge(Return.calculate(Ad(SPY)), Return.calculate(Ad(TLT)), join='inner') returns <- returns[-1,] configs <- list() for(i in 1:21) { weightSPY <- (i-1)*.05 weightTLT <- 1-weightSPY config <- Return.portfolio(R = returns, weights=c(weightSPY, weightTLT), rebalance_on = "months") configs[[i]] <- config } configs <- do.call(cbind, configs) cumRets <- cumprod(1+configs) period <- 72 roll72CumAnn <- (cumRets/lag(cumRets, period))^(252/period) - 1 roll72SD <- sapply(X = configs, runSD, n=period)*sqrt(252)

Next, the code for creating the weights:

sd_f_factor <- 2.5 modSharpe <- roll72CumAnn/roll72SD^sd_f_factor monthlyModSharpe <- modSharpe[endpoints(modSharpe, on="months"),] findMax <- function(data) { return(data==max(data)) } weights <- t(apply(monthlyModSharpe, 1, findMax)) weights <- weights*1 weights <- xts(weights, order.by=as.Date(rownames(weights))) weights[is.na(weights)] <- 0 weights$zeroes <- 1-rowSums(weights) configs$zeroes <- 0

That is, simply take the setting that maximizes the monthly modified Sharpe Ratio calculation at each rebalancing date (the end of every month).

Next, here’s the performance:

stratRets <- Return.portfolio(R = configs, weights = weights) rbind(table.AnnualizedReturns(stratRets), maxDrawdown(stratRets)) charts.PerformanceSummary(stratRets)

Which gives the results:

> rbind(table.AnnualizedReturns(stratRets), maxDrawdown(stratRets)) portfolio.returns Annualized Return 0.1317000 Annualized Std Dev 0.0990000 Annualized Sharpe (Rf=0%) 1.3297000 Worst Drawdown 0.1683851

With the following equity curve:

Not perfect, but how does it compare to the ingredients?

Let’s take a look:

stratAndComponents <- merge(returns, stratRets, join='inner') charts.PerformanceSummary(stratAndComponents) rbind(table.AnnualizedReturns(stratAndComponents), maxDrawdown(stratAndComponents)) apply.yearly(stratAndComponents, Return.cumulative)

Here are the usual statistics:

> rbind(table.AnnualizedReturns(stratAndComponents), maxDrawdown(stratAndComponents)) SPY.Adjusted TLT.Adjusted portfolio.returns Annualized Return 0.0907000 0.0783000 0.1317000 Annualized Std Dev 0.1981000 0.1381000 0.0990000 Annualized Sharpe (Rf=0%) 0.4579000 0.5669000 1.3297000 Worst Drawdown 0.5518552 0.2659029 0.1683851

In short, it seems the strategy performs far better than either of the ingredients. Let’s see if the equity curve comparison reflects this.

Indeed, it does. While it does indeed have the drawdown in the crisis, both instruments were in drawdown at the time, so it appears that the strategy made the best of a bad situation.

Here are the annual returns:

> apply.yearly(stratAndComponents, Return.cumulative) SPY.Adjusted TLT.Adjusted portfolio.returns 2002-12-31 -0.02054891 0.110907611 0.01131366 2003-12-31 0.28179336 0.015936985 0.12566042 2004-12-31 0.10695067 0.087089794 0.09724221 2005-12-30 0.04830869 0.085918063 0.10525398 2006-12-29 0.15843880 0.007178861 0.05294557 2007-12-31 0.05145526 0.102972399 0.06230742 2008-12-31 -0.36794099 0.339612265 0.19590423 2009-12-31 0.26352114 -0.218105306 0.18826736 2010-12-31 0.15056113 0.090181150 0.16436950 2011-12-30 0.01890375 0.339915713 0.24562838 2012-12-31 0.15994578 0.024083393 0.06051237 2013-12-31 0.32303535 -0.133818884 0.13760060 2014-12-31 0.13463980 0.273123290 0.19637382 2015-02-20 0.02773183 0.006922893 0.02788726

2002 was an incomplete year. However, what’s interesting here is that on a whole, while the strategy rarely if ever does as well as the better of the two instruments, it always outperforms the worse of the two instruments–and not only that, but it has delivered a positive performance in every year of the backtest–even when one instrument or the other was taking serious blows to performance, such as SPY in 2008, and TLT in 2009 and 2013.

For the record, here is the weight of SPY in the strategy.

weightSPY <- apply(monthlyModSharpe, 1, which.max) weightSPY <- do.call(rbind, weightSPY) weightSPY <- (weightSPY-1)*.05 align <- cbind(weightSPY, stratRets) align <- na.locf(align) chart.TimeSeries(align[,1], date.format="%Y", ylab="Weight SPY", main="Weight of SPY in SPY-TLT pair")

Now while this may serve as a standalone strategy for some people, the takeaway in my opinion from this is that dynamically re-weighting two return streams that share a negative correlation can lead to some very strong results compared to the ingredients from which they were formed. Furthermore, rather than simply rely on one number to summarize a relationship between two instruments, the approach that Frank Grossman took to actually model the combined returns was one I find interesting, and undoubtedly has applications as a general walk-forward process.

Thanks for reading.

NOTE: I am a freelance consultant in quantitative analysis on topics related to this blog. If you have contract or full time roles available for proprietary research that could benefit from my skills, please contact me through my LinkedIn here.