# 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))
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
filt <- filter(filt, c(c2, c3), method="recursive")
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(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

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
arguments=list(HLC=quote(HLC(mktdata)), n=period),
label="atrX")
arguments=list(x=quote(Cl(mktdata)), n=nSMA),
label="SMA")
arguments=list(x=quote(Cl(mktdata)), n=nRSI),
label="RSI")

#signals
arguments=list(columns=c("Close", "SMA"), relationship="gt"),
label="ClGtSMA")

arguments=list(column="CycleRSI.RSI", threshold=RSIentry,
relationship="lt", cross=FALSE),
label="RSIltEntryThresh")

arguments=list(columns=c("ClGtSMA", "RSIltEntryThresh"),
cross=TRUE),
label="longEntry")

arguments=list(columns=c("Close", "SMA"), relationship="lt"),
label="exitSMA")

arguments=list(column="CycleRSI.RSI", threshold=RSIexit,
relationship="gt", cross=TRUE),
label="longExit")

#rules
#rules
arguments=list(sigcol="longEntry", sigval=TRUE,
ordertype="market",
orderside="long", replace=FALSE,
prefer="Open", osFUN=osDollarATR,
atrMod="X"),
type="enter", path.dep=TRUE)

arguments=list(sigcol="longExit", sigval=TRUE,
orderqty="all", ordertype="market",
orderside="long", replace=FALSE,
prefer="Open"),
type="exit", path.dep=TRUE)

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
[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]
for(Symbol in Symbols) {
}
switch(scale, cash = {
.ylab <- "Profit/Loss (cash)"
if (type == "MAE") {
.xlab <- "Drawdown (cash)"
.main <- "Maximum Adverse Excursion (MAE)"
} else {
.xlab <- "Run Up (cash)"
.main <- "Maximum Favourable Excursion (MFE)"
}
}, percent = {
.ylab <- "Profit/Loss (%)"
if (type == "MAE") {
.xlab <- "Drawdown (%)"
.main <- "Maximum Adverse Excursion (MAE)"
} else {
.xlab <- "Run Up (%)"
.main <- "Maximum Favourable Excursion (MFE)"
}
}, tick = {
.ylab <- "Profit/Loss (ticks)"
if (type == "MAE") {
.xlab <- "Drawdown (ticks)"
.main <- "Maximum Adverse Excursion (MAE)"
} else {
.xlab <- "Run Up (ticks)"
.main <- "Maximum Favourable Excursion (MFE)"
}
})
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.

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

1. Antoine says:

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.

2. Gwez M says:

Very nice article what tool set do you use?

3. Diego Peroni says:

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.

• Diego Peroni says: