Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
This post will display my implementation of the Logical Invest Enhanced Bond Rotation strategy. This is a strategy that indeed does work, but is dependent on reinvesting dividends, as bonds pay coupons, which means bond ETFs do likewise.
The strategy is fairly simple — using four separate fixed income markets (long-term US government bonds, high-yield bonds, emerging sovereign debt, and convertible bonds), the strategy aims to deliver a low-risk, high Sharpe profile. Every month, it switches to two separate securities, in either a 60-40 or 50-50 split (that is, a 60-40 one way, or the other). My implementation for this strategy is similar to the ones I’ve done for the Logical Invest Universal Investment Strategy, which is to maximize a modified Sharpe ratio in a walk-forward process.
Here’s the code:
LogicInvestEBR <- function(returns, lowerBound, upperBound, period, modSharpeF) { count <- 0 configs <- list() instCombos <- combn(colnames(returns), m = 2) for(i in 1:ncol(instCombos)) { inst1 <- instCombos[1, i] inst2 <- instCombos[2, i] rets <- returns[,c(inst1, inst2)] weightSeq <- seq(lowerBound, upperBound, by = .1) for(j in 1:length(weightSeq)) { returnConfig <- Return.portfolio(R = rets, weights = c(weightSeq[j], 1-weightSeq[j]), rebalance_on="months") colnames(returnConfig) <- paste(inst1, weightSeq[j], inst2, 1-weightSeq[j], sep="_") count <- count + 1 configs[[count]] <- returnConfig } } configs <- do.call(cbind, configs) cumRets <- cumprod(1+configs) #rolling cumulative rollAnnRets <- (cumRets/lag(cumRets, period))^(252/period) - 1 rollingSD <- sapply(X = configs, runSD, n=period)*sqrt(252) modSharpe <- rollAnnRets/(rollingSD ^ modSharpeF) monthlyModSharpe <- modSharpe[endpoints(modSharpe, on="months"),] findMax <- function(data) { return(data==max(data)) } #configs$zeroes <- 0 #zeroes for initial periods during calibration 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) configCopy <- configs configCopy$zeroes <- 0 stratRets <- Return.portfolio(R = configCopy, weights = weights) return(stratRets) }
The one thing different about this code is the way I initialize the return streams. It’s an ugly piece of work, but it takes all of the pairwise combinations (that is, 4 choose 2, or 4c2) along with a sequence going by 10% for the different security weights between the lower and upper bound (that is, if the lower bound is 40% and upper bound is 60%, the three weights will be 40-60, 50-50, and 60-40). So, in this case, there are 18 configurations. 4c2*3. Do note that this is not at all a framework that can be scaled up. That is, with 20 instruments, there will be 190 different combinations, and then anywhere between 3 to 11 (if going from 0-100) configurations for each combination. Obviously, not a pretty sight.
Beyond that, it’s the same refrain. Bind the returns together, compute an n-day rolling cumulative return (far faster my way than using the rollApply version of Return.annualized), divide it by the n-day rolling annualized standard deviation divided by the modified Sharpe F factor (1 gives you Sharpe ratio, 0 gives you pure returns, greater than 1 puts more of a focus on risk). Take the highest Sharpe ratio, allocate to that configuration, repeat.
So, how does this perform? Here’s a test script, using the same 73-day lookback with a modified Sharpe F of 2 that I’ve used in the previous Logical Invest strategies.
symbols <- c("TLT", "JNK", "PCY", "CWB", "VUSTX", "PRHYX", "RPIBX", "VCVSX") suppressMessages(getSymbols(symbols, from="1995-01-01", src="yahoo")) etfClose <- Return.calculate(cbind(Cl(TLT), Cl(JNK), Cl(PCY), Cl(CWB))) etfAdj <- Return.calculate(cbind(Ad(TLT), Ad(JNK), Ad(PCY), Ad(CWB))) mfClose <- Return.calculate(cbind(Cl(VUSTX), Cl(PRHYX), Cl(RPIBX), Cl(VCVSX))) mfAdj <- Return.calculate(cbind(Ad(VUSTX), Ad(PRHYX), Ad(RPIBX), Ad(VCVSX))) colnames(etfClose) <- colnames(etfAdj) <- c("TLT", "JNK", "PCY", "CWB") colnames(mfClose) <- colnames(mfAdj) <- c("VUSTX", "PRHYX", "RPIBX", "VCVSX") etfClose <- etfClose[!is.na(etfClose[,4]),] etfAdj <- etfAdj[!is.na(etfAdj[,4]),] mfClose <- mfClose[-1,] mfAdj <- mfAdj[-1,] etfAdjTest <- LogicInvestEBR(returns = etfAdj, lowerBound = .4, upperBound = .6, period = 73, modSharpeF = 2) etfClTest <- LogicInvestEBR(returns = etfClose, lowerBound = .4, upperBound = .6, period = 73, modSharpeF = 2) mfAdjTest <- LogicInvestEBR(returns = mfAdj, lowerBound = .4, upperBound = .6, period = 73, modSharpeF = 2) mfClTest <- LogicInvestEBR(returns = mfClose, lowerBound = .4, upperBound = .6, period = 73, modSharpeF = 2) fiveStats <- function(returns) { return(rbind(table.AnnualizedReturns(returns), maxDrawdown(returns), CalmarRatio(returns))) } etfs <- cbind(etfAdjTest, etfClTest) colnames(etfs) <- c("Adjusted ETFs", "Close ETFs") charts.PerformanceSummary((etfs)) mutualFunds <- cbind(mfAdjTest, mfClTest) colnames(mutualFunds) <- c("Adjusted MFs", "Close MFs") charts.PerformanceSummary(mutualFunds) chart.TimeSeries(log(cumprod(1+mutualFunds)), legend.loc="topleft") fiveStats(etfs) fiveStats(mutualFunds)
So, first, the results of the ETFs:
Equity curve:
Five statistics:
> fiveStats(etfs) Adjusted ETFs Close ETFs Annualized Return 0.12320000 0.08370000 Annualized Std Dev 0.06780000 0.06920000 Annualized Sharpe (Rf=0%) 1.81690000 1.20980000 Worst Drawdown 0.06913986 0.08038459 Calmar Ratio 1.78158934 1.04078405
In other words, reinvesting dividends makes up about 50% of these returns.
Let’s look at the mutual funds. Note that these are for the sake of illustration only–you can’t trade out of mutual funds every month.
Equity curve:
Log scale:
Statistics:
Adjusted MFs Close MFs Annualized Return 0.11450000 0.0284000 Annualized Std Dev 0.05700000 0.0627000 Annualized Sharpe (Rf=0%) 2.00900000 0.4532000 Worst Drawdown 0.09855271 0.2130904 Calmar Ratio 1.16217559 0.1332706
In this case, day and night, though how much of it is the data source may also be an issue. Yahoo isn’t the greatest when it comes to data, and I’m not sure how much the data quality deteriorates going back that far. However, the takeaway seems to be this: with bond strategies, dividends will need to be dealt with, and when considering returns data presented to you, keep in mind that those adjusted returns assume the investor stays on top of dividend maintenance. Fail to reinvest the dividends in a timely fashion, and, well, the gap can be quite large.
To put it into perspective, as I was writing this post, I wondered whether or not most of this was indeed due to dividends. Here’s a plot of the difference in returns between adjusted and close ETF returns.
chart.TimeSeries(etfAdj - etfClose, legend.loc="topleft", date.format="%Y-%m", main = "Return differences adjusted vs. close ETFs")
With the resulting image:
While there may be some noise to the order of the negative fifth power on most days, there are clear spikes observable in the return differences. Those are dividends, and their compounding makes a sizable difference. In one case for CWB, the difference is particularly striking (Dec. 29, 2014). In fact, here’s a quick little analysis of the effect of the dividend effects.
dividends <- etfAdj - etfClose divReturns <- list() for(i in 1:ncol(dividends)) { diffStream <- dividends[,i] divPayments <- diffStream[diffStream >= 1e-3] divReturns[[i]] <- Return.annualized(divPayments) } divReturns <- do.call(cbind, divReturns) divReturns divReturns/Return.annualized(etfAdj)
And the result:
> divReturns TLT JNK PCY CWB Annualized Return 0.03420959 0.08451723 0.05382363 0.05025999 > divReturns/Return.annualized(etfAdj) TLT JNK PCY CWB Annualized Return 0.453966 0.6939243 0.5405922 0.3737499
In short, the effect of the dividend is massive. In some instances, such as with JNK, the dividend comprises more than 50% of the annualized returns for the security!
Basically, I’d like to hammer the point home one last time–backtests using adjusted data assume instantaneous maintenance of dividends. In order to achieve the optimistic returns seen in the backtests, these dividend payments must be reinvested ASAP. In short, this is the fine print on this strategy, and is a small, but critical detail that the SeekingAlpha article doesn’t mention. (Seriously, do a ctrl + F in your browser for the word “dividend”. It won’t come up in the article itself.) I wanted to make sure to add it.
One last thing: gaudy numbers when using monthly returns!
> fiveStats(apply.monthly(etfs, Return.cumulative)) Adjusted ETFs Close ETFs Annualized Return 0.12150000 0.082500 Annualized Std Dev 0.06490000 0.067000 Annualized Sharpe (Rf=0%) 1.87170000 1.232100 Worst Drawdown 0.03671871 0.049627 Calmar Ratio 3.30769620 1.662642
Look! A Calmar Ratio of 3.3, and a Sharpe near 2!*
*: Must manage dividends. Statistics reported are monthly.
Okay, in all fairness, this is a pretty solid strategy, once one commits to managing the dividends. I just felt that it should have been a topic made front and center considering its importance in this case, rather than simply swept under the “we use adjusted returns” rug, since in this instance, the effect of dividends is massive.
In conclusion, while I will more or less confirm the strategy’s actual risk/reward performance (unlike some other SeekingAlpha strategies I’ve backtested), which, in all honesty, I find really impressive, it comes with a caveat like the rest of them. However, the caveat of “be detail-oriented/meticulous/paranoid and reinvest those dividends!” in my opinion is a caveat that’s a lot easier to live with than 30%+ drawdowns that were found lurking in other SeekingAlpha strategies. So for those that can stay on top of those dividends (whether manually, or with machine execution), here you go. I’m basically confirming the performance of Logical Invest’s strategy, but just belaboring one important detail.
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.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.