A Hammer Trading System — Demonstrating Custom Indicator-Based Limit Orders in Quantstrat
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
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.
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.