A John Ehlers oscillator — Cycle RSI(2)

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))

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"

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:




#trade sizing and initial equity settings
tradeSize <- 100000
initEq <- tradeSize*length(symbols)

strategy.st <- portfolio.st <- account.st <- "Cycle_RSI_I"
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)




add.indicator(strategy.st, name="lagATR", 
              arguments=list(HLC=quote(HLC(mktdata)), n=period), 
add.indicator(strategy.st, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), n=nSMA),
add.indicator(strategy.st, name="CycleRSI",
              arguments=list(x=quote(Cl(mktdata)), n=nRSI),

add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("Close", "SMA"), relationship="gt"),

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="CycleRSI.RSI", threshold=RSIentry, 
                          relationship="lt", cross=FALSE),

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("ClGtSMA", "RSIltEntryThresh"), 

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("Close", "SMA"), relationship="lt"),

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="CycleRSI.RSI", threshold=RSIexit,
                          relationship="gt", cross=TRUE),

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", sigval=TRUE, 
                        orderside="long", replace=FALSE, 
                        prefer="Open", osFUN=osDollarATR,
                        tradeSize=tradeSize, pctATR=pctATR, 
         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, 
         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, 
         type="exit", path.dep=TRUE)

#apply strategy
t1 <- Sys.time()
out <- applyStrategy(strategy=strategy.st,portfolios=portfolio.st)
t2 <- Sys.time()

#set up analytics
dateRange <- time(getPortfolio(portfolio.st)$summary)[-1]

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))
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)
Annualized Sharpe Ratio (Rf=0%) 1.215391
> Return.annualized(portfRets)
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")) {
  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)
  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.

9 thoughts on “A John Ehlers oscillator — Cycle RSI(2)

    • 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.

  1. 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

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s