This post will demonstrate a method to create an ensemble filter based on a trade-off between smoothness and responsiveness, two properties looked for in a filter. An ideal filter would both be responsive to price action so as to not hold incorrect positions, while also be smooth, so as to not incur false signals and unnecessary transaction costs.

So, ever since my volatility trading strategy, using three very naive filters (all SMAs) completely missed a 27% month in XIV, I’ve decided to try and improve ways to create better indicators in trend following. Now, under the realization that there can potentially be tons of complex filters in existence, I decided instead to focus on a way to create ensemble filters, by using an analogy from statistics/machine learning.

In static data analysis, for a regression or classification task, there is a trade-off between bias and variance. In a nutshell, variance is bad because of the possibility of overfitting on a few irregular observations, and bias is bad because of the possibility of underfitting legitimate data. Similarly, with filtering time series, there are similar concerns, except bias is called lag, and variance can be thought of as a “whipsawing” indicator. Essentially, an ideal indicator would move quickly with the data, while at the same time, not possess a myriad of small bumps-and-reverses along the way, which may send false signals to a trading strategy.

So, here’s how my simple algorithm works:

The inputs to the function are the following:

A) The time series of the data you’re trying to filter

B) A collection of candidate filters

C) A period over which to measure smoothness and responsiveness, defined as the square root of the n-day EMA (2/(n+1) convention) of the following:

a) Responsiveness: the squared quantity of price/filter – 1

b) Smoothness: the squared quantity of filter(t)/filter(t-1) – 1 (aka R’s return.calculate) function

D) A conviction factor, to which power the errors will be raised. This should probably be between .5 and 3

E) A vector that defines the emphasis on smoothness (vs. emphasis on responsiveness), which should range from 0 to 1.

Here’s the code:

require(TTR) require(quantmod) getSymbols('SPY', from = '1990-01-01') smas <- list() for(i in 2:250) { smas[[i]] <- SMA(Ad(SPY), n = i) } smas <- do.call(cbind, smas) xtsApply <- function(x, FUN, n, ...) { out <- xts(apply(x, 2, FUN, n = n, ...), order.by=index(x)) return(out) } sumIsNa <- function(x){ return(sum(is.na(x))) }

This gets SPY data, and creates two utility functions–xtsApply, which is simply a column-based apply that replaces the original index that using a column-wise apply discards, and sumIsNa, which I use later for counting the numbers of NAs in a given row. It also creates my candidate filters, which, to keep things simple, are just SMAs 2-250.

Here’s the actual code of the function, with comments in the code itself to better explain the process from a technical level (for those still unfamiliar with R, look for the hashtags):

ensembleFilter <- function(data, filters, n = 20, conviction = 1, emphasisSmooth = .51) { # smoothness error filtRets <- Return.calculate(filters) sqFiltRets <- filtRets * filtRets * 100 #multiply by 100 to prevent instability smoothnessError <- sqrt(xtsApply(sqFiltRets, EMA, n = n)) # responsiveness error repX <- xts(matrix(data, nrow = nrow(filters), ncol=ncol(filters)), order.by = index(filters)) dataFilterReturns <- repX/filters - 1 sqDataFilterQuotient <- dataFilterReturns * dataFilterReturns * 100 #multiply by 100 to prevent instability responseError <- sqrt(xtsApply(sqDataFilterQuotient, EMA, n = n)) # place smoothness and responsiveness errors on same notional quantities meanSmoothError <- rowMeans(smoothnessError) meanResponseError <- rowMeans(responseError) ratio <- meanSmoothError/meanResponseError ratio <- xts(matrix(ratio, nrow=nrow(filters), ncol=ncol(filters)), order.by=index(filters)) responseError <- responseError * ratio # for each term in emphasisSmooth, create a separate filter ensembleFilters <- list() for(term in emphasisSmooth) { # compute total errors, raise them to a conviction power, find the normalized inverse totalError <- smoothnessError * term + responseError * (1-term) totalError <- totalError ^ conviction invTotalError <- 1/totalError normInvError <- invTotalError/rowSums(invTotalError) # ensemble filter is the sum of candidate filters in proportion # to the inverse of their total error tmp <- xts(rowSums(filters * normInvError), order.by=index(data)) #NA out time in which one or more filters were NA initialNAs <- apply(filters, 1, sumIsNa) tmp[initialNAs > 0] <- NA tmpName <- paste("emphasisSmooth", term, sep="_") colnames(tmp) <- tmpName ensembleFilters[[tmpName]] <- tmp } # compile the filters out <- do.call(cbind, ensembleFilters) return(out) }

The vast majority of the computational time takes place in the two xtsApply calls. On 249 different simple moving averages, the process takes about 30 seconds.

Here’s the output, using a conviction factor of 2:

t1 <- Sys.time() filts <- ensembleFilter(Ad(SPY), smas, n = 20, conviction = 2, emphasisSmooth = c(0, .05, .25, .5, .75, .95, 1)) t2 <- Sys.time() print(t2-t1) plot(Ad(SPY)['2007::2011']) lines(filts[,1], col='blue', lwd=2) lines(filts[,2], col='green', lwd = 2) lines(filts[,3], col='orange', lwd = 2) lines(filts[,4], col='brown', lwd = 2) lines(filts[,5], col='maroon', lwd = 2) lines(filts[,6], col='purple', lwd = 2) lines(filts[,7], col='red', lwd = 2)

And here is an example, looking at SPY from 2007 through 2011.

In this case, I chose to go from blue to green, orange, brown, maroon, purple, and finally red for smoothness emphasis of 0, 5%, 25%, 50%, 75%, 95%, and 1, respectively.

Notice that the blue line is very wiggly, while the red line sometimes barely moves, such as during the 2011 drop-off.

One thing that I noticed in the course of putting this process together is something that eluded me earlier–namely, that naive trend-following strategies which are either fully long or fully short based on a crossover signal can lose money quickly in sideways markets.

However, theoretically, by finely varying the jumps between 0% to 100% emphasis on smoothness, whether in steps of 1% or finer, one can have a sort of “continuous” conviction, by simply adding up the signs of differences between various ensemble filters. In an “uptrend”, the difference as one moves from the most responsive to most smooth filter should constantly be positive, and vice versa.

In the interest of brevity, this post doesn’t even have a trading strategy attached to it. However, an implied trading strategy can be to be long or short the SPY depending on the sum of signs of the differences in filters as you move from responsiveness to smoothness. Of course, as the candidate filters are all SMAs, it probably wouldn’t be particularly spectacular. However, for those out there who use more complex filters, this may be a way to create ensembles out of various candidate filters, and create even better filters. Furthermore, I hope that given enough candidate filters and an objective way of selecting them, it would be possible to reduce the chances of creating an overfit trading system. However, anything with parameters can potentially be overfit, so that may be wishful thinking.

All in all, this is still a new idea for me. For instance, the filter to compute the error terms can probably be improved. The inspiration for an EMA 20 essentially came from how Basel computes volatility (if I recall, correctly, it uses the square root of an 18 day EMA of squared returns), and the very fact that I use an EMA can itself be improved upon (why an EMA instead of some other, more complex filter). In fact, I’m always open to how I can improve this concept (and others) from readers.

Thanks for reading.

NOTE: I am currently contracting in Chicago in an analytics capacity. If anyone would like to meet up, let me know. You can email me at ilya.kipnis@gmail.com, or contact me through my LinkedIn here.