In this article, we will implement a simple trading strategy using the ATR indicator.
Trading Idea:
From the discussion in the previous post, ATR itself does not provide indication about price trend. Instead, it is used to measure market volatility. A stock with a high level of volatility usually has a higher ATR, and similarly, a stock with a lower volatility has a lower ATR.
An old trading saying that "Market take the stairs up and the elevator down." This phenomenon is explained by the fear factor from Behavioral Finance, which is the way how the equity market has been functioning for centuries. As seen from the chart below, during downtrends, the ATR indicator tends to post higher volatility than during up trend.
Thus, we can use ATR as a stop loss indicator for order exit and to protect downside risk (so called Chandelier Exit). The chandelier exit places a trailing stop under the highest high the stock reached since you entered the trade. The distance between the highest high and the stop level is defined as some multiple of the ATR. For example,
- if you buy a stock at $100 and the ATR value is 3, you may place a stop loss at 2 x ATR below the entry price. i.e. 100-2*3 = $94,
- if you short-sell a stock at $100 and the ATR is 3, you may place a stop loss at 2 x ATR above the entry price. i.e. 100+2*3 = $106
- when the market price continues to rise (drop for a sell order), we will revise the stop loss level upward (downward) with the newly calculated ATR multiple
Trading Logic:
Suppose we use RSI(14) for order entry (refer to Technical Indicator - Relative Strength Index (RSI)) and ATR(14) for order exit, and apply this strategy to the daily closing price of Nikkei 225 Index CFD (i.e. JPXJPY) over the year of 2020.
- Based on a sliding window approach to collect the previous 14 closing price
- Calculate the latest ATR and RSI values
- Order open conditions:
- if we have NO outstanding position,
- if RSI value < 30, we open a buy order with stoploss level set to current price - 2*ATR
- if RSI value > 70, we open a sell order with stoploss level set to current price + 2*ATR
- Order close conditions:
- if we have outstanding position,
- if we previously submit a buy order, update the stop-loss level to the maximum of the previous stop-loss and (current price - 2*ATR)
- if we previously submit a sell order, update the stop-loss level to the minimum of the previous stop-loss and (current price + 2*ATR)
- Repeat the process until the backtest period end
Now, let's write down our trading algorithm step-by-step.
Step 1. Calculate RSI and ATR value
First of all, we define 'RSI_period', 'ATR_period' and 'instrument' at initialization.
1 2 3 4 5 6 7 8 9 10 11 | from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest from datetime import datetime, timedelta from talib import RSI, ATR import numpy as np class AlgoEvent: def __init__(self): self.timer = datetime(1970,1,1) self.RSI_period = 14 self.ATR_period = 14 self.instrument = "JPXJPY" |
We use the API function getHistoricalBar to collect historical observations. Then, we apply a python library 'talib.RSI', 'talib.ATR' to calculate the latest RSI and ATR value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def on_marketdatafeed(self, md, ab): # execute stratey every 24 hours if md.timestamp >= self.timer+timedelta(hours=24): # get last 14 closing price res = self.evt.getHistoricalBar({"instrument":self.instrument}, self.RSI_period+1, "D") arrClose = np.array([res[t]['c'] for t in res]) arrHigh = np.array([res[t]['h'] for t in res]) arrLow = np.array([res[t]['l'] for t in res]) # calculate the current RSI value RSI_cur = RSI(arrClose, self.RSI_period)[-1] # calculate the current ATR value ATR_cur = ATR(arrHigh, arrLow, arrClose, self.ATR_period)[-1] # print out RSI and ATR value to console self.evt.consoleLog("RSI_cur, ATR_cur = ", RSI_cur, ATR_cur) # update timer self.timer = md.timestamp |
Step 2. Order Entry Conditions
We further define 'rsi_overbought', 'rsi_oversold' and 'ATR_multiple' at initialization.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest from datetime import datetime, timedelta from talib import RSI, ATR import numpy as np class AlgoEvent: def __init__(self): self.timer = datetime(1970,1,1) self.timer = datetime(1970,1,1) self.RSI_period = 14 self.ATR_period = 14 self.instrument = "JPXJPY" self.rsi_overbought = 70 self.rsi_oversold = 30 self.ATR_multiple = 2 |
We also create a function 'open_order' to handle order submissions.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | def open_order(self, buysell, current_price, ATR): # set stop loss level to be 2*ATR below current price for buy order if buysell==1: sl = current_price - self.ATR_multiple*ATR # set stop loss level to be 2*ATR above current price for buy order else: sl = current_price + self.ATR_multiple*ATR # create order object order = AlgoAPIUtil.OrderObject( instrument = self.instrument, openclose = 'open', buysell = buysell, #1=buy, -1=sell ordertype = 0, #0=market, 1=limit volume = 0.01, stopLossLevel = sl ) # send order to server self.evt.sendOrder(order) |
Now, we update the system's function 'on_marketdatafeed' for order open logic.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | def on_marketdatafeed(self, md, ab): # execute stratey every 24 hours if md.timestamp >= self.timer+timedelta(hours=24): # get last 14 closing price res = self.evt.getHistoricalBar({"instrument":self.instrument}, self.RSI_period+1, "D") arrClose = np.array([res[t]['c'] for t in res]) arrHigh = np.array([res[t]['h'] for t in res]) arrLow = np.array([res[t]['l'] for t in res]) # calculate the current RSI value RSI_cur = RSI(arrClose, self.RSI_period)[-1] # calculate the current ATR value ATR_cur = ATR(arrHigh, arrLow, arrClose, self.ATR_period)[-1] # print out RSI and ATR value to console self.evt.consoleLog("RSI_cur, ATR_cur = ", RSI_cur, ATR_cur) # get outstanding position pos, osOrder, pendOrder = self.evt.getSystemOrders() # open an order if we have no outstanding position if pos[self.instrument]["netVolume"]==0: # open a sell order if it is overbought if RSI_cur>self.rsi_overbought: self.open_order(-1, md.lastPrice, ATR_cur) # open a buy order if it is oversold elif RSI_cur<self.rsi_oversold: self.open_order(1, md.lastPrice, ATR_cur) # update timer self.timer = md.timestamp |
Step 3. Order Close Condition
For order closing logic, we recalculate and update the new stop loss level in 'on_marketdatafeed' function. (refer to line 36 - 49)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | def on_marketdatafeed(self, md, ab): # execute stratey every 24 hours if md.timestamp >= self.timer+timedelta(hours=24): # get last 14 closing price res = self.evt.getHistoricalBar({"instrument":self.instrument}, self.RSI_period+1, "D") arrClose = np.array([res[t]['c'] for t in res]) arrHigh = np.array([res[t]['h'] for t in res]) arrLow = np.array([res[t]['l'] for t in res]) # calculate the current RSI value RSI_cur = RSI(arrClose, self.RSI_period)[-1] # calculate the current ATR value ATR_cur = ATR(arrHigh, arrLow, arrClose, self.ATR_period)[-1] # print out RSI and ATR value to console self.evt.consoleLog("RSI_cur, ATR_cur = ", RSI_cur, ATR_cur) # get outstanding position pos, osOrder, pendOrder = self.evt.getSystemOrders() # open an order if we have no outstanding position if pos[self.instrument]["netVolume"]==0: # open a sell order if it is overbought if RSI_cur>self.rsi_overbought: self.open_order(-1, md.lastPrice, ATR_cur) # open a buy order if it is oversold elif RSI_cur<self.rsi_oversold: self.open_order(1, md.lastPrice, ATR_cur) else: # calculate the new ATR trailing stop loss level for tradeID in list(osOrder): buysell = osOrder[tradeID]["buysell"] sl = osOrder[tradeID]["stopLossLevel"] if buysell==1: sl = max(sl, md.lastPrice-self.ATR_multiple*ATR_cur) elif buysell==-1: sl = min(sl, md.lastPrice+self.ATR_multiple*ATR_cur) # update to new stop loss level self.evt.update_opened_order(tradeID=tradeID, sl=sl) # update timer self.timer = md.timestamp |
Full Source Code
Combining all above, the full script is presented below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | from AlgoAPI import AlgoAPIUtil, AlgoAPI_Backtest from datetime import datetime, timedelta from talib import RSI, ATR import numpy as np class AlgoEvent: def __init__(self): self.timer = datetime(1970,1,1) self.timer = datetime(1970,1,1) self.RSI_period = 14 self.ATR_period = 14 self.instrument = "JPXJPY" self.rsi_overbought = 70 self.rsi_oversold = 30 self.ATR_multiple = 2 def start(self, mEvt): self.evt = AlgoAPI_Backtest.AlgoEvtHandler(self, mEvt) self.evt.start() def on_marketdatafeed(self, md, ab): # execute stratey every 24 hours if md.timestamp >= self.timer+timedelta(hours=24): # get last 14 closing price res = self.evt.getHistoricalBar({"instrument":self.instrument}, self.RSI_period+1, "D") arrClose = np.array([res[t]['c'] for t in res]) arrHigh = np.array([res[t]['h'] for t in res]) arrLow = np.array([res[t]['l'] for t in res]) # calculate the current RSI value RSI_cur = RSI(arrClose, self.RSI_period)[-1] # calculate the current ATR value ATR_cur = ATR(arrHigh, arrLow, arrClose, self.ATR_period)[-1] # print out RSI and ATR value to console self.evt.consoleLog("RSI_cur, ATR_cur = ", RSI_cur, ATR_cur) # get outstanding position pos, osOrder, pendOrder = self.evt.getSystemOrders() # open an order if we have no outstanding position if pos[self.instrument]["netVolume"]==0: # open a sell order if it is overbought if RSI_cur>self.rsi_overbought: self.open_order(-1, md.lastPrice, ATR_cur) # open a buy order if it is oversold elif RSI_cur<self.rsi_oversold: self.open_order(1, md.lastPrice, ATR_cur) else: # calculate the new ATR trailing stop loss level for tradeID in list(osOrder): buysell = osOrder[tradeID]["buysell"] sl = osOrder[tradeID]["stopLossLevel"] if buysell==1: sl = max(sl, md.lastPrice-self.ATR_multiple*ATR_cur) elif buysell==-1: sl = min(sl, md.lastPrice+self.ATR_multiple*ATR_cur) # update to new stop loss level self.evt.update_opened_order(tradeID=tradeID, sl=sl) # update timer self.timer = md.timestamp def open_order(self, buysell, current_price, ATR): # set stop loss level to be 2*ATR below current price for buy order if buysell==1: sl = current_price - self.ATR_multiple*ATR # set stop loss level to be 2*ATR above current price for buy order else: sl = current_price + self.ATR_multiple*ATR # create order object order = AlgoAPIUtil.OrderObject( instrument = self.instrument, openclose = 'open', buysell = buysell, #1=buy, -1=sell ordertype = 0, #0=market, 1=limit volume = 0.01, stopLossLevel = sl ) # send order to server self.evt.sendOrder(order) def on_bulkdatafeed(self, isSync, bd, ab): pass def on_newsdatafeed(self, nd): pass def on_weatherdatafeed(self, wd): pass def on_econsdatafeed(self, ed): pass def on_orderfeed(self, of): pass def on_dailyPLfeed(self, pl): pass def on_openPositionfeed(self, op, oo, uo): pass |
Results
Now, we are prepared to backtest this strategy.
Backtest Settings:
- Instrument: JPXJPY
- Period: 2020.01 - 2020.12
- Initial Capital: US$10,000
- Data Interval: 1-day bar
- Leverage: 100
- Allow Shortsell: True
Backtest Result
Final Thoughts
Some highlights for applying ATR in a trading strategy as below:
- ATR is mainly used to determine order exit condition, but not for order entry
- Multi-period ATRs (eg. 5 and 14) could be used to compare and have a sense about the recent "market volatility"
- The rule of thumb for ATR period is 14
- Is there any better choice?
- Can this value be dynamically changing?
- Will it be generally applicable to other financial instruments/ asset classes?
Like and follow me
If you find my articles inspiring, like this post and follow me here to receive my latest updates.
Enter my promote code "AjpQDMOSmzG2" for any purchase on ALGOGENE, you will automatically get 5% discount.