Algorithmic Trading with Average Directional Index in Python

Learn to build the ADX indicator from scratch and backtest a trading strategy in python



There are four different branches of technical indicators out there but the most popular one is known as the trend indicators. These indicators help traders in identifying the direction and strength of the market trend, and trade along with them. In most cases, the indicators that come under the category of trend reveals good results unless we use them efficiently.


In this article, we are going to explore one of the most popular trend indicators, the Average Directional Index (shortly known as ADX). We will first build some basic understanding of what ADX is all about and its calculation, then, move on to building the indicator from scratch and construct a trading strategy based on that indicator in python. To evaluate our strategy, we will first backtest it with the Apple stock, and then to make more sense, we will compare our strategy returns with the SPY ETF returns (an ETF specifically designed to track the movement of the S&P 500 index).


Average True Range (ATR)


Before moving on to discovering ADX, it is essential to know what the Average True Range (ATR) is as it is involved in the calculation of the Average Directional Index (ADX).


The Average True Range is a technical indicator that measures how much an asset moves on an average. It is a lagging indicator meaning that it takes into account the historical data of an asset to measure the current value but it’s not capable of predicting the future data points. This is not considered as a drawback while using ATR as it’s one of the indicators to track the volatility of a market more accurately. Along with being a lagging indicator, ATR is also a non-directional indicator meaning that the movement of ATR is inversely proportional to the actual movement of the market. To calculate ATR, it is requisite to follow two steps:

  • Calculate True Range (TR): A True Range of an asset is calculated by taking the greatest values of three price differences which are: market high minus marker low, market high minus previous market close, previous market close minus market low. It can be represented as follows:



MAX [ {HIGH - LOW}, {HIGH - P.CLOSE}, {P.CLOSE - LOW} ]

where,
MAX = Maximum values
HIGH = Market High
LOW = Market Low
P.CLOSE = Previous market close


  • Calculate ATR: The calculation for the Average True Range is simple. We just have to take a smoothed average of the previously calculated True Range values for a specified number of periods. The smoothed average is not just any SMA or EMA but an own type of smoothed average created by Wilder Wiles himself but there aren’t any restrictions in using other MAs too. In this article, we will be using SMA to calculate ATR rather than the custom moving average created by the founder of the indicator just to make things simple. The calculation of ATR with a traditional setting of 14 as the number of periods can be represented as follows:



ATR 14 = SMA 14 [ TR ]

where,
ATR 14 = 14 Period Average True Range
SMA 14 = 14 Period Simple Moving Average
TR = True Range


While using ATR as an indicator for trading purposes, traders must ensure that they are cautious than ever as the indicator is very lagging. Now that we have an understanding of what the Average True Range is all about. Let’s now dive into the main concept of this article, the Average Directional Index.


Average Directional Index (ADX)


ADX is a technical indicator that is widely used in measuring the strength of the market trend. Now, the ADX doesn’t measure the direction of the trend, whether it’s bullish or bearish, but just represents how strong the trend is. So, to identify the direction of the trend, ADX is combined with a Positive Directional Index (+ DI) and a Negative Directional Index (- DI). As the name suggests, the + DI measures the bullish or positive trend of the market, similarly, the - DI measures the bearish or negative trend of the market. The values of all the components are bound between 0 to 100, hence acting as an oscillator. The traditional setting of ADX is 14 as the lookback period.


To calculate the values of ADX with 14 as the lookback period, first, the Positive (+ DM) and Negative Directional Movement (- DM) is determined. The + DM is calculated by finding the difference between the current high and the previous high, and similarly, the - DM is calculated by finding the difference between the previous low and the current low. It can be represented as follows:



+ DM = CURRENT HIGH - PREVIOUS HIGH
- DM = PREVIOUS LOW - CURRENT LOW


Then, an ATR with 14 as the lookback period is calculated. Now, using the calculated directional movement and the ATR values, the Positive Directional Index (+ DI) and the Negative Directional Index (- DI) are calculated. To determine the values of + DI, the value received by taking the Exponential Moving Average (EMA) of the Positive Directional Movement (+ DM) with 14 as the lookback period is divided by the previously calculated 14-day ATR and then, multiplied by 100. This same applies to determining the - DI too but instead of taking the 14-day EMA of + DM, the Negative Directional Movement (- DM) is taken into account. The formula to calculate both the + DI and the - DI can be represented as follows:



+ DI 14 = 100 * [ EMA 14 ( + DM ) / ATR 14 ]
- DI 14 = 100 * [ EMA 14 ( - DM ) / ATR 14 ]


The next step is to use the + DI and - DI to calculate the Directional Index. It can be determined by dividing the absolute value of the difference between the +DI and - DI by the absolute value of the total of + DI and - DI multiped by 100. The formula to calculate the Directional Index can be represented as follows:



DI 14 = | (+ DI 14) - (- DI 14) | / | (+ DI 14) + (- DI 14) |  * 100


The final step is to calculate the ADX itself by utilizing the determined Directional Index values. The ADX is calculated by multiplying the previous Directional Index value with 13 (lookback period - 1) and adding it with the Directional Index, then multiplied by 100. The formula to calculate the values of ADX can be represented as follows:



ADX 14 = [ ( PREV DI 14 * 13 ) + DI 14 ] * 100


The ADX cannot be used as it is but needs to be smoothed. Since founded by Wilder Wiles (the founder of ATR too), the ADX is smoothed by a custom moving average we discussed before. We neglected using this custom moving average while calculating ATR as it is possible to use other types of moving averages but it is essential to use while smoothing ADX to get accurate values.


That’s the whole process of calculating the values of ADX. Now, let’s discuss how a simple ADX-based trading strategy can be constructed.


About our trading strategy: In this article, we are going to build a simple crossover strategy that reveals a buy signal whenever the ADX line crosses from below to above 25 and the + DI line is above the - DI line. Similarly, a sell signal is generated whenever the ADX line crosses from below to above 25 and the - DI line is above the + DI line. Our trading strategy can be represented as follows:



IF P.ADX < 25 AND C.ADX > 25 AND + DI LINE > - DI LINE ==> BUY
IF P.ADX < 25 AND C.ADX > 25 AND + DI LINE < - DI LINE ==> SELL


This concludes the theory part of this article. Now, let’s code this indicator from scratch and build the discussed trading strategy out of it in python and backtest it with the Apple stock to see some exciting results. We will also compare our ADX crossover strategy returns with the returns of the SPY ETF to see how well our trading strategy has performed against a benchmark. Without further ado, let’s dive into the coding part.


Implementation in Python


The coding part is classified into various steps as follows:



1. Importing Packages
2. Extracting Stock Data from Twelve Data
3. ADX Calculation
4. ADX Indicator Plot
5. Creating the Trading Strategy
6. Plotting the Trading Lists
7. Creating our Position
8. Backtesting
9. SPY ETF Comparison


We will be following the order mentioned in the above list and buckle up your seat belts to follow every upcoming coding part.


Step-1: Importing Packages


Importing the required packages into the python environment is a non-skippable step. The primary packages are going to be Pandas to work with data, NumPy to work with arrays and for complex functions, Matplotlib for plotting purposes, and Requests to make API calls. The secondary packages are going to be Math for mathematical functions and Termcolor for font customization (optional).


Python Implementation:



import pandas as pd
import numpy as np
import requests
import matplotlib.pyplot as plt
from math import floor
from termcolor import colored as cl

plt.style.use('fivethirtyeight')
plt.rcParams['figure.figsize'] = (20,10)


Now that we have imported all the required packages into our python. Let’s pull the historical data of Apple with Twelve Data’s API endpoint.


Step-2: Extracting data from Twelve Data


In this step, we are going to pull the historical stock data of Apple using an API endpoint provided by twelvedata.com. Before that, a note on twelvedata.com: Twelve Data is one of the leading market data providers having an enormous amount of API endpoints for all types of market data. It is very easy to interact with the APIs provided by Twelve Data and has one of the best documentation ever. Also, ensure that you have an account on twelvedata.com, only then, you will be able to access your API key (vital element to extract data with an API).


Python Implementation:



def get_historical_data(symbol, start_date):
    api_key = 'YOUR API KEY'
    api_url = f'https://api.twelvedata.com/time_series?symbol={symbol}&interval=1day&outputsize=5000&apikey={api_key}'
    raw_df = requests.get(api_url).json()
    df = pd.DataFrame(raw_df['values']).iloc[::-1].set_index('datetime').astype(float)
    df = df[df.index >= start_date]
    df.index = pd.to_datetime(df.index)
    return df

aapl = get_historical_data('AAPL', '2020-01-01')
aapl


Output:



Code Explanation: The first thing we did is to define a function named ‘get_historical_data’ that takes the stock’s symbol (‘symbol’) and the starting date of the historical data (‘start_date’) as parameters. Inside the function, we are defining the API key and the URL and stored them into their respective variable. Next, we are extracting the historical data in JSON format using the ‘get’ function and stored it into the ‘raw_df’ variable. After doing some processes to clean and format the raw JSON data, we are returning it in the form of a clean Pandas dataframe. Finally, we are calling the created function to pull the historic data of Apple from the starting of 2020 and stored it into the ‘aapl’ variable.


Step-4: ADX Calculation


In this step, we are going to calculate the values of ADX by following the method we discussed before.


Python Implementation:



def get_adx(high, low, close, lookback):
    plus_dm = high.diff()
    minus_dm = low.diff()
    plus_dm[plus_dm < 0] = 0
    minus_dm[minus_dm > 0] = 0
    
    tr1 = pd.DataFrame(high - low)
    tr2 = pd.DataFrame(abs(high - close.shift(1)))
    tr3 = pd.DataFrame(abs(low - close.shift(1)))
    frames = [tr1, tr2, tr3]
    tr = pd.concat(frames, axis = 1, join = 'inner').max(axis = 1)
    atr = tr.rolling(lookback).mean()
    
    plus_di = 100 * (plus_dm.ewm(alpha = 1/lookback).mean() / atr)
    minus_di = abs(100 * (minus_dm.ewm(alpha = 1/lookback).mean() / atr))
    dx = (abs(plus_di - minus_di) / abs(plus_di + minus_di)) * 100
    adx = ((dx.shift(1) * (lookback - 1)) + dx) / lookback
    adx_smooth = adx.ewm(alpha = 1/lookback).mean()
    return plus_di, minus_di, adx_smooth

aapl['plus_di'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[0]).rename(columns = {0:'plus_di'})
aapl['minus_di'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[1]).rename(columns = {0:'minus_di'})
aapl['adx'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[2]).rename(columns = {0:'adx'})
aapl = aapl.dropna()
aapl.tail()


Output:


Code Explanation: We are first defining a function named ‘get_adx’ that takes a stock’s high (‘high’), low (‘low’), and close data (‘close’) along with the lookback period (‘lookback’) as parameters.


Inside the function, we are first calculating and storing the + DM and - DM into the ‘plus_dm’ and ‘minus_dm’ respectively. Then comes the ATR calculation where we are first calculating the three differences and defined a variable ‘tr’ to store the highest values among the determined differences, then, we calculated and stored the values of ATR into the ‘atr’ variable.


Using the calculated directional movements and ATR values, we are calculating the + DI and - DI and stored them into the ‘plus_di’ and ‘minus_di’ variables respectively. With the help of the previously discussed formula, we are calculating the Directional Index values and stored them into the ‘dx’ variable, and applied those values into the ADX formula to calculate the Average Directional Index values. Then, we defined a variable ‘adx_smooth’ to store the smoothed values of ADX. Finally, we are returning and calling the function to obtain the + DI, - DI, and ADX values of Apple with 14 as the lookback period.


Step-4: ADX Plot


In this step, we are going to plot the calculated ADX values of Apple to make more sense out of it. The main aim of this part is not on the coding section but instead to observe the plot to gain a solid understanding of the Average Directional Index.


Python Implementation:



ax1 = plt.subplot2grid((11,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((11,1), (6,0), rowspan = 5, colspan = 1)
ax1.plot(aapl['close'], linewidth = 2, color = '#ff9800')
ax1.set_title('AAPL CLOSING PRICE')
ax2.plot(aapl['plus_di'], color = '#26a69a', label = '+ DI 14', linewidth = 3, alpha = 0.3)
ax2.plot(aapl['minus_di'], color = '#f44336', label = '- DI 14', linewidth = 3, alpha = 0.3)
ax2.plot(aapl['adx'], color = '#2196f3', label = 'ADX 14', linewidth = 3)
ax2.axhline(25, color = 'grey', linewidth = 2, linestyle = '--')
ax2.legend()
ax2.set_title('AAPL ADX 14')
plt.show()


Output:


<