Friday, March 12, 2010

Modified Donchian Band Trend Follower using R, Quantmod, TTR

I've been toying around with the examples given on the FOSS trading site for some of the great work they've put together in the Quantmod and TTR packages. Those viewers who are looking for a nice (and free) backtesting suite to possibly complement some of your other results or work in say, Weka, should familiarize yourselves with R. Not only can it serve as a canvas to simulate ideas and concepts, but can process the backend results towards more trader oriented metrics, than using something like Weka as a standalone tool. As you gain more proficiency in data mining and machine learning concepts in Weka, you can also make the move to integrate the tools inside of R, as R contains the majority of machine learning schemes inside of various packages.



Fig 1. Modified Donchian Channel System Simulation

As an example of how to use some of their tools (along with traditional R packages) for fast prototyping, I put together an example of a modified Donchian Channel trend following system along with how you might simulate it using R. The typical Donchian Channel Bands are used as breakout entry and exit signals. I.e. once an n period high has been breached you go long, then exit when the n period low has been breached -- visa versa for short. In this example, however, we simply enter long on the average line break and stay long as long as it is above. A short signal is entered on the average line break to the downside. Unlike a price/moving average type system, there wasn't a lot of choppiness causing false starts around the average line, which is a plus.

I am still trying to familiarize myself more with the tools, and am still at the point where I like how simple and fast the static vector computations work (similar to numpy in Python), but I am wondering how fast more sophisticated entry/exits requiring loops will work. I still expect to work on some of these types of scenarios, as I am really enjoying the capabilities of R along with some of these trading oriented packages.

Although the system (using QQQQ as an example) is in no way optimized nor analyzed for robustness, it returned a respectable 60% versus a buy and hold loss over the past roughly two years (showing a simple example of trend type trading).

Here is the complete code for you to replicate (I used the R version 2.7.10.1).
Note: if some of it looks familiar to the FOSS RSI example, it is exactly because I used that example as a starting point, so there will be some overlap in comments and actions.

# We will need the quantmod package for charting and pulling
# data and the TTR package to calculate Donchian Bands.
# You can install packages via: install.packages("packageName")
# install.packages(c("quantmod","TTR"))
# See Foss Trading Blog for RSI template
library(quantmod)
library(TTR)

tckr<-"QQQQ"
tckr_obj<-QQQQ

start<-"2008-01-01"
end<- "2010-03-08"

# Pull tckr index data from Yahoo! Finance
getSymbols(tckr, from=start, to=end)
QQQQ.cl<-QQQQ[,6]
QQQQ.H<-QQQQ[,2]
QQQQ.L<-QQQQ[,3]
dc<-DonchianChannel(cbind(QQQQ.H,QQQQ.L),n=80)

#Plotting Donchian Channel
ymin=25
ymax=55


par(mfrow=c(2,2), oma=c(2,2,2,2))

# max, avg, min <- red, blue, green
plot(dc[,1],col="red",ylim=c(ymin,ymax),main="")
par(new=T)
plot(dc[,2],col="blue",ylim=c(ymin,ymax),main="")
par(new=T)
plot(dc[,3],col="green",ylim=c(ymin,ymax),main="")
par(new=T)
plot(QQQQ.cl,ylim=c(ymin,ymax),pch=15,main="donchian bands max/avg/min")
lines(QQQQ.cl,ylim(ymin,ymax))
###################################################


# Create the long (up) and short (dn) signals
sigup <-ifelse(QQQQ.cl > dc[,2],1,0)
sigdn <-ifelse(QQQQ.cl < dc[,2],-1,0)

# Lag signals to align with days in market,
# not days signals were generated
sigup <- lag(sigup,1) # Note k=1 implies a move *forward*
sigdn <- lag(sigdn,1) # Note k=1 implies a move *forward*

# Replace missing signals with no position
# (generally just at beginning of series)
sigup[is.na(sigup)] <- 0
sigdn[is.na(sigdn)] <- 0

# Combine both signals into one vector
sig <- sigup + sigdn

# Calculate Close-to-Close returns
ret <- ROC(tckr_obj[,6])
ret[1] <- 0

# Calculate equity curves
eq_up <- cumprod(1+ret*sigup)
eq_dn <- cumprod(1+ret*sigdn)
eq_all <- cumprod(1+ret*sig)

#graphics
mfg=c(1,2)
plot(eq_up,ylab="Long",col="green")
mfg=c(2,2)
plot(eq_all,ylab="Combined",col="blue",main="combined L/S equity")
mfg=c(2,1)
plot(eq_dn,ylab="Short",col="red")
title("Modified Donchian Band Trend Following System (intelligenttradingtech.blogspot.com)", outer = TRUE)

##############################################################################################################


P.S. As always, please use your own due diligence in all work borrowed from this site. There are some areas that I believe are not quite correct in the simulation framework, needless to say, you have a complete script to start your own examples and backtesting.

14 comments:

  1. I have yet to download R and give it a try for backtesting. However, it sems like a sensible place to start.

    May I ask what you anticipate in terms of trading frequency with this algorithm? In particular, will your gains be eaten considerably by transaction cost churn?

    Good post by the way!

    ReplyDelete
  2. Hi michael,

    The nice thing about using this type of script is it is very easy to change the dates and look at how it performed over various periods outside of my example. Although I showed it was profitable over the past 2 yrs (very profitable if you were using leveraged instruments), and had few trades (choppiness), a cursory rerun over different periods was not as great, hence the importance of developing a method that is robust over different time frames and being careful to look beyond the cherry picked regions (in any system).
    The blotter/performance analytics packages show promise here.

    The particular system uses a slow parameter of 80days, and thus you would expect a medium to long term type frequency (as is expected in trend following type algorithms), the benefit being less churn/commisions and greater profit/commission/churn etc.. ratio.

    I highly suggest to download the latest version of R (since most of the quantmod packages require it and you'll have minimum bugs). Then once you launch R, just copy my script and paste it in, then hit enter and watch the magic -- that simple.
    Change a few variables, like start and end date to see how it performed in other time frames.

    Thanks for visiting.

    ReplyDelete
  3. Thanks for introducing me to Donchian indicator. I have programed it into my system and like the results so far 50 day gives me better results. Also link you provided to FOSS,they mention there ta-lib it looks very interesting. I have been working on a program and am in the process of doing my own indicators ta-lib looks very promising. In case you are interested you can find my program here http://www.turtrades.com

    ReplyDelete
  4. Andy,

    Looks like a cool simulator. I've always wanted to build one of those for real time discretionary trading=) I'll have to play with it some time. Thanks for stopping by.

    ReplyDelete
  5. It looks like the lag of the signal in your R code is looking at the future data and hence you are getting that impressive performance. The actual performance is had by changing these two lines of code. In general whenever you get a performance that looks this good there is got to be something off..


    ---original
    sigup <- lag(sigup,1) # Note k=1 implies a move *forward*
    sigdn <- lag(sigdn,1) # Note k=1 implies a move *forward*

    ---new

    sigup <- lag(sigup,-1) # Note k=1 implies a move *forward*
    sigdn <- lag(sigdn,-1) # Note k=1 implies a move *forward*

    ReplyDelete
  6. Hi kris,

    That line of code was taken from the FOSS example I referenced. However, I believe it is correct. You aren't actually looking forward at unseen data, even though the signal is deferred until the next day. The reason is that you want the signal to line up with the day after the signal threshold was breached, or else, then you would be cheating.

    Take for example a slice:
    slice<-sigup[120:130]
    QQQQ.Adjusted
    2008-06-23 1
    2008-06-24 1
    2008-06-25 1
    2008-06-26 0
    2008-06-27 0
    2008-06-30 0
    2008-07-01 0
    2008-07-02 0
    2008-07-03 0
    2008-07-07 0
    2008-07-08 0


    when you use lag(slice,1) you get
    QQQQ.Adjusted
    2008-06-23 NA
    2008-06-24 1
    2008-06-25 1
    2008-06-26 1
    2008-06-27 0
    2008-06-30 0
    2008-07-01 0
    2008-07-02 0
    2008-07-03 0
    2008-07-07 0
    2008-07-08 0
    Notice the signal ends 26, now, because you didn't close the long signal until the next day.


    Let's say you used some kind of moving average crossover and got a long signal. Your signal would have occurred at the end of the day, so if you tallied up your position as being long that day, it would be incorrect, since you couldn't have went long the day in advance. So, to compensate you push the signal out to the next day, meaning you went long at close of today and exited at the close tomorrow; that is why you push the signal one day forward. There is no look forward bias here. You are only tallying the data as having taken the trade the close of the signal event to the next days close.

    This is common in testing systems. Sometimes people do not want end of day bias, as they don't know whether they could have gotten eod data, so they go long the open next day, and consider a trade element the o-cl of next day. This can be coded up as well.

    Now if you lagged the data in the other direction (negaitve) as in your example, then you would really be cheating, as it would mean you took the signal the day before it actually happened! I think if you run the scenario (although I haven't) you'd get far better performance.

    ReplyDelete
  7. Agree with you , think I got thrown by the close and the adjusted close columns. Anyway I added something like this to take tcost in to account and perf looks more inline..

    cumprod(1+ret*sig)
    ----
    cumprod(1+ret*sig-tcost*dsig)

    where dsig is the diff(sig) with the na removed & tcost is your tcost assumption!

    ReplyDelete
  8. Kris,

    Not a bad idea, although you have to be careful to divide up the cost properly (cost/day) since you are compounding each daily event; it's likely you are not trading in and out every day. Also, you need to be careful to express it in terms of the percentage friction per day.

    ReplyDelete
  9. I have to say first that I am an 'R' newbie.

    Having said that I liked the code but had trouble when I tried to put another symbol through it like 'SPY' or 'DIA'.

    The best I can tell from testing is the data is different - data from Yahoo is off - or there is something R specific that I do not understand - QQQQ is 2 by 2 XX.XX and SPY is 3 by 2 or XXX.XX.

    Any help?

    ReplyDelete
  10. Hi mh,
    Thanks for stopping by. I haven't run this particular script in a while, but did have some bugs when running SPY. Don't have a lot of time to debug at the moment; I'll try to look back when I get a chance. It's possible the data length of SPY is shorter and there are some NA issues.
    IT

    ps. Just ran successfully with a quick kludge.
    Try to replace all QQQQ with SPY, and then change the first few lines to reflect the following:

    tckr<-"SPY"
    tckr_obj<-SPY

    start<-"2008-01-01"
    end<- "2010-03-08"

    # Pull tckr index data from Yahoo! Finance
    getSymbols(tckr, from=start, to=end)
    SPY.cl<-SPY[,6]
    SPY.H<-SPY[,2]
    SPY.L<-SPY[,3]
    dc<-DonchianChannel(cbind(SPY.H,SPY.L),n=80)
    dc<- dc[90:length(dc[,1])]


    #Plotting Donchian Channel
    ymin<-min(dc)
    ymax<-max(dc)

    ReplyDelete
  11. Can this strategy be applied to a portfolio of 10+ stocks? In other words, does the code need to be replicated in order to generate a signal for additional symbols? Thanks.

    ReplyDelete
  12. anon,

    You could manually run through each of the ten symbols and just divide by total start capital to accomplish this.

    Of course it could be applied to any number of symbols with a slight modification to the code.

    IT

    ReplyDelete
  13. You may want to update the code. QQQQ has changed to QQQ.

    Also tckr_obj <- QQQQ produces an error:
    Error: object 'QQQQ' not found

    ReplyDelete
  14. Anon,
    Tks. Just Replace all QQQQ with QQQ.
    IT

    ReplyDelete