Build A Profitable ADX Trading Strategy

How To Build A Profitable ADX Trading Strategy Using Python

Average Directional Index

The direction or trend has played a major role in the financial market. An increase or decrease in the price of a financial instrument can occur for two reasons: randomness or fundamental factors. There is a saying in the financial market “The trend is your friend”.

The Average Directional Index is a trading indicator that calculates the trend strength. It does not measure the direction. Welles Wilder developed it in 1978. This article shows you how to build a profitable ADX trading strategy using Python. The aim is to show how it can be done in Python, and only secondary is the strategy itself.

The idea of the indicator is to compare the difference between the last two Highs and the difference between the last two Lows.

The first step is to generate the following series:

\begin{align*}
\text{Step 1} \\
&\text{Up Movement} = \text{High} - \text{High}_{t-1} \\
&\text{Down Movement} = \text{Low} - \text{Low}_{t-1} \\
&+DM = 
\begin{cases}
\text{Up Movement,} & \text{if Up Movement} > 0 \text{ and Up Movement} > \text{Down Movement} \\
0, & \text{Otherwise}
\end{cases} \\
&-DM = 
\begin{cases}
\text{Down Movement,} & \text{if Down Movement} > 0 \text{ and Down Movement} > \text{Up Movement} \\
0, & \text{Otherwise}
\end{cases} \\
\text{Step 2} \\
&+DI = \frac{\text{SmootedDM}(n)}{\text{ATR}} \times 100 \\
&-DI = \frac{\text{SmootedDM}(n)}{\text{ATR}} \times 100 \\
\text{Step 3} \\
&DX = \frac{\left|\text{+DI} - \text{-DI}\right|}{\left|\text{+DI} + \text{-DI}\right|} \times 100 \\
\text{Step 4} \\
&\text{ADX} = \frac{n \times \text{ADX}_{t-1} - \text{DX}}{14}
\end{align*}

Where:

Up Movent: current High minus previous High 

Up Down: current Low minus previous Low

+DI: Positive Directional Index

-DI: Negative Directional Index

DM: Directional Movement

SmotedDM(n): n-period Moving Average from DM 

DX: Directional Movement Index

ADX: Average Directional Movement Index

Python Section

Event Driven Backtest vs Vectorized Backtest

A backtest simulates the market condition using a predefined trading strategy. Backtesting aims to produce reasonable results that mirror the market conditions. There are two types of backtests: event-driven and vectorized.

An event-driven backtest calculates relevant metrics period by period, including profit and loss, number of trades, Sharpe ratio, etc. It can also consider commissions, slippage, order rejection, etc. This type of infrastructure is suitable for real data and live trading. Meanwhile, a vectorized backtesting system can include them after running the strategy. It is a disadvantage because it is unsuitable for real data and live trading.

Backtrader

Backtrader is a Python library designed to backtest trading strategies. This library uses the event-driven approach. In GitHub, the code repository, backtrader is at the top, in the search “backtest” or “backtesting”. This framework can import data from different sources, generate metrics, plot charts, and make live trading.

Backtrader is not the only backtesting package available. Other alternatives are: zipline, backtesting, bt, qstrader, qsforex, Basana, etc.

Implementing Strategy

This article will implement two strategies:

Go long if ADX(14) > 30 and RSI(4) < 30, hold until ADX(16) > 30 and RSI(4) > 70.

Go short if ADX(14) > 30 and RSI(4) > 70, and hold until ADX(16) > 30 and RSI(4) < 30.

The reason for choosing those parameters comes after analyzing the data and running the code with different parameter values. An ADX(16) > 30 for the data indicates a strong increase in the price, and the RSI(4) is to find when the stock is overbought (or oversold). Therefore, the joint strategy will look for a retracement to place trades.

Full Code

The following images show the code of this article. Later on, this article will break down each component.

Long strategy:

How To Build A Profitable ADX Trading Strategy Using Python
How To Build A Profitable ADX Trading Strategy Using Python

Short strategy:

Download data:

Function to run backtrader:

Run backtest:

Backtesting Strategy

As in the previous tutorial, the first part is to import the Python libraries:

Before delving into the next section of the code, it is important to be familiar with the Object Oriented Programming approach (OOP). This article backtests a trading strategy following the OOP. In addition, the following link explains the inheritance mechanism in the OOP. Both articles will shed light on the Backtrader code structure.

To use backtrader is convenient to start the explanation from the button to the top. Let’s start by downloading the data.

The Cerebro class is the cornerstone of Backtrader and serves to gather data feeds, strategies, observers, analyzers, and writers. It implements backtesting or live trading, collects and returns results, and plots the backtesting result.

The run_backtest function implements in one place the Backtrader backtesting procedure. The function has three parameters, the strategy class StrategyToBackTest, the historical data in pandas data frame format data_to_use, and the ADX window length adx_window.

First, you need to instantiate cerebro = bt.Cerebro(stdstats=False). Then, add the strategy and the window length to cerebro.addstrategy(StrategyToBackTest, adx_window).

Subsequently, Backtrader requires to add the pandas dataframe cerebro.adddata(data0). Backtrader allows the possibility to import data in different formats.

The analyzer helps to obtain trading metrics; this tutorial will use an analyzer for trades bt.analyzers.TradeAnalyzer, returns bt.analyzers.Returns, drawdown bt.analyzers.DrawDown, and the pyfolio analyzer bt.analyzers.PyFolio.

The next step is to run the strategy bresult = cerebro.run(stdstats=False); in this step, Backtrader will wrap data, strategy, and analyzer and implement the backtest.

Let’s see how to get the analyzer results:

Before running the backtest in red you will find _name = “trades. That is the name to identify the trade analyzer TradeAnalyzer.

After running the backtest bresult = cerebro.run(stdstats=False). You can see insightful information about the strategy. For example, to see the number of trades bresult[0].analyzers.trades.get_analysis().total.closed.

You need to add the trade analyzer name trades to bresult[0].analyzer, and then add .get_analysis(). In the same way to get the number of days in the market bresult[0].analyzers.trades.get_analysis().len.total. In blue, you will see highlighted the procedure to get the return analyzer.

The remaining section from run_backtest will show statistics and plot our strategy.

Let’s execute the long strategy:

The run_backtest function will get the ADXStrategy for long positions, the dataset dataset, and the number of parameters 16.

An essential part of backtrader is to add a class with the strategy to backtest ADXStrategy. This class has the logic to implement the ADX(16)-RSI(4) strategy.

The following image shows the ADXStrategy. The strategy for long positions. This strategy inheritance the bt.Strategy module. bt.Strategy will add all the backtrader functionalities to run your backtest. ADXStrategy has four functions: __init__lognotify_order, and next.

Let’s break down the previous code.

The __init__ function is the place to add the main parameters of the strategy; this is the part to initialize the ADX, RSI, and thresholds. The terms self.adx and self.rsi calculate the ADX and RSI, respectively. While self.adx_thresholdself.rsi_upper_threshold, and self.rsi_lower_threshold are the thresholds to generate the buy and sell signals. Finally, self.order will get the buy (or sell) operations.

The next function implements the trading logic. In case the ADX > 30, and the RSI < 30, and there is no position, the algorithm will invest 95% of the portfolio, and it will place long positions. The remaining part of the function evaluates the moment to close the positions.

In case the algorithm places an order self.order_target_percent, the order will go to the notify_order function. This function will check if the order was submitted or accepted (if order.status in [order.Submitted, order.Accepted]).

If it is true, the notify_order function will do nothing. Then it will check if the order is completed order.status is order.completed. If the previous condition is met, the algorithm will check if the order was a long order.isbuy() or short position order.issell().

The last function log will print the operations. The term dt.isoformat() refers to the date, and txt refers to the operation transaction.

After running the code, the plot is:

In the previous image, there are four charts. At the top, the first chart illustrates the equity curve. The second chart, the main chart, plots the closing price, and in green or red triangles, adds the buy or sell signals.

Also, it adds the volume. The third chat represents the Average Volume Index (ADX). The last chart at the bottom shows the RSI.

Having a threshold for the Average Volume Index (ADX) of 30 allows to capture profitable only long strategy. The chart has over 20 years of daily data, making it difficult to visualize and analyze the trades.

A recommendation is to zoom over the matplotlib chart and see the trades. Remember that the ADX measures the intensity of the trade and not the direction.

When we see the green arrows (long positions) and the red arrows (short positions), they generate trades along the time interval. This is a good symptom because picks up profitable trades.

These are some statistics:

  • Maximum Drawdown = 42.32%
  • Buy and Hold Total Return = 358.39%
  • Strategy Total Return = 931.44%
  • CAGR Strategy: 10.70%
  • Time spent in the market: 50.99%
  • % Won Strategy = 0.833

Short Strategy

The short strategy takes the properties from the long strategy ADXStrategyShort(ADXStrategy), and only modifies the next function to implement the short trading strategy. The next function will place short positions when ADX >30, and RSI > 70%, and if there is no position, the algorithm will invest 95% of the portfolio.

To run the only short position strategy:

The plot is:

Some metrics:

  • Total return: -12.92%
  • CARG = -0.60%
  • Time spent in the market: 44.89%
  • % Won = 63.41%
  • Maximum Drawdown: 70.03%

This article was an introduction to Backtrader, but Backtrader has too many features that you can include for backtesting a strategy.

Conclusion

The Average Volume Index (ADX) indicator works. It needs time to analyze to analyze the data, but you will find great ideas.

Similar Posts