Researching the Average Directional Index in Python
Two indicators in one, or is it technically 4 indicators in one? Either way, there are indicators, graphs, math, and code! Still got the PDF too.
The next indicator in our list is complete and I am running out of time in April. What are the chances I can get one more indicator completed, testing and research, and a backtest completed before the end of the month? It seems like a long shot. I will settle for having it all done this week. Being a stay at home parent is not for the feint of heart.
Today's post is covering the Average Directional Index (ADX). In order to calculate the ADX, we are going to need to calculate the average true range of our data too. Instead of doing this inline, I decided to go ahead and code it separately so we can use it again in the future. Technically, the ADX indicator is built by comparing the positive and negative directional movements/indexes, which could be used separately but I don't code these calculations separately.
Like the last post, this post also has an accompanying PDF. This document has all the code associate with this post, including the code to fetch some free data and plot visualizations. The link for this PDF will be located at the bottom of the post for paid subscribers.
Free subscribers, don't skip it. The entire article is open for everyone with the exception of the PDF document. I am experimenting with different ways to format the PDF so that I can create a paid version (with code) and a free version (sans code) for readers.
Disclaimer: the following post is an organized representation of my research and project notes. It doesn’t represent any type of advice, financial or otherwise. Its purpose is to be informative and educational. Backtest results are based on historical data, not real-time data. There is no guarantee that these hypothetical results will continue in the future. Day trading is extremely risky, and I do not suggest running any of these strategies live.
Average True Range (ATR)
The ATR provides a measure of market volatility, reflecting the largest movement (either up or down) of the asset price during the day/period. It measures the change both within a bar and between adjacent bars. The purpose of this calculation is to help scale indicators against a more durable measure of price volatility. It's method of measurement makes it more resistant to long-term price changes or slow steady trends.
Calculating True Range ()
The calculation for getting the True Range ():
where $H_t$ is the high price, $L_t$ is the low price, and $C_{t-1}$ is the previous period's closing price, capturing the greatest single-period price movement either within the day or from the closing of the previous period
Calculating ATR
The ATR is then calculated using a rolling method. The initial ATR at the specified period is simply the average of the True Ranges up to that point:
where $n$ is the lookback period.
Subsequent ATR values are computed using:
Code for ATR
This code defines a function atr
that calculates the Average True Range (ATR), a technical analysis indicator used to measure market volatility. The function is optimized with JIT compilation from Numba to enhance performance. The atr
function accepts arrays containing the high, low, and close prices of a financial instrument over a series of periods, along with a period
parameter specifying the number of periods over which to calculate the ATR. An optional boolean parameter use_log
is included, which, if set to True, will adjust the calculation to use logarithmic methods.
The function begins by calculating the True Range for each period, defined as the maximum of the difference between the current high and low, the absolute difference between the current high and the previous close, and the absolute difference between the current low and the previous close. These values are stored in the true_ranges
array. The ATR is then calculated on a rolling basis starting from the specified period
. The first ATR value is computed as a simple average of the initial true ranges. Subsequent ATR values are determined using a formula akin to an exponential moving average, where each new ATR is derived from the previous one adjusted by the newest true range.
The output of the function is an array, atr_values
, which contains the rolling ATR for each period, with initial values set as NaN (Not a Number) until enough data is available to compute the first full ATR. The use of np.empty
for pre-allocating arrays enhances performance, crucial for processing large datasets efficiently.
@jit(nopython=True)
def atr(high_prices, low_prices, close_prices, period, use_log=False):
"""
Calculate the Average True Range (ATR) using either standard or logarithmic returns.
Parameters:
- high_prices (np.ndarray): Array of high prices.
- low_prices (np.ndarray): Array of low prices.
- close_prices (np.ndarray): Array of close prices.
- period (int): The number of periods over which to calculate the ATR.
- use_log (bool): If True, calculates the true range using logarithmic returns.
Returns:
- np.ndarray: The calculated ATR values, where the first `period` values are NaN.
"""
n = len(close_prices)
true_ranges = np.empty(n - 1)
atr_values = np.empty(n)
atr_values[:period] = np.nan # Initialize first 'period' values as undefined
if use_log:
# Logarithmic return calculations
for i in range(1, n):
high_log = np.log(high_prices[i] / high_prices[i - 1])
low_log = np.log(low_prices[i] / low_prices[i - 1])
close_log = np.log(close_prices[i] / close_prices[i - 1])
tr1 = high_log - low_log
tr2 = np.abs(high_log - close_log)
tr3 = np.abs(low_log - close_log)
true_ranges[i - 1] = max(tr1, tr2, tr3)
else:
# Standard ATR calculation
for i in range(1, n):
tr1 = high_prices[i] - low_prices[i]
tr2 = np.abs(high_prices[i] - close_prices[i - 1])
tr3 = np.abs(low_prices[i] - close_prices[i - 1])
true_ranges[i - 1] = max(tr1, tr2, tr3)
# Calculate the ATR using an exponential moving average
for i in range(period, n):
if i == period:
atr_values[i] = np.mean(true_ranges[:period])
else:
atr_values[i] = (atr_values[i - 1] * (period - 1) + true_ranges[i - 1]) / period
return atr_values
Average Directional Index (ADX)
Originally created by J. Welles Wilder Jr., the ADX indicator is designed to measure the strength of a trend without specifying the direction of the trend. [1] It differs from other indicators in this regard, as most of them give a direction. That means that this indicator needs to be used in conjunction with another indicator. The ADX indicator is based on looking at changes in the highs and lows separately (much like the RSI) and uses the Average True Range as a basis for judging the importance in the highs and lows. [1]
The ADX indicator uses two levels of smoothing. In both levels, in the original ADX calculations, a simple moving average (SMA) was used to begin the smoothing process and before switching to exponential. According to Masters, this is a tedious, likely unnecessary, and a possibly counterproductive process. [1]
Instead of using this dual smoothing method, and trying to stick to the original calculation, I am going to use exponential smoothing throughout the entire process.
Mathematical Logic for the ADX Indicator:
The Positive Directional Movement (+DM) and Negative Directional Movement (-DM) are calculated as follows, using exponential smoothing:
where is the high price, is the low price. is the previous period's closing price.
Smoothing Calculations:
Exponential smoothing is applied to calculate smoothed +DM and -DM:
Directional Indicators:
The Directional Indicators are then calculated to assess the market direction:
Directional Index (DX):
The Directional Index measures the strength of the directional movement:
ADX:
Finally, the Average Directional Index (ADX) is calculated using exponential smoothing of DX:
Code
This code defines a function adx
that calculates the Average Directional Index (ADX), which quantifies the strength of a trend. The ADX is derived from the directional movement indicators (+DI and -DI) and the ATR. The function is optimized with JIT compilation using Numba for improved performance and takes arrays of open, high, low, and close prices as inputs, along with a lookback parameter specifying the number of periods over which the ADX is calculated.
The function first calculates the Positive Directional Movement ($+DM$) and Negative Directional Movement ($-DM$) for each period. $+DM$ is calculated as the maximum of the difference between the current and previous high prices if this value is greater than the difference between the previous and current low prices; otherwise, it is zero. $-DM$ is calculated similarly but considers the low prices. This process captures the predominant direction of price movements.
These values are then smoothed using a method akin to an exponential moving average over the `lookback` period to compute the smoothed $+DM$ ($+DMS$) and $-DM$ ($-DMS$). This smoothing is crucial as it tempers the volatility of the daily movements into a more stable indicator of directional movement.
Utilizing pre-computed Average True Range (ATR) values from a separate ATR function, the smoothed $+DMS$ and $-DMS$ values are normalized to calculate the Positive Directional Indicator ($+DI$) and Negative Directional Indicator ($-DI$). The formula for these calculations involves dividing the $+DMS$ and $-DMS$ by the ATR and multiplying by 100 to express them as percentages. These indicators reflect the relative strength of upward versus downward movements.
The Directional Index (DX) is subsequently calculated using the formula mentioned above. This index quantifies the absolute difference between $+DI$ and $-DI$ relative to their sum, effectively measuring the decisiveness of the price movement.
Finally, the ADX itself is computed as a moving average of DX values, providing a smooth and consistent measure of the strength of the trend over the lookback
period. The initial ADX value is the first DX, and subsequent values are calculated using a rolling method, incorporating the current DX into the previous ADX to ensure a continuous assessment of trend strength.
The function outputs an array, output
, which contains the ADX values for each period, with initial values set as NaN (Not a Number) until sufficient data are available to compute the first full ADX. This array provides a chronological measure of trend strength, allowing traders to gauge the robustness of trends in price movements effectively.
@jit(nopython=True)
def adx(open_prices, high_prices, low_prices, close_prices, lookback):
"""
Calculates the Average Directional Index (ADX) using pre-computed Average True Range (ATR)
values from a given ATR function.
Parameters:
- high_prices (np.ndarray): Array of high prices.
- low_prices (np.ndarray): Array of low prices.
- close_prices (np.ndarray): Array of close prices.
- lookback (int): The number of periods over which ADX is calculated.
Returns:
- np.ndarray: An array of ADX values.
"""
n = len(close_prices)
output = np.zeros(n)
output[:lookback] = np.nan # Initial values are undefined due to insufficient data for full lookback
DMSplus = DMSminus = 0.0
ATR_values = atr(open_prices, high_prices, low_prices, close_prices, lookback)
for icase in range(lookback, n):
DMplus = max(high_prices[icase] - high_prices[icase-1], 0.0)
DMminus = max(low_prices[icase-1] - low_prices[icase], 0.0)
if DMplus >= DMminus:
DMminus = 0.0
else:
DMplus = 0.0
# Use pre-computed ATR values from the ATR function
current_atr = ATR_values[icase] if icase < len(ATR_values) else np.nan
DMSplus = (lookback - 1.0) / lookback * DMSplus + DMplus
DMSminus = (lookback - 1.0) / lookback * DMSminus + DMminus
if current_atr > 0:
DIplus = 100.0 * DMSplus / current_atr
DIminus = 100.0 * DMSminus / current_atr
DX = 100.0 * np.abs(DIplus - DIminus) / (DIplus + DIminus + 1e-10)
if icase == lookback:
ADX = DX # Initialize ADX with the first DX value
else:
ADX = (ADX * (lookback - 1) + DX) / lookback
output[icase] = ADX
else:
output[icase] = output[icase - 1] # Handle cases where ATR is zero or undefined
return output
Plotting the ADX Indicator
Before plotting, I want to note a few things about the data and visualizations for reference. The data is ten years of daily data fetched using the yfinace library. The purpose of the tests at this level are to test the indicator itself and set up some simple visualizations that will help us when we get to the strategy research phase. For these purposes, the data from yfinance
will be just fine. The code for this section and how I formatted my data frame can be found in the PDF.
I will be using the Logarithmic Returns (log returns) for comparison on plots and charts. Log returns, calculated by taking the natural logarithm of the ratio of consecutive closing prices, are preferred for mathematical tractability in financial models. In the PDF, I calculate simple and cumulative returns for examples as well.
The following sections are a series of visualizations for examining the ADX and determining whether we can see any patterns or extract information manually. We will plot the ADX and returns separately and look for any apparent nonstationarity in our series. Then, we will create a series of scatter plots and get some initial impressions from our data.
ADX Plot
Impressions
Visual inspection of the ADX plot shows our indicator (at least in the 10 year window we are looking at) is mostly stationary. Values appear to rarely break above the middle line at 50. It is possible that the lookback window of 7 is overly sensitive.
Log Returns Plot
Impressions
Returns appear mostly stationary. There is a clear aberrancy in 2020 that might show a break in the mean in future testing.
ADX vs. Log Returns
This plot will help us understand where the most valuable information in the ADX resides in comparison to the returns. A vertical line has been placed on the chart to help with visualization, along with one horizontal line denoting where our threshold value is located.
Impressions
Returns appear evenly spread across the median of returns and the ADX threshold value. Below the threshold value, returns appear to have a tighter cluster. As ADX values increase (especially over 50), there appears to be more occurrences of positive returns but only slightly so. Overall, the plot appears quite symmetrical.
ADX vs. Volume
These charts help us determine if there is any correlation between the volume and the ADX value. First, we plot all the data as a scatter plot to determine if there are any specific variations we want to examine. Then, we generate scatter plots for ADX values above and below any area of interest.
Impressions
The majority of data is below 1e8 (100,000,000), maybe more like .751e8 (75,000,000. There is a very dense/tight cluster below the ADX threshold but it opens up above the threshold.
ADX vs. Returns w/ Volume Thresholds
This comparison will separate our returns into two different plots. The first one will be high volume (defined as > 1e8 volume) followed by low volume (defined as < 1e8 volume). This should help us determine if the volume can help improve trade entry confidence.
Impressions
Low or high volume doesn't seem to change the shape of this plot much. It appears to be symmetrical regardless. Adjusting the volume threshold my help get a better understanding, but I believe that the symmetry is going to be seen throughout most of the comparisons.
Key Observations:
The ADX indicator remains mostly stationary throughout the 10-year period, with infrequent excursions above the critical threshold of 50. This suggests that the chosen lookback period of 7 days, while sensitive, is generally stable across long-term analysis.
Log returns and the ADX indicator exhibit a largely symmetrical relationship. Although there is a marginal indication that higher ADX values correlate with slightly more positive returns, this effect is not pronounced enough to assert significant predictive power without further analysis.
Notable deviations in return patterns, such as the aberration observed in 2020, underscore the potential for external market shocks to disrupt the mean. Such instances warrant closer scrutiny in future tests to ascertain their implications on predictive strategies.
The examination of volume interaction with ADX values revealed that most trading activity clusters below 75 million units. There is no significant correlation observed between trading volume and ADX values, indicating that volume thresholds alone provide limited predictive value concerning ADX movements.
The visual analysis suggests that while the ADX indicator provides a reliable measure of market trends and stability, its utility in predicting future returns is limited without the integration of additional variables, such as other indicators. Further research should explore the integration of other indicators and external variables to enhance the predictive accuracy and develop a trading strategy.
Conclusion
We ended up having to create an additional indicator to create this one, but now it's done. We have an ATR indicator and the ADX indicator. That was a bit more complicated than I wanted it to be. I am happy I didn't decide to use the original smoothing method. The results for my ADX and the TA-Lib ADX appear to be the same, so that's nice.
The next indicator is the MACD. We have two trend analysis indicators and we are going to wrap it up with the MACD. While the MACD is also a trend indicator, it can also be used a trigger for trade entry with trading strategies. After this, we move into research and testing. Anyone have any idea what criteria we are going to look at using for this strategy yet?
Happy trading and happy coding.
The code for strategies and the custom functions/framework I use for strategy development in Python and NinjaScript can be found at the Hunt Gather Trade GitHub. This code repository will house all code related to articles and strategy development. If there are any paid subscribers without access, please contact me via e-mail. I do my best to invite members quickly after they subscribe. That being said, please try and make sure the e-mail you use with Substack is the e-mail associated with GitHub. It is difficult to track members otherwise.
Feel free to comment below or e-mail me if you need help with anything, wish to criticize, or have thoughts on improvements. Paid subscribers can access this code and more at the private HGT GitHub repo. As always, this newsletter represents refined versions of my research notes. That means these notes are plastic. There could be mistakes or better ways to accomplish what I am trying to do. Nothing is perfect, and I always look for ways to improve my techniques.
[1]: Masters, T. (2020). Statistically sound indicators for financial market prediction.
Research Notes on PDF