This post will cover ideas from two individuals: David Varadi of CSS Analytics with whom I am currently collaborating on some volatility trading strategies (the extent of which I hope will end up as a workable trading strategy–my current replica of some of VolatilityMadeSimple’s publicly displayed “example” strategies (note, from other blogs, not to be confused with their proprietary strategy) are something that I think is too risky to be traded as-is), and Cesar Alvarez, of Alvarez Quant Trading. If his name sounds familiar to some of you, that’s because it should. He used to collaborate (still does?) with Larry Connors of TradingMarkets.com, and I’m pretty sure that sometime in the future, I’ll cover those strategies as well.
The strategy for this post is simple, and taken from this post from CSS Analytics.
Pretty straightforward–compute a 20-day SMA on the SPY (I use unadjusted since that’s what the data would have actually been). When the SPY’s close crosses above the 20-day SMA, buy the high-yield bond index, either VWEHX or HYG, and when the converse happens, move to the cash-substitute security, either VFISX or SHY.
Now, while the above paragraph may make it seem that VWEHX and HYG are perfect substitutes, well, they aren’t, as no two instruments are exactly alike, which, as could be noted from my last post, is a detail that one should be mindful of. Even creating a synthetic “equivalent” is never exactly perfect. Even though I try my best to iron out such issues, over the course of generally illustrating an idea, the numbers won’t line up exactly (though hopefully, they’ll be close). In any case, it’s best to leave an asterisk whenever one is forced to use synthetics for the sake of a prolonged backtest.
The other elephant/gorilla in the room (depending on your preference for metaphorical animals), is whether or not to use adjusted data. The upside to that is that dividends are taken into account. The *downside* to that is that the data isn’t the real data, and also assumes a continuous reinvestment of dividends. Unfortunately, shares of a security are not continuous quantities–they are discrete quantities made so by their unit price, so the implicit assumptions in adjusted prices can be optimistic.
For this particular topic, Cesar Alvarez covered it exceptionally well on his blog post, and I highly recommend readers give that post a read, in addition to following his blog in general. However, just to illustrate the effect, let’s jump into the script.
getSymbols("VWEHX", from="1950-01-01") getSymbols("SPY", from="1900-01-01") getSymbols("HYG", from="1990-01-01") getSymbols("SHY", from="1990-01-01") getSymbols("VFISX", from="1990-01-01") spySma20Cl <- SMA(Cl(SPY), n=20) clSig <- Cl(SPY) > spySma20Cl clSig <- lag(clSig, 1) vwehxCloseRets <- Return.calculate(Cl(VWEHX)) vfisxCloseRets <- Return.calculate(Cl(VFISX)) vwehxAdjustRets <- Return.calculate(Ad(VWEHX)) vfisxAdjustRets <- Return.calculate(Ad(VFISX)) hygCloseRets <- Return.calculate(Cl(HYG)) shyCloseRets <- Return.calculate(Cl(SHY)) hygAdjustRets <- Return.calculate(Ad(HYG)) shyAdjustRets <- Return.calculate(Ad(SHY)) mutualAdRets <- vwehxAdjustRets*clSig + vfisxAdjustRets*(1-clSig) mutualClRets <- vwehxCloseRets*clSig + vfisxCloseRets*(1-clSig) etfAdRets <- hygAdjustRets*clSig + shyAdjustRets*(1-clSig) etfClRets <- hygCloseRets*clSig + shyCloseRets*(1-clSig)
Here are the results:
mutualFundBacktest <- merge(mutualAdRets, mutualClRets, join='inner') charts.PerformanceSummary(mutualFundBacktest) data.frame(t(rbind(Return.annualized(mutualFundBacktest)*100, maxDrawdown(mutualFundBacktest)*100, SharpeRatio.annualized(mutualFundBacktest))))
Which produces the following equity curves:
As can be seen, the choice to adjust or not can be pretty enormous. Here are the corresponding three statistics:
Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0.. VWEHX.Adjusted 14.675379 2.954519 3.979383 VWEHX.Close 7.794086 4.637520 3.034225
Even without the adjustment, the strategy itself is…very very good, at least from this angle. Let’s look at the ETF variant now.
etfBacktest <- merge(etfAdRets, etfClRets, join='inner') charts.PerformanceSummary(etfBacktest) data.frame(t(rbind(Return.annualized(etfBacktest)*100, maxDrawdown(etfBacktest)*100, SharpeRatio.annualized(etfBacktest))))
The resultant equity curve:
With the corresponding statistics:
Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0.. HYG.Adjusted 11.546005 6.344801 1.4674301 HYG.Close 5.530951 9.454754 0.6840059
Again, another stark difference. Let’s combine all four variants.
fundsAndETFs <- merge(mutualFundBacktest, etfBacktest, join='inner') charts.PerformanceSummary(fundsAndETFs) data.frame(t(rbind(Return.annualized(fundsAndETFs)*100, maxDrawdown(fundsAndETFs)*100, SharpeRatio.annualized(fundsAndETFs))))
The equity curve:
With the resulting statistics:
Annualized.Return Worst.Drawdown Annualized.Sharpe.Ratio..Rf.0.. VWEHX.Adjusted 17.424070 2.787889 4.7521579 VWEHX.Close 11.739849 3.169040 3.8715923 HYG.Adjusted 11.546005 6.344801 1.4674301 HYG.Close 5.530951 9.454754 0.6840059
In short, while the strategy itself seems strong, the particular similar (but not identical) instruments used to implement the strategy make a large difference. So, when backtesting, make sure to understand what taking liberties with the data means. In this case, by turning two levers, the Sharpe Ratio varied from less than 1 to above 4.
Next, I’d like to demonstrate a little trick in quantstrat. Although plenty of examples of trading strategies only derive indicators (along with signals and rules) from the market data itself, there are also many strategies that incorporate data from outside simply the price action of the particular security at hand. Such examples would be many SPY strategies that incorporate VIX information, or off-instrument signal strategies like this one.
The way to incorporate off-instrument information into quantstrat simply requires understanding what the mktdata object is, which is nothing more than an xts type object. By default, a security may originally have just the OHLCV and open interest columns. Most demos in the public space generally use data only from the instruments themselves. However, it is very much possible to actually pre-compute signals.
Here’s a continuation of the script to demonstrate, with a demo of the unadjusted HYG leg of this trade:
####### BOILERPLATE FROM HERE require(quantstrat) currency('USD') Sys.setenv(TZ="UTC") symbols <- "HYG" stock(symbols, currency="USD", multiplier=1) initDate="1990-01-01" strategy.st <- portfolio.st <- account.st <- "preCalc" rm.strat(portfolio.st) rm.strat(strategy.st) initPortf(portfolio.st, symbols=symbols, initDate=initDate, currency='USD') initAcct(account.st, portfolios=portfolio.st, initDate=initDate, currency='USD') initOrders(portfolio.st, initDate=initDate) strategy(strategy.st, store=TRUE) ######### TO HERE clSig <- Cl(SPY) > SMA(Cl(SPY), n=20) HYG <- merge(HYG, clSig, join='inner') names(HYG) <- "precomputed_signal" #no parameters or indicators--we precalculated our signal add.signal(strategy.st, name="sigThreshold", arguments=list(column="precomputed_signal", threshold=.5, relationship="gt", cross=TRUE), label="longEntry") add.signal(strategy.st, name="sigThreshold", arguments=list(column="precomputed_signal", threshold=.5, relationship="lt", cross=TRUE), label="longExit") add.rule(strategy.st, name="ruleSignal", arguments=list(sigcol="longEntry", sigval=TRUE, orderqty=1, ordertype="market", orderside="long", replace=FALSE, prefer="Open"), type="exit", path.dep=TRUE) add.rule(strategy.st, name="ruleSignal", arguments=list(sigcol="longExit", sigval=TRUE, orderqty="all", ordertype="market", orderside="long", replace=FALSE, prefer="Open"), type="exit", path.dep=TRUE) #apply strategy t1 <- Sys.time() out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st) t2 <- Sys.time() print(t2-t1) #set up analytics updatePortf(portfolio.st) dateRange <- time(getPortfolio(portfolio.st)$summary)[-1] updateAcct(portfolio.st,dateRange) updateEndEq(account.st)
As you can see, no indicators computed from the actual market data, because the strategy used a pre-computed value to work off of. The lowest-hanging fruit of applying this methodology, of course, would be to append the VIX index as an indicator for trading strategies on the SPY.
And here are the results, trading a unit quantity:
> data.frame(round(t(tradeStats(portfolio.st)[-c(1,2)]),2)) HYG Num.Txns 217.00 Num.Trades 106.00 Net.Trading.PL 36.76 Avg.Trade.PL 0.35 Med.Trade.PL 0.01 Largest.Winner 9.83 Largest.Loser -2.71 Gross.Profits 67.07 Gross.Losses -29.87 Std.Dev.Trade.PL 1.67 Percent.Positive 50.00 Percent.Negative 50.00 Profit.Factor 2.25 Avg.Win.Trade 1.27 Med.Win.Trade 0.65 Avg.Losing.Trade -0.56 Med.Losing.Trade -0.39 Avg.Daily.PL 0.35 Med.Daily.PL 0.01 Std.Dev.Daily.PL 1.67 Ann.Sharpe 3.33 Max.Drawdown -7.24 Profit.To.Max.Draw 5.08 Avg.WinLoss.Ratio 2.25 Med.WinLoss.Ratio 1.67 Max.Equity 43.78 Min.Equity -1.88 End.Equity 36.76
And the corresponding position chart:
In conclusion, hopefully this post showed a potentially viable strategy, understanding the nature of the data you’re working with, and how to pre-compute values in quantstrat.
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.