Since I’ve hit a rut in trend following (how do you quantify rising/falling/flat? What even defines those three terms in precise, machine definition? How do you avoid buying tops while not getting chopped by whipsaws?), I decided to look the other way, with oscillators. Certainly, I’m not ready to give up on Dr. Ehlers just yet. So, in this post, I’ll introduce a recent innovation of the RSI by Dr. John Ehlers.

The indicator is Dr. Ehlers’s modified RSI from Chapter 7 of Cycle Analytics for Traders.

For starters, here’s how the Ehlers RSI is different than the usual ones: it gets filtered with a high-pass filter and then smoothed with a supersmoother filter. While Michael Kapler also touched on this topic a while back, I suppose it can’t hurt if I attempted to touch on it myself.

Here is the high pass filter and the super smoother, from the utility.R file in DSTrading. They’re not exported since as of the moment, they’re simply components of other indicators.

highPassFilter <- function(x) { alpha1 <- (cos(.707*2*pi/48)+sin(.707*2*pi/48)-1)/cos(.707*2*pi/48) HP <- (1-alpha1/2)*(1-alpha1/2)*(x-2*lag(x)+lag(x,2)) HP <- HP[-c(1,2)] HP <- filter(HP, c(2*(1-alpha1), -1*(1-alpha1)*(1-alpha1)), method="recursive") HP <- c(NA, NA, HP) HP <- xts(HP, order.by=index(x)) return(HP) } superSmoother <- function(x) { a1 <- exp(-1.414*pi/10) b1 <- 2*a1*cos(1.414*pi/10) c2 <- b1 c3 <- -a1*a1 c1 <- 1-c2-c3 filt <- c1*(x+lag(x))/2 leadNAs <- sum(is.na(filt)) filt <- filt[-c(1:leadNAs)] filt <- filter(filt, c(c2, c3), method="recursive") filt <- c(rep(NA,leadNAs), filt) filt <- xts(filt, order.by=index(x)) }

In a nutshell, both of these functions serve to do an exponential smoothing on the data using some statically computed trigonometric quantities, the rationale of which I will simply defer to Dr. Ehlers’s book (link here).

Here’s the modified ehlers RSI, which I call CycleRSI, from the book in which it’s defined:

"CycleRSI" <- function(x, n=20) { filt <- superSmoother(highPassFilter(x)) diffFilt <- diff(filt) posDiff <- negDiff <- diffFilt posDiff[posDiff < 0] <- 0 negDiff[negDiff > 0] <- 0 negDiff <- negDiff*-1 posSum <- runSum(posDiff, n) negSum <- runSum(negDiff, n) denom <- posSum+negSum rsi <- posSum/denom rsi <- superSmoother(rsi)*100 colnames(rsi) <- "CycleRSI" return(rsi) }

Here’s a picture comparing four separate RSIs.

The first is the RSI featured in this post (cycle RSI) in blue. The next is the basic RSI(2) in red. The one after that is Larry Connors’s Connors RSI , which may be touched on in the future, and the last one, in purple, is the generalized Laguerre RSI, which is yet another Dr. Ehlers creation (which I’ll have to test sometime in the future).

To start things off with the Cycle RSI, I decided to throw a simple strategy around it:

Buy when the CycleRSI(2) crosses under 10 when the close is above the SMA200, which is in the vein of a Larry Connors trading strategy from “Short Term ETF Trading Strategies That Work” (whether they work or not remains debatable), and sell when the CycleRSI(2) crosses above 70, or when the close falls below the SMA200 so that the strategy doesn’t get caught in a runaway downtrend.

Since the strategy comes from an ETF Trading book, I decided to use my old ETF data set, from 2003 through 2010.

Here’s the strategy code, as usual:

require(DSTrading) require(IKTrading) require(quantstrat) require(PerformanceAnalytics) initDate="1990-01-01" from="2003-01-01" to="2010-12-31" options(width=70) verbose=TRUE source("demoData.R") #trade sizing and initial equity settings tradeSize <- 100000 initEq <- tradeSize*length(symbols) strategy.st <- portfolio.st <- account.st <- "Cycle_RSI_I" 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',initEq=initEq) initOrders(portfolio.st, initDate=initDate) strategy(strategy.st, store=TRUE) #parameters nRSI=2 RSIentry=10 RSIexit=70 nSMA=200 period=10 pctATR=.04 #indicators add.indicator(strategy.st, name="lagATR", arguments=list(HLC=quote(HLC(mktdata)), n=period), label="atrX") add.indicator(strategy.st, name="SMA", arguments=list(x=quote(Cl(mktdata)), n=nSMA), label="SMA") add.indicator(strategy.st, name="CycleRSI", arguments=list(x=quote(Cl(mktdata)), n=nRSI), label="RSI") #signals add.signal(strategy.st, name="sigComparison", arguments=list(columns=c("Close", "SMA"), relationship="gt"), label="ClGtSMA") add.signal(strategy.st, name="sigThreshold", arguments=list(column="CycleRSI.RSI", threshold=RSIentry, relationship="lt", cross=FALSE), label="RSIltEntryThresh") add.signal(strategy.st, name="sigAND", arguments=list(columns=c("ClGtSMA", "RSIltEntryThresh"), cross=TRUE), label="longEntry") add.signal(strategy.st, name="sigCrossover", arguments=list(columns=c("Close", "SMA"), relationship="lt"), label="exitSMA") add.signal(strategy.st, name="sigThreshold", arguments=list(column="CycleRSI.RSI", threshold=RSIexit, relationship="gt", cross=TRUE), label="longExit") #rules #rules add.rule(strategy.st, name="ruleSignal", arguments=list(sigcol="longEntry", sigval=TRUE, ordertype="market", orderside="long", replace=FALSE, prefer="Open", osFUN=osDollarATR, tradeSize=tradeSize, pctATR=pctATR, atrMod="X"), type="enter", 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) add.rule(strategy.st, name="ruleSignal", arguments=list(sigcol="exitSMA", 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)

And here are the results:

> (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses)) [1] 1.846124 > (aggCorrect <- mean(tStats$Percent.Positive)) [1] 65.071 > (numTrades <- sum(tStats$Num.Trades)) [1] 2048 > (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE)) [1] 1.028333 > print(t(durStats)) [,1] Min 1 Q1 6 Med 9 Mean 11 Q3 14 Max 43 WMin 1 WQ1 7 WMed 9 WMean 11 WQ3 13 WMax 40 LMin 1 LQ1 6 LMed 11 LMean 12 LQ3 15 LMax 43 > print(mean(as.numeric(as.character(mktExposure$MktExposure)))) [1] 0.2806 > mean(corMeans) [1] 0.2763 > SharpeRatio.annualized(portfRets) [,1] Annualized Sharpe Ratio (Rf=0%) 1.215391 > Return.annualized(portfRets) [,1] Annualized Return 0.1634448 > maxDrawdown(portfRets) [1] 0.1694307

Overall, the statistics don’t look bad. However, the 1:1 annualized returns to max drawdown isn’t particularly pleasing, as it means that this strategy can’t be leveraged effectively to continue getting outsized returns in this state. Quite irritating. Here’s the equity curve.

In short, as with other mean reverters, when drawdowns happen, they happen relatively quickly and brutally.

Here’s an individual instrument position chart.

By the looks of things, the strategy does best in a market that grinds upwards, rather than a completely choppy sideways market.

Finally, here’s some code for charting all of the different trades.

agg.chart.ME <- function(Portfolio, Symbols, type=c("MAE", "MFE"), scale=c("cash", "percent", "tick")) { type=type[1] scale=scale[1] trades <- list() length(trades) <- length(Symbols) for(Symbol in Symbols) { trades[[Symbol]] <- pts <- perTradeStats(Portfolio=Portfolio, Symbol=Symbol, includeOpenTrade=FALSE) } trades <- do.call(rbind, trades) trades$Pct.Net.Trading.PL <- 100 * trades$Pct.Net.Trading.PL trades$Pct.MAE <- 100 * trades$Pct.MAE trades$Pct.MFE <- 100 * trades$Pct.MFE profitable <- (trades$Net.Trading.PL > 0) switch(scale, cash = { .ylab <- "Profit/Loss (cash)" if (type == "MAE") { .cols <- c("MAE", "Net.Trading.PL") .xlab <- "Drawdown (cash)" .main <- "Maximum Adverse Excursion (MAE)" } else { .cols <- c("MFE", "Net.Trading.PL") .xlab <- "Run Up (cash)" .main <- "Maximum Favourable Excursion (MFE)" } }, percent = { .ylab <- "Profit/Loss (%)" if (type == "MAE") { .cols <- c("Pct.MAE", "Pct.Net.Trading.PL") .xlab <- "Drawdown (%)" .main <- "Maximum Adverse Excursion (MAE)" } else { .cols <- c("Pct.MFE", "Pct.Net.Trading.PL") .xlab <- "Run Up (%)" .main <- "Maximum Favourable Excursion (MFE)" } }, tick = { .ylab <- "Profit/Loss (ticks)" if (type == "MAE") { .cols <- c("tick.MAE", "tick.Net.Trading.PL") .xlab <- "Drawdown (ticks)" .main <- "Maximum Adverse Excursion (MAE)" } else { .cols <- c("tick.MFE", "tick.Net.Trading.PL") .xlab <- "Run Up (ticks)" .main <- "Maximum Favourable Excursion (MFE)" } }) .main <- paste("All trades", .main) plot(abs(trades[, .cols]), type = "n", xlab = .xlab, ylab = .ylab, main = .main) grid() points(abs(trades[profitable, .cols]), pch = 24, col = "green", bg = "green", cex = 0.6) points(abs(trades[!profitable, .cols]), pch = 25, col = "red", bg = "red", cex = 0.6) abline(a = 0, b = 1, lty = "dashed", col = "darkgrey") legend(x = "bottomright", inset = 0.1, legend = c("Profitable Trade", "Losing Trade"), pch = c(24, 25), col = c("green", "red"), pt.bg = c("green", "red")) }

And the resulting plot:

One last thing to note…that $50,000 trade in the upper left hand corner? That was a yahoo data issue and is a false print. Beyond that, once again, this seems like standard fare for a mean reverter–when trades go bad, they’re *really* bad, but the puzzle of where to put a stop is a completely separate issue, as it usually means locking in plenty of losses that decrease in magnitude, along with possibly turning winners into losers. On the flip side, here’s the maximum favorable excursion plot.

In short, there are definitely trades that could have been stopped for a profit that turned into losers.

In conclusion, while the initial trading system seems to be a good start, it’s far from complete.

Thanks for reading.

Don’t you get bigger positions with the same tradeSize and pctATR than in your FRAMA studies ?

If you’re asking about notional amounts, I do believe that I updated that number at some point. But most of my analysis is agnostic of the actual size of the orders, so long as the relationship between those orders remains consistent.

Very nice article what tool set do you use?

This blog displays these tools.

Dear Ilya, very interesting script!

I’ve installed your DSTrading package searching for Adaptive RSI Oscillator (Cap. 11) and Even Better Sinewave Indicator (Cap.12) but I didn’t found them. Are those two present in latest version of your package or in other packages?

Thank you

Diego

Not in them. I believe they needed the autocorrelation periodograms implemented, and I can’t wrap my head around those.

Thanks for your answer!

I’ve seen the autocorrelation periodograms already implemented in your package and center of gravity too… correct?

To complete the Adaptive RSI I need to implement Discrete Fourier Transform.

Diego

Center of Gravity yes, autocorrelation periodogram no.

Pingback: A John Ehlers oscillator — Cycle RSI(2) | Econometria aplicada