A Hammer Trading System — Demonstrating Custom Indicator-Based Limit Orders in Quantstrat

So several weeks ago, I decided to listen on a webinar (and myself will be giving one on using quantstrat on Sep. 3 for Big Mike’s Trading, see link). Among some of those talks was a trading system called the “Trend Turn Trade Take Profit” system. This is his system:

Define an uptrend as an SMA10 above an SMA30.
Define a pullback as an SMA5 below an SMA10.

Define a hammer as a candle with an upper shadow less than 20% of the lower shadow, and a body less than 50% of the lower shadow. Enter on the high of the hammer, with the stop loss set at the low of the hammer and an additional one third of the range. The take profit target is 1.5 to 1.7 times the distance between the entry and the stop price.

Additionally (not tested here) was the bullish engulfing pattern, which is a two-bar pattern with the conditions of a down day followed by an up day on which the open of the up day was less than the close of the down day, and the close of the up day was higher than the previous day’s open, with the stop set to the low of the pattern, and the profit target in the same place.

This system was advertised to be correct about 70% of the time, with trades whose wins were 1.6 times as much as the losses, so I decided to investigate it.

The upside to this post, in addition to investigating someone else’s system, is that it will allow me to demonstrate how to create more nuanced orders with quantstrat. The best selling point for quantstrat, in my opinion, is that it provides a framework to do just about anything you want, provided you know how to do it (not trivial). In any case, the salient thing to take from this strategy is that it’s possible to create some interesting custom orders with some nuanced syntax.

Here’s the syntax for this strategy:

hammer <- function(OHLC, profMargin=1.5) {
  dailyMax <- pmax(Op(OHLC), Cl(OHLC))
  dailyMin <- pmin(Op(OHLC), Cl(OHLC))
  upShadow <- Hi(OHLC) - dailyMax
  dnShadow <- dailyMin - Lo(OHLC)
  body <- dailyMax-dailyMin
  hammerDay <- dnShadow/body > 2 & dnShadow/upShadow > 5
  hammers <- OHLC[hammerDay==1,]
  hammers$stopLoss <- 4/3*Lo(hammers)-1/3*Hi(hammers)
  hammers$takeProfit <- Hi(hammers) + (Hi(hammers)-hammers$stopLoss)*profMargin
  hammers <- cbind(hammerDay, hammers$stopLoss, hammers$takeProfit)
  hammers$stopLoss <- na.locf(hammers$stopLoss)
  hammers$takeProfit <- na.locf(hammers$takeProfit)
  colnames(hammers) <- c("hammer", "SL", "TP")
  return(hammers)
}

require(IKTrading)
require(quantstrat)
require(PerformanceAnalytics)

initDate="1990-01-01"
from="2003-01-01"
to=as.character(Sys.Date())
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 <- "Hammer_4TP"
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
nSMA1=10
nSMA2=30
nSMA3=5
profMargin=1.5

period=10
pctATR=.1


#indicators
add.indicator(strategy.st, name="lagATR", 
              arguments=list(HLC=quote(HLC(mktdata)), 
                             n=period), 
              label="atrX")

add.indicator(strategy.st, name="hammer",
              arguments=list(OHLC=quote(OHLC(mktdata)), 
                             profMargin=profMargin),
              label="hammer")

add.indicator(strategy.st, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), 
                             n=nSMA1),
              label="sma1")

add.indicator(strategy.st, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), 
                             n=nSMA2),
              label="sma2")

add.indicator(strategy.st, name="SMA",
              arguments=list(x=quote(Cl(mktdata)), 
                             n=nSMA3),
              label="sma3")
#signals
add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("SMA.sma1", "SMA.sma2"), 
                          relationship="gt"),
           label="upTrend")

add.signal(strategy.st, name="sigComparison",
           arguments=list(columns=c("SMA.sma3", "SMA.sma1"), 
                          relationship="lt"),
           label="pullback")

add.signal(strategy.st, name="sigThreshold",
           arguments=list(column="hammer.hammer", threshold=.5, 
                          relationship="gt", cross=TRUE),
           label="hammerDay")

add.signal(strategy.st, name="sigAND",
           arguments=list(columns=c("upTrend", 
                                    "pullback", 
                                    "hammerDay"), 
                          cross=TRUE),
           label="longEntry")

add.signal(strategy.st, name="sigCrossover",
           arguments=list(columns=c("SMA.sma1", "SMA.sma2"), 
                          relationship="lt"),
           label="SMAexit")
#rules
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        osFUN=osDollarATR,
                        tradeSize=tradeSize, 
                        prefer="High",
                        pctATR=pctATR,
                        atrMod="X",
                        orderset="orders"), 
         type="enter", path.dep=TRUE,
         label="hammerEntry")
 
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$SL.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="stopLossLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="limit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$TP.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="takeProfitLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal",
         arguments=list(sigcol="SMAexit",
                        sigval=TRUE,
                        ordertype="market",
                        orderside="long",
                        replace=TRUE,
                        orderqty='all',
                        prefer='Open',
                        orderset='orders'
                        ),
         type='exit',
         label='SMAexitLong',
         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)

I added one additional rule to the strategy in that if the trend reverses (SMA10 < SMA30), to get out of the trade.

First off, let's take a closer look at the entry and exit rules.

#rules
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        osFUN=osDollarATR,
                        tradeSize=tradeSize, 
                        prefer="High",
                        pctATR=pctATR,
                        atrMod="X",
                        orderset="orders"), 
         type="enter", path.dep=TRUE,
         label="hammerEntry")
 
add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="stoplimit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$SL.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="stopLossLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal", 
         arguments=list(sigcol="longEntry", 
                        sigval=TRUE, 
                        ordertype="limit", 
                        orderside="long", 
                        replace=FALSE, 
                        orderqty='all',
                        order.price=quote(mktdata$TP.hammer[timestamp]),
                        orderset="orders"), 
         type="chain", 
         parent="hammerEntry",
         label="takeProfitLong",
         path.dep=TRUE)

add.rule(strategy.st, name="ruleSignal",
         arguments=list(sigcol="SMAexit",
                        sigval=TRUE,
                        ordertype="market",
                        orderside="long",
                        replace=TRUE,
                        orderqty='all',
                        prefer='Open',
                        orderset='orders'
                        ),
         type='exit',
         label='SMAexitLong',
         path.dep=TRUE)

The rules used here use a few new concepts that I haven't used in previous blog posts. First off, the argument of orderset puts all the orders within one order set as a one-canceling-the-other mechanism. Next, the order.price syntax works similarly to the market data syntax on specifying indicators — EG add.indicator(strategy.st, name=”SMA”, arguments=list(x=quote(Cl(mktdata)), etc…), except this time, it specifies a certain column in the market data (which is, in fact, what Cl(mktdata) does, or HLC(mktdata), and so on), but also, the [timestamp] syntax is necessary so it knows what specific quantity in time is being referred to.

For take-profit orders, as you want to sell above the market, or buy below the market, the correct type of order (that is, the ordertype argument) is a limit order. With stop-losses or trailing stops (not shown here), since you want to sell below the market or buy above the market, the correct ordertype is a stoplimit order.

Finally, the rule I added (the SMA exit) actually improves the strategy's performance (I wanted to give this system the benefit of the doubt).

Here are the results, with the strategy leveraged up to .1 pctATR (the usual strategies I test range between .02 and .04):

> (aggPF <- sum(tStats$Gross.Profits)/-sum(tStats$Gross.Losses))
[1] 1.55156
> (aggCorrect <- mean(tStats$Percent.Positive))
[1] 52.42367
> (numTrades <- sum(tStats$Num.Trades))
[1] 839
> (meanAvgWLR <- mean(tStats$Avg.WinLoss.Ratio[tStats$Avg.WinLoss.Ratio < Inf], na.rm=TRUE))
[1] 1.481

print(t(durStats))
      [,1]
Min      1
Q1       1
Med      4
Mean     5
Q3       7
Max     56
WMin     1
WQ1      2
WMed     4
WMean    6
WQ3      7
WMax    56
LMin     1
LQ1      1
LMed     3
LMean    5
LQ3      6
LMax    42

> print(mktExposure)
   Symbol MktExposure
1     EFA       0.023
2     EPP       0.019
3     EWA       0.026
4     EWC       0.015
5     EWG       0.019
6     EWH       0.023
7     EWJ       0.017
8     EWS       0.024
9     EWT       0.022
10    EWU       0.025
11    EWY        0.02
12    EWZ       0.019
13    EZU       0.023
14    IEF        0.01
15    IGE       0.022
16    IYR        0.02
17    IYZ       0.024
18    LQD       0.022
19    RWR       0.023
20    SHY       0.017
21    TLT       0.007
22    XLB       0.016
23    XLE       0.021
24    XLF       0.012
25    XLI       0.022
26    XLK       0.019
27    XLP       0.023
28    XLU       0.022
29    XLV        0.02
30    XLY       0.018
> print(mean(as.numeric(as.character(mktExposure$MktExposure))))
[1] 0.01976667

> SharpeRatio.annualized(portfRets)
                                    [,1]
Annualized Sharpe Ratio (Rf=0%) 1.027048
> Return.annualized(portfRets)
                        [,1]
Annualized Return 0.06408888
> maxDrawdown(portfRets)
[1] 0.09036151

> round(apply.yearly(dailyRetComparison, Return.cumulative),3)
           strategy    SPY
2003-12-31    0.179  0.369
2004-12-31    0.075  0.079
2005-12-30   -0.036  0.025
2006-12-29    0.143  0.132
2007-12-31    0.121  0.019
2008-12-31   -0.042 -0.433
2009-12-31    0.066  0.192
2010-12-31    0.135  0.110
2011-12-30    0.057 -0.028
2012-12-31    0.039  0.126
2013-12-31   -0.023  0.289
2014-08-06    0.048  0.036
> round(apply.yearly(dailyRetComparison, SharpeRatio.annualized),3)
           strategy    SPY
2003-12-31    2.971  3.100
2004-12-31    1.039  0.706
2005-12-30   -0.774  0.238
2006-12-29    2.355  1.312
2007-12-31    2.024  0.123
2008-12-31   -0.925 -1.050
2009-12-31    1.026  0.719
2010-12-31    2.504  0.614
2011-12-30    0.644 -0.122
2012-12-31    0.640  0.990
2013-12-31   -0.520  2.594
2014-08-06    1.171  0.586
> round(apply.yearly(dailyRetComparison, maxDrawdown),3)
           strategy   SPY
2003-12-31    0.030 0.056
2004-12-31    0.058 0.085
2005-12-30    0.046 0.074
2006-12-29    0.035 0.077
2007-12-31    0.039 0.102
2008-12-31    0.061 0.520
2009-12-31    0.044 0.280
2010-12-31    0.029 0.167
2011-12-30    0.069 0.207
2012-12-31    0.057 0.099
2013-12-31    0.071 0.062
2014-08-06    0.032 0.058

In short, looking at the trade stats, this system is…far from what was advertised. In fact, here's the equity curve.

Anything but spectacular the past several years, which is why I suppose it was free to give it away in a webinar. Overall, however, the past several years have just seen the S&P just continue to catch up to this strategy. At the end of the day, it’s a highly unimpressive system in my opinion, and I won’t be exploring the other aspects of it further. However, as an exercise in showing some nuanced features of quantstrat, I think this was a worthwhile endeavor.

Thanks for reading.

11 thoughts on “A Hammer Trading System — Demonstrating Custom Indicator-Based Limit Orders in Quantstrat

  1. Pingback: The Whole Street’s Daily Wrap for 8/18/2014 | The Whole Street

  2. Pingback: Webinar: An Introduction To R for Trading w/Ilya Kipnis - Matlab, R project and Python | Big Mike Trading

  3. I might interpret the results from the last 7 years (which is too short of a time span for me) as Working VERY effectively at limiting the losses when you’re concerned you’re near the top of a bull market or in a bear market.

    A different analysis where you applied it later in the bull market would be interesting.

    I’d think about applying it now after having enjoyed several years of good returns.

    • A good point, Alan, but from all the systems I’ve traded, none have had their equity curves just crap out the way this has. As for the past seven years, the issue is that ETFs don’t go back that far, and I’m hesitant to use adjusted stock prices, due to dividends being factored in when the system may not have actually been in a position to receive the dividend, but on the other hand, you have the issue of stock splits, and so on.

      Also, I suppose my standards for trading systems may also be more stringent. If a system loses more than say, 5% in any one year, that is already a troubling sign for me.

      See, the way I approach trading systems isn’t that they’re supposed to hit home runs so much as consistently bat singles, aka have a good Sharpe ratio and good return to drawdown metrics.

      A bad system is a bad system, but a system, even with low absolute returns, but great return for risk can be leveraged to meet the appropriate return/risk appetite.

  4. Hi Ilya,how can i access previous bars in the indicator?

    for example i want to build Bill Williams fractal indicator, that needs to lookback 5 bars to build it.
    I know how to reference the current bar (as you have in your above example) but how to i look back?

    Thanks Dane

  5. Hi Ilya,

    I cam across R and quantstrat and all the other modules in this context just recently and I also watched your Big Mike webinar, which was very very helpful.

    Related to this post, I am not interested in the strategy itself, but what I need for my backtests is to have stop-limit orders at the high of the candle (just as here) and a stop loss at the low of a candle.

    I have applied your example to the apple stock and checked many trades manually and what I saw is the following problem:

    – If the next days Open is higher than the previos days High (so there is a gap) then the system still goes long at this new open price, which is “too high” and in some cases, it is even higher than the take profit price. The system then buys for the higher price, and sells the next day for the lower take profit price, therefore making a loss. Is this how stop limit orders should work? I would expect that it the open is already above the stop limit order price, that the order is not executed but changed into a pending limit order at least and only triggers, if the price goes back down. Not having any intraday data available, I would therefore expect to have quantstrat only enter the long position if there is no up-gap or only when the market comes down on another day later, without any exit condition bein triggered before that.

    – Another problem I am not sure yet how to solev with not having intraday data is, if the candle after the signal candle has a higher high and a lower low, therefore hitting both the enter and the exit (stop loss) order on the same day. It is then not clear, if in reality, the market went up first, triggering the long entry and then went down to sell with a loss, or if the low level was reached first, canceling the signal completely and therefore not entering any trade at all.

    I am wondering if and how I can use quantstrat to deal with both situations
    1. not entering the market if the exact stop buy price is not seen
    2. defining a way to deal with ambigious situations. This could be to treat such cases always as 100% loss or as 50% loss etc.

    Another problem I have not found a way yet is how I can calculate the order sizing based on the stop-loss price, so to say, having a fixed amoung of money at stake with each trade. But thats another story. Also how margin trading could be simulated (meaning: blocking part of the capital if openign a trade but not 100% as for real stock trades)

    Thanks for your advice if you have any!

    • On stop limits: they have a very strict definition, which is at or past your threshold. Quantstrat won’t read your mind. What you might want to do is put in some sort of automatic exit order if there’s a gap.

      Regarding leverage/margin: you can size your trades however you like. Starting equity has no effect on that unless you directly tie it into your order sizing. See my ATR order sizing function at the beginning of my blog for inspiration as to how to do this.

  6. Thanks for the super fast reply. I understand the stoplimit order model of quantstrat now. I am trying to simulate what I would do in reality as good as possible.

    I actually though about an automatic exit order which would immediately exit the position again, however there seems not to be an immediately in quantstrat – the exit is only on the next day, so prices could have moved significantly until then.

    Therefore your comment makes me think that I might want to artificially split every data row into 2 rows with the first one just having O=H=L=C = open of the actual bar, and the second one as the actual bar. Given that quantstrat is a next bar trade system, I could then on such bars generate the actual entry orders for the next day. I import my signals as TRUE / FALSE values together with the OHLC data from an external system so I am not dependent on price related calculations of indicators.

    Of course I dont expect a software to read my mind…I am sure that what I want to do is 100% describable unambigiously so it can be modeled.

    If you think that makes sense at all I will be happy to describe this approach (once I have figured it out haha) and post it somewhere if it is useable in general.

  7. Hi Ilya,

    I have watched your webnar and I was trying to reproduce your code, but I have some problem during the “applyStrategy”. I think I have typed something wrong during my script building. Do you have the script for comparison?I have tried to see any types and I found 2 of them,but it still didn’t work. When I run, “out” become empty and it didn’t simulate the trades.

    Thank you for your attention.

Leave a comment