Using a simple moving average to time markets has been a successful strategy over a very long period of time. Nothing to brag home about, but it cuts the drawdown of a buy and hold by about a half, sacrificing less than 1% of the CAGR in the process. In two words, simple yet effective. Here are the important numbers (using the S&P 500 index from 1994 to 2013, inclusive):
|Statitistic||SMA 200||Buy and Hold|
|Pct in Market||70.14||99.94|
|Avg Annual Drawdown||9.12%||15.83%|
|Gain to Pain||0.71||0.45|
The moving average system is better by far. First, we can leverage it 2:1 (thus, bringing CAGR up to approximately 13%) and still, our maximum drawdown is going to be comparable to the buy and hold. Furthermore, it’s in the market only 70% – less risk and probably the returns can be further boosted by putting the money to work in alternative assets.
One problem with these type of systems are whipsaws. The system works well when the price stays away from the moving average. However, in close proximity, one may have to enter/exit in succession losing money on the way.
One way to address this issue is to use an alternative “line” to trigger the exits (or the entries, for that matter). It could be a percentage band, but that’s hardly universal. Better to bring volatility into picture.
Let’s do something more robust as an illustration.
require(quantmod) require(btutils) # For construct.indicator # Download data gspc = getSymbols("^GSPC",from="1900-01-01",auto.assign=F) # Compute the returns rets = ROC(Cl(gspc),type="discrete") # Compute the adjusted returns adj.rets = rets/sqrt(runSum(rets*rets,10)/9) # The moving average sma = runMean(adj.rets,n=200) # The standard deviation stddev = sqrt(runSum(adj.rets*adj.rets,200)/199) # Long trades are entered when the average turns positive upper.band = 0.0 # Long trades are closed when the average return goes below -0.05 standard deviations lower.band = -0.05*stddev # For the "uncushioned" version use # lower.band = 0 uu = ifelse(sma>upper.band,1,0) dd = ifelse(sma<lower.band,-1,0) long.entries = (uu == 1 & lag(uu) == 0) long.exits = (dd == -1 & lag(dd) == 0) short.entries = long.entries short.exits = long.entries short.entries = FALSE short.exits = FALSE # Given entries and exits (both for long and short positions), this function builds # the corresponding indicator. It's pure C++, so it's fast too. ind = btutils::construct.indicator(long.entries, long.exits, short.entries, short.exits)
First we apply the moving average not to the prices, but to the returns (an interpretation of David Varadi’s Error Adjusted Momentum). The “cushioned” system goes long when the average becomes positive, but to exit, it leaves some cushion below the moving average. What do we gain?
|Statitistic||Cushioned EA||EA||SMA 200||Buy and Hold|
|Pct in Market||80.28%||76.29%||70.14%||99.94%|
|Avg Annual Drawdown||8.90%||9.26%||9.12%||15.83%|
|Gain to Pain||1.23||0.98||0.71||0.45|
To compare apples to apples, we added two systems. The first one (dubbed EA) uses error adjusted returns to compute the SMA, but enters and exits when the SMA crosses the 0 line. The “cushioned” version is the system implemented by the above code.
Even after taking into account that the new strategies stays longer in the market, there seems to be slight improvement. But that’s not the point. The “cushioned” approach did 4 trades in total – it exited for the two bear markets. That’s about 4 trades. The non-cushioned approach had 80 trades. Mission accomplished in other words.