The Donchian Channel & Moving Average Trading Strategy.
Creating & Back-testing the Donchian Channel & Moving Average Trading Strategy in Python.
A lot of trading strategies can be found online where authors almost hint (or even guarantee) that you will make a lot of money if you follow it. This article takes one of those strategies and tries to apply a simple back-test to test its predictability.
I have just published a new book after the success of my previous one “New Technical Indicators in Python”. It features a more complete description and addition of structured trading strategies with a GitHub page dedicated to the continuously updated code. If you feel that this interests you, feel free to visit the below link, or if you prefer to buy the PDF version, you could contact me on LinkedIn.
The Book of Trading Strategies
Amazon.com: The Book of Trading Strategies: 9798532885707: Kaabar, Sofien: Bookswww.amazon.com
Fetching Historical FX Data
One of the most famous trading platforms in the retail community is the MetaTrader5 software. It is a powerful tool that comes with its own programming language and its huge online community support. It also offers the possibility to export its historical short-term and long-term FX data.
The first thing we need to do is to simply download the platform from the official website. Then, after creating the demo account, we are ready to import the library in Python that allows to import the OHLC data from MetaTrader5.
A library is a group of structured functions that can be imported into our Python interpreter from where we can call and use the ones we want.
The easiest way to install the MetaTrader5 library is to go to the Python prompt on our computer and type:
pip install MetaTrader5
This should install the library in our local Python. Now, we want to import it to the Python interpreter (such as PyCharm or SPYDER) so that we can use it. Let us actually import all the libraries we will be using for this:
import datetime # Date acquiring
import pytz # Time zone management
import pandas as pd # Mostly for Data frame manipulation
import MetaTrader5 as mt5 # Importing OHLC data
import matplotlib.pyplot as plt # Plotting charts
import numpy as np # Mostly for array manipulation
Anything that comes after “as” is a shortcut. The plt shortcut is there so that each time we want to call a function from that library we do not have to type the full matplotlib.pyplot statement.
The official documentation for the Metatrader5 library can be found here.
The first thing we can do is to select which time frame we want to import. Let us suppose that there are only two time frames, the 30-minute and the hourly bars. We can therefore create variables that hold the statement to tell the MetaTrader5 library which time frame we want.
# Choosing the 30-minute time frame
frame_M30 = mt5.TIMEFRAME_M30
# Choosing the hourly time frame
frame_H1 = mt5.TIMEFRAME_H1
Then, by staying in the spirit of importing variables, we can define the variable that states what date is it now. This helps the algorithm know the stopping date of the import. We can do this by the simple line of code below.
# Defining the variable now to give out the current date
now = datetime.datetime.now()
Note that these code snippets are better used chronologically, hence, I encourage you to copy them in order and then execute them one by one so that you understand the evolution of what you are doing. The below is a function that holds which assets we want. Generally, I use 10 or more but for simplicity, let us consider that there are only two currency pairs: EURUSD and USDCHF.
def asset_list(asset_set):
if asset_set == 'FX':assets = ['EURUSD', 'USDCHF']
return assets
Now, with the key function that gets us the OHLC data. The below establishes a connection to MetaTrader5, applies the current date, and extracts the needed data. Notice the arguments year, month, and day. These will be filled by us to select from when do we want the data to start. Note, I have inputed Europe/Paris as my time zone, you should use your time zone to get more accurate data.
def get_quotes(time_frame, year = 2005, month = 1, day = 1, asset = "EURUSD"):
# Establish connection to MetaTrader 5
if not mt5.initialize():
print("initialize() failed, error code =", mt5.last_error())
quit()
timezone = pytz.timezone("Europe/Paris")
utc_from = datetime.datetime(year, month, day, tzinfo = timezone)
utc_to = datetime.datetime(now.year, now.month, now.day + 1, tzinfo = timezone)
rates = mt5.copy_rates_range(asset, time_frame, utc_from, utc_to)
rates_frame = pd.DataFrame(rates)
return rates_frame
And finally, the last function we will use is the one that uses the below get_quotes function and then cleans the results so that we have a nice array. We have selected data since January 2019 as shown below.
def mass_import(asset, horizon):
if horizon == 'M30':
data = get_quotes(frame_M30, 2019, 1, 1, asset = assets[asset])
data = data.iloc[:, 1:5].values
data = data.round(decimals = 5)
return data
Finally, we are done building the blocks necessary to import the data. To import EURUSD OHLC historical data, we simply use the below code line:
# Choosing the horizon
horizon = 'M30'
# Creating an array called EURUSD having M30 data since 2019
EURUSD = mass_import(0, horizon)
And voila, now we have the EURUSD OHLC data from 2019.
The Donchian Channel
Created by Richard Donchian, this simple and great indicator is used to identify breakouts and reversals. Our goal is to determine objectively a range exit by the surpass or break of any of the barriers.
The way it is formed is by first calculating the maximum of the last n-period highs and the minimum of the last n-period lows, then calculating the average of them both. This gives us three measures:
The Donchian upper band.
The Donchian lower band.
the Middle band as the average between the two bands. We will be omitting this band from the research as it does not bring much to the table.
We can code the channel using this code.
def adder(Data, times):
for i in range(1, times + 1):
new = np.zeros((len(Data), 1), dtype = float)
Data = np.append(Data, new, axis = 1)
return Data
def deleter(Data, index, times):
for i in range(1, times + 1):
Data = np.delete(Data, index, axis = 1)
return Data
def jump(Data, jump):
Data = Data[jump:, ]
return Data
def donchian(Data, low, high, lookback, where, median = 1):
for i in range(len(Data)):
try:
Data[i, where] = max(Data[i - lookback:i + 1, high])
except ValueError:
pass
for i in range(len(Data)):
try:
Data[i, where + 1] = min(Data[i - lookback:i + 1, low])
except ValueError:
pass
if median == 1:
for i in range(len(Data)):
try:
Data[i, where + 2] = (Data[i, where] + Data[i, where + 1]) / 2
except ValueError:
pass
Data = jump(Data, lookback)
lookback_donchian = 20
# Calling the function
my_data = donchian(my_data, 2, 1, lookback_donchian, 4, median = 0)
The way the channel is used is by assuming each time the market surpasses the last n-period high or low, a breakout has happened. Traders like this indicator because it is objective as opposed to just tracing trend lines manually to detect breakouts.
If you want to support me and the articles I regularly publish, please consider subscribing to my Newsletter (A Free Plan is Available) via the below link. This would help me continue sharing my research. Thank you!
All About Trading!
Sharing Trading Strategies, Knowledge, and Technical Tools in Detail.abouttrading.substack.com
Moving Averages
Moving averages help us confirm and ride the trend. They are the most known technical indicator and this is because of their simplicity and their proven track record of adding value to the analyses. We can use them to find support and resistance levels, stops and targets, and to understand the underlying trend. This versatility makes them an indispensable tool in our trading arsenal.
As the name suggests, this is your plain simple mean that is used everywhere in statistics and basically any other part in our lives. It is simply the total values of the observations divided by the number of observations. Mathematically speaking, it can be written down as:
We can see that the moving average is providing decent dynamic support and resistance levels from where we can place our orders in case the market goes down there. The code for the moving average can be written down as the following:
def ma(Data, lookback, close, where):
Data = adder(Data, 1)
for i in range(len(Data)):
try:
Data[i, where] = (Data[i - lookback + 1:i + 1, close].mean())
except IndexError:
pass
return Data
The below states that the moving average function will be called on the array named my_data for a lookback period of 200, on the column indexed at 3 (closing prices in an OHLC array). The moving average values will then be put in the column indexed at 4 which is the one we have added using the adder function.
my_data = ma(my_data, 200, 3, 4)
If you are also interested by more technical indicators and using Python to create strategies, then my best-selling book on Technical Indicators may interest you:
New Technical Indicators in Python
Amazon.com: New Technical Indicators in Python: 9798711128861: Kaabar, Mr Sofien: Bookswww.amazon.com
Creating & Evaluating the Strategy
The strategy is part of the trend-following arsenal and it capitulates on the fact that when an objective breakout happens, we need to confirm that we are on the right side by looking at the current status of two moving averages. Here is how.
For a buy (Long) signal, the market price must surpass the previousDonchian high (Therefore, the current close is higher than the highest high n-periods ago excluding the current high), while simultaneously, the short-term (20-period) moving average is above the long-term (60-period) moving average.
For a sell (Short) signal, the market price must break the previous Donchian low (Therefore, the current close is lower than the lowest low n-periods ago excluding the current low), while simultaneously, the short-term (20-period) moving average is below the long-term (60-period) moving average.
To code the above conditions, we need to write this signal function.
def signal(Data, close, donchian_upper, donchian_lower, ma_long_column, ma_short_column, buy, sell):
for i in range(len(Data)):
if Data[i, close] > Data[i - 1, donchian_upper] and \
Data[i, ma_short_column] > Data[i, ma_long_column] and \
Data[i - 1, buy] == 0:
Data[i, buy] = 1
elif Data[i, close] < Data[i - 1, donchian_lower] and \
Data[i, ma_short_column] < Data[i, ma_long_column] and \
Data[i - 1, sell] == 0:
Data[i, sell] = -1
return Data
# Strategy Code
lookback_donchian = 20
lookback_ma_long = 60
lookback_ma_short = 20
# Adding a few empty columns to be populated
my_data = adder(my_data, 10)
# Calculating the 20-period Donchian Channel
my_data = donchian(my_data, 2, 1, lookback_donchian, 4, median = 0)
# Calculating the 60-period moving average
my_data = ma(my_data, lookback_ma_long, 3, 6)
# Calculating the 20-period moving average
my_data = ma(my_data, lookback_ma_short, 3, 7)
# Calling the function
my_data = signal(my_data, 3, 4, 5, 6, 7, 8, 9)
Having had the signals, we now know when the algorithm would have placed its buy and sell orders, meaning, that we have an approximate replica of the past where can can control our decisions with no hindsight bias. We have to simulate how the strategy would have done given our conditions. This means that we need to calculate the returns and analyze the performance metrics. Let us see a neutral metric that can give us somewhat a clue on the predictability of the indicator or the strategy. For this study, we will use the Signal Quality metric.
The signal quality is a metric that resembles a fixed holding period strategy. It is simply the reaction of the market after a specified time period following the signal. Generally, when trading, we tend to use a variable period where we open the positions and close out when we get a signal on the other direction or when we get stopped out (either positively or negatively).
Sometimes, we close out at random time periods. Therefore, the signal quality is a very simple measure that assumes a fixed holding period and then checks the market level at that time point to compare it with the entry level. In other words, it measures market timing by checking the reaction of the market after a specified time period.
# Choosing a Holding Period for a trend-following strategy
my_data = deleter(my_data, 4, 2)
period = 30
def signal_quality(Data, closing, buy, sell, period, where):
Data = adder(Data, 1)
for i in range(len(Data)):
try:
if Data[i, buy] == 1:
Data[i + period, where] = Data[i + period, closing] - Data[i, closing]
if Data[i, sell] == -1:
Data[i + period, where] = Data[i, closing] - Data[i + period, closing]
except IndexError:
pass
return Data
# Applying the Signal Quality Function
my_data = signal_quality(my_data, 3, 6, 7, period, 8)
positives = my_data[my_data[:, 8] > 0]
negatives = my_data[my_data[:, 8] < 0]
# Calculating Signal Quality
signal_quality = len(positives) / (len(negatives) + len(positives))
print('Signal Quality = ', round(signal_quality * 100, 2), '%')
# Output Signal Quality EURUSD = 49.75%
# Output Signal Quality USFCHF = 50.06%
# Output Signal Quality GBPUSD = 49.42%
# Output Signal Quality AUDUSD = 48.64%
# Output Signal Quality NZDUSD = 48.58%
# Output Signal Quality USDCAD = 47.60%
A signal quality of 49.75% means that on 100 trades, we tend to see a profitable result in 49–50 of the cases without taking into account transaction costs.
We now know that the fixed holding period strategy does not work using 30 periods. I will spare you the hassle, it does not work with other periods either. Maybe with variable holding periods and with risk management it can be improved. The moral of the story, always do your homework before considering adopting a strategy online. Even this back-test is not conclusive, therefore, you must draw your own conclusions as to whether a strategy is good or bad. All I am doing is providing codes for technical indicators, ideas for different combinations, and simplistic results.
Conclusion
Remember to always do your back-tests. You should always believe that other people are wrong. My indicators and style of trading may work for me but maybe not for you.
I am a firm believer of not spoon-feeding. I have learnt by doing and not by copying. You should get the idea, the function, the intuition, the conditions of the strategy, and then elaborate (an even better) one yourself so that you back-test and improve it before deciding to take it live or to eliminate it. My choice of not providing specific Back-testing results should lead the reader to explore more herself the strategy and work on it more.
To sum up, are the strategies I provide realistic? Yes, but only by optimizing the environment (robust algorithm, low costs, honest broker, proper risk management, and order management). Are the strategies provided only for the sole use of trading? No, it is to stimulate brainstorming and getting more trading ideas as we are all sick of hearing about an oversold RSI as a reason to go short or a resistance being surpassed as a reason to go long. I am trying to introduce a new field called Objective Technical Analysis where we use hard data to judge our techniques rather than rely on outdated classical methods.