The SuperTrend Indicator Combined With the MACD Oscillator.
Creating a Strategy Based on the SuperTrend & the MACD Oscillator in Python.
Combining technical indicators remains the first step towards building a complex trading strategy. When we use indicators, we are trying to understand the reaction and the direction of the market, but we are simply following the hidden signals and trying to trade the mentioned reactions as opposed to actually predicting a semi-random complex environment.
I have just published a new book after the success of my previous one “New Technical Indicatorsin 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: Books www.amazon.com
The Super Trend Indicator
The first concept we should understand before creating the SuperTrend indicator is volatility. We sometimes measure volatility using the Average True Range. Although the ATR is considered a lagging indicator, it gives some insights as to where volatility is right now and where has it been last period (day, week, month, etc.). But before that, we should understand how the True Range is calculated (the ATR is just the average of that calculation).
The true range is simply the greatest of the three price differences:
High — Low
| High — Previous close |
| Previous close — Low |
Once we have got the maximum out of the above three, we simply take an average of n periods of the true ranges to get the Average True Range. Generally, since in periods of panic and price depreciation we see volatility go up, the ATR will most likely trend higher during these periods, similarly in times of steady uptrends or downtrends, the ATR will tend to go lower.
One should always remember that this indicator is very lagging and therefore has to be used with extreme caution. Below is the function code that calculates a form of the ATR. Note that the moving average function is discussed in the second part of the article.
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 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 # Cleaning Data = jump(Data, lookback) return Data def ema(Data, alpha, lookback, what, where): alpha = alpha / (lookback + 1.0) beta = 1 - alpha # First value is a simple SMA Data = ma(Data, lookback, what, where) # Calculating first EMA Data[lookback + 1, where] = (Data[lookback + 1, what] * alpha) + (Data[lookback, where] * beta) # Calculating the rest of EMA for i in range(lookback + 2, len(Data)): try: Data[i, where] = (Data[i, what] * alpha) + (Data[i - 1, where] * beta) except IndexError: pass return Data def atr(Data, lookback, high, low, close, where, genre = 'Smoothed'): # Adding the required columns Data = adder(Data, 1) # True Range Calculation for i in range(len(Data)): try: Data[i, where] = max(Data[i, high] - Data[i, low], abs(Data[i, high] - Data[i - 1, close]), abs(Data[i, low] - Data[i - 1, close])) except ValueError: pass Data[0, where] = 0 if genre == 'Smoothed': # Average True Range Calculation Data = ema(Data, 2, lookback, where, where + 1) if genre == 'Simple': # Average True Range Calculation Data = ma(Data, lookback, where, where + 1) # Cleaning Data = deleter(Data, where, 1) Data = jump(Data, lookback) return Data
Now that we have understood what the ATR is and how to calculate it, we can proceed further with the SuperTrend indicator.
Now that we have our plan of attack laid out in front of us, we can proceed by understanding the SuperTrend and coding it.
Calculate the ATR using the function provided above.
Use the below formulas to calculate the SuperTrend.
Plot the indicator, create trading rules and analyze the results.
The indicator seeks to provide entry and exit levels for trend followers. You can think of it as a moving average or an MACD. Its uniqueness is its main advantage and although its calculation method is much more complicated than the other two indicators, it is intuitive in nature and not that hard to understand. Basically, we have two variables to choose from. The ATR lookback and the multiplier’s value. The former is just the period used to calculated the ATR while the latter is generally an integer (usually 2 or 3).
The eATR above in the function is simply a normal ATR with a lookback multiplied by 2, then we subtract 1. But this is for illustrative purposes only and the only difference is the lookback period. You can use 5 or 500 if you wish.
The first thing to do is to average the high and low of the price bar, then we will either add or subtract the average with the selected multiplier multiplied by the ATR as shown in the above formulas. This will give us two arrays, the basic upper band and the basic lower band which form the first building blocks in the SuperTrend indicator. The next step is to calculate the final upper band and the final lower band using the below formulas.
It may seem complicated but most of these conditions are repetitive and in any case, I will provide the Python code below so that you can play with the function and optimize it to your trading preferences. Finally, using the previous two calculations, we can find the SuperTrend.
def supertrend(Data, multiplier, atr_col, close, high, low, where): Data = adder(Data, 6) for i in range(len(Data)): # Average Price Data[i, where] = (Data[i, high] + Data[i, low]) / 2 # Basic Upper Band Data[i, where + 1] = Data[i, where] + (multiplier * Data[i, atr_col]) # Lower Upper Band Data[i, where + 2] = Data[i, where] - (multiplier * Data[i, atr_col]) # Final Upper Band for i in range(len(Data)): if i == 0: Data[i, where + 3] = 0 else: if (Data[i, where + 1] < Data[i - 1, where + 3]) or (Data[i - 1, close] > Data[i - 1, where + 3]): Data[i, where + 3] = Data[i, where + 1] else: Data[i, where + 3] = Data[i - 1, where + 3] # Final Lower Band for i in range(len(Data)): if i == 0: Data[i, where + 4] = 0 else: if (Data[i, where + 2] > Data[i - 1, where + 4]) or (Data[i - 1, close] < Data[i - 1, where + 4]): Data[i, where + 4] = Data[i, where + 2] else: Data[i, where + 4] = Data[i - 1, where + 4] # SuperTrend for i in range(len(Data)): if i == 0: Data[i, where + 5] = 0 elif (Data[i - 1, where + 5] == Data[i - 1, where + 3]) and (Data[i, close] <= Data[i, where + 3]): Data[i, where + 5] = Data[i, where + 3] elif (Data[i - 1, where + 5] == Data[i - 1, where + 3]) and (Data[i, close] > Data[i, where + 3]): Data[i, where + 5] = Data[i, where + 4] elif (Data[i - 1, where + 5] == Data[i - 1, where + 4]) and (Data[i, close] >= Data[i, where + 4]): Data[i, where + 5] = Data[i, where + 4] elif (Data[i - 1, where + 5] == Data[i - 1, where + 4]) and (Data[i, close] < Data[i, where + 4]): Data[i, where + 5] = Data[i, where + 3] # Cleaning columns Data = deleter(Data, where, 5) return Data
The above chart shows the hourly values of the EURUSD with a 10-period SuperTrend (represented by the ATR period) and a multiplier of 1.25. For the function to work, we need an OHLC array containing a few spare columns.
Applying the above function on an OHLC array will give us something like the below when we plot it. The way we should understand the indicator is that when it goes above the market price, we should be looking to short and when it goes below the market price, we should be looking to go long as we anticipate a bullish trend. Remember that the SuperTrend is a trend-following indicator. The aim here is to capture trends at the beginning and to close out when they are over.
The MACD Oscillator
The MACD is probably the second most known oscillator after the RSI. One that is heavily followed by traders. It stands for Moving Average Convergence Divergence and it is used mainly for divergences and flips. Many people also consider it a trend-following indicator but others use graphical analysis on it to find reversal points, making the MACD a versatile indicator.
How is the MACD calculated? It is the difference between the 26-period Exponential Moving Average applied to the closing price and the 12-period Exponential Moving Average also applied to the closing price. The value found after taking the difference is called the MACD line. The 9-period Exponential Moving Average of that calculation is called the MACD signal. And here is how to code a function in Python that outputs the MACD on an OHLC array:
def macd(Data, what, long_ema, short_ema, signal_ema, where): Data = adder(Data, 1) Data = ema(Data, 2, long_ema, what, where) Data = ema(Data, 2, short_ema, what, where + 1) Data[:, where + 2] = Data[:, where + 1] - Data[:, where] Data = jump(Data, long_ema) Data = ema(Data, 2, signal_ema, where + 2, where + 3) Data = deleter(Data, where, 2) Data = jump(Data, signal_ema) return Data
As a reminder, the MACD line is the difference between the two exponential moving averages which is plotted as histograms in green and red. The MACD signal is simply the 9-period exponential moving average of the MACD line. It is the blue dashed line in the above plot. To plot the histograms, we can follow this function:
def indicator_plot_double_macd(Data, first, second, name = '', name_ind = '', window = 250): fig, ax = plt.subplots(2, figsize = (10, 5)) Chosen = Data[-window:, ] for i in range(len(Chosen)): ax.vlines(x = i, ymin = Chosen[i, 2], ymax = Chosen[i, 1], color = 'black', linewidth = 1) ax.grid() for i in range(len(Chosen)): if Chosen[i, second] > 0: ax.vlines(x = i, ymin = 0, ymax = Chosen[i, second], color = 'green', linewidth = 1) if Chosen[i, second] < 0: ax.vlines(x = i, ymin = Chosen[i, second], ymax = 0, color = 'red', linewidth = 1) if Chosen[i, second] == 0: ax.vlines(x = i, ymin = Chosen[i, second], ymax = 0, color = 'black', linewidth = 1) ax.grid() ax.axhline(y = 0, color = 'black', linewidth = 0.5, linestyle = '--') # Using the function indicator_plot_double_macd(my_data, closing_price, macd_column_first, name = '', name_ind = 'MACD', window = 500) plt.plot(my_data[-500:, macd_column_second], color = 'blue', linestyle = '--', linewidth = 0.5)
Creating the Strategy
As with any proper research method, the aim is to test the strategy and to be able to see for ourselves whether it is worth having as an add-on to our pre-existing trading framework or not.
The first step is creating the trading rules. When will the system buy and when will it go short? In other words, when is the signal given that tells the system that the current market will go up or down? We can create the following rules:
Buy (Long) whenever the market switches above the SuperTrend while the MACD is already above its zero level.
Sell (Short) whenever the market switches below the SuperTrend while the MACD is already below its zero level.
The above chart shows the signals generated following the above conditions using a 10-period SuperTrend indicator with 1.25 multiplier and a standard MACD (12, 26) without the signal line.
# Indicator / Strategy parameters ema_long = 26 ema_short = 12 signal_ema = 9 multiplier = 1.25 # Calculating the SuperTrend my_data = supertrend(my_data, multiplier, 4, 3, 1, 2, 5) # Calculating the MACD my_data = macd(my_data, 3, ema_long, ema_short, signal_ema, 6) # Removing the Signal Line my_data = deleter(my_data, 6, 1
The signal function to get the trading triggers can be written down as follows:
def signal(Data, close, super_trend_col, macd_col, buy, sell): Data = adder(Data, 10) for i in range(len(Data)): if Data[i, close] > Data[i, super_trend_col] and \ Data[i - 1, close] < Data[i - 1, super_trend_col] and \ Data[i, macd_col] > 0: Data[i, buy] = 1 elif Data[i, close] < Data[i, super_trend_col] and \ Data[i - 1, close] > Data[i - 1, super_trend_col] and \ Data[i, macd_col] < 0: Data[i, sell] = -1 return Data
The above function takes an OHLC data array with multiple empty columns to spare and populates columns 6 (Buy) and 7 (Sell) with the conditions that we discussed earlier. We want to input 1’s in the column we call “buy” and -1 in the column we call “sell”.
This later allows you to create a function that calculates the profit and loss by looping around these two columns and taking differences in the market price to find the profit and loss of a close-to-close strategy. Then you can use a risk management function that uses stops and profit orders.
Evaluating the Strategy
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. For the performance evaluation to be unbiased, we will use three signal periods:
3 closing bars: This signal period relates to quick market timing strategies based on immediate reactions. In other words, we will be measuring the difference between the closing price 3 periods after the signal and the entry price at the trigger.
8 closing bars: This signal period relates to market timing strategies with some slight lag. In other words, we will be measuring the difference between the closing price 8 periods after the signal and the entry price at the trigger.
21 closing bars: This signal period relates to more important reactions that can even signal a change in the trend. In other words, we will be measuring the difference between the closing price 21 periods after the signal and the entry price at the trigger.
# Choosing an example of 3 periods period = 21 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 # Using 3 Periods as a Window of signal Quality Check 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 for 3 periods: 49.94% # Output for 8 periods: 50.29% # Output for 21 periods: 51.52%
As this is a trend-following strategy, longer holding periods should correspond to marginally better quality . A signal quality of 51.52% means that on 100 trades, we tend to see in 51 of the cases a higher price 21 periods after getting the signal.
Conclusion & Important Disclaimer
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 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.