top of page
  • Nikhil Adithyan

Step-By-Step Implementation of the SuperTrend Indicator in Python

Updated: Apr 19, 2023

Learn to build a powerful trading strategy with the SuperTrend indicator in python




I’ve backtested a lot of trading strategies based on an extensive amount of technical indicators. Among those, some indicators’ results are off the charts and I term these as premium indicators. In today’s article, we are going to discuss a trend-following indicator that joins this exclusive list of premium indicators concerning its performance and efficiency. It’s none other than the SuperTrend indicator.


We will first explore what this indicator is all about and its complex calculation. Then, we will move on to building the indicator from scratch in Python and construct a simple trading strategy based on it. Finally, we will backtest it on the stock of Tesla and compare the strategy’s performance with the returns of SPY ETF (an ETF specifically designed to track the movements of the S&P 500 market index). Without further ado, let’s hop into the article.


Average True Range


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


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 the Exponential Moving Average (EMA) 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 = EMA 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 SuperTrend Indicator.


SuperTrend Indicator


As the name suggests, the SuperTrend indicator tracks the direction of a trending market. This indicator is well-known for its precision in spotting efficient buy and sell signals for trades. I’ve got to be honest that the calculation of the SuperTrend is kinda a complex one but I’ll breakdown into pieces to make you understand better.


There are two main components involved in the calculation of this indicator which are the lookback period and the multiplier. The lookback period is nothing but the number of data points to take into account for the calculation and the multiplier is the value used to multiply the ATR. The traditional setting of the SuperTrend indicator is 10 as the lookback period and 3 as the multiplier. By taking these settings into consideration, let’s proceed to the steps involved in the calculation of the SuperTrend indicator.


The first step involved in the calculation is to determine the 10-day ATR using the formula we discussed before. The second step is to determine the basic upper and lower band. To calculate these two bands, we need to first find the High Low average (let’s now call it HLA) which is calculated by adding the high and low values of the stock and dividing it by 2. Using these HLA values, the upper band is calculated by first multiplying the 10-day ATR values with the multiplier (which is 3) and adding the product with the HLA values. The same procedure applies to the calculation of the basic lower band too but instead of adding, we need to subtract the product with the HLA values. The calculation of the two bands can be mathematically represented as follows:



BASIC UPPER BAND = HLA + [ MULTIPLIER * 10-DAY ATR ]
BASIC LOWER BAND = HLA - [ MULTIPLIER * 10-DAY ATR ]

where,
HLA = High Low Average
MULTIPLIER = 3

Then comes the calculation of the final upper and lower bands which are the core components involved in the calculation of the SuperTrend indicator. There is no formula for the calculation of the final bands but instead, conditions are passed and the values will be appended to the bands concerning which condition gets satisfied. The condition for the current final upper band goes as follows:


  • If the current basic upper band is lesser than the previous final upper band or the previous closing price of the stock is greater than the previous final upper band, then, the current final upper band’s value is the current basic upper band.


  • If the condition fails to get satisfied, then the current final upper band’s value is the previous final upper band. The condition of the final upper band can be represented as follows:



IF C.BUB < P.FUB OR P.CLOSE > P.FUB: C.FUB = C.BUB
IF THE CONDITION IS NOT SATISFIED: C.FUB = P.FUB

where,
C.BUB = Current Basic Upper Band
P.FUB = Previous Final Upper Band
P.CLOSE = Previous Closing Price of the Stock
C.FUB = Current Final Upper Band

The condition for the current lower band goes as follows:


  • If the current basic lower band is greater than the previous final lower band or the previous closing price of the stock is lesser than the previous final lower band, then, the current final lower band’s value is the current basic lower band.


  • If this condition of the current final lower band fails to get satisfied, then the current final lower band is the previous final lower band. The condition can be represented as follows:



IF C.BLB > P.FLB OR P.CLOSE < P.FLB: C.FLB = C.BLB
IF THE CONDITION IS NOT SATISFIED: C.FLB = P.FLB

where,
C.BLB = Current Basic Lower Band
P.FLB = Previous Final Lower Band
P.CLOSE = Previous Closing Price of the Stock
C.FLB = Current Final Lower Band

Now we have all the essential components to determine the values of the SuperTrend indicator. Like how we used conditions to calculate the final bands’ values, the same applies to the calculation of the SuperTrend indicator too. While there is only one condition for determining the final bands’ values, there are four different conditions for the SuperTrend indicator. The conditions for the current SuperTrend value goes as follows:


  • If the previous SuperTrend indicator value is equal to the previous final upper band and the current closing price of the stock is lesser than the current final upper band, then, the current SuperTrend indicator value is the current final upper band.


  • If the previous SuperTrend indicator value is equal to the previous final upper band and the current closing price of the stock is greater than the current final upper band, then, the current SuperTrend indicator value is the current final lower band.


  • If the previous SuperTrend indicator value is equal to the previous final lower band and the current closing price of the stock is greater than the current final lower band, then, the current SuperTrend indicator value is the current final lower band.


  • If the previous SuperTrend indicator value is equal to the previous final lower band and the current closing price of the stock is lesser than the current final lower band, then, the current SuperTrend indicator value is the current final upper band.


When putting all these conditions together, the collective number of conditions can be represented as follows:



IF P.ST == P.FUB AND C.CLOSE < C.FUB: C.ST = C.FUB
IF P.ST == P.FUB AND C.CLOSE > C.FUB: C.ST = C.FLB
IF P.ST == P.FLB AND C.CLOSE > C.FLB: C.ST = C.FLB
IF P.ST == P.FLB AND C.CLOSE < C.FLB: C.ST = C.FUB

where,
P.ST = Previous SuperTrend indicator value
P.FUB = Previous Final Upper Band
P.FLB = Previous Final Lower Band
C.CLOSE = Current Closing Price of the Stock
C.ST = Current SuperTrend indicator value
C.FUB = Current Final Upper Band
C.FLB = Current Final Lower Band

That’s the whole process of calculating the SuperTrend indicator values. To build a stronger understanding of the indicator and how it works, let’s explore a chart where the closing price of a stock is plotted along with the SuperTrend indicator’s readings.




In the above chart, the blue line represents the closing price of the Tesla stock and the line with both red and green color represents the readings of the SuperTrend indicator. The line of the SuperTrend indicator turns green if the readings of the indicator are below the closing price and turns red if it’s above the closing price. As I said before, the SuperTrend indicator is a trend-following indicator and this can be observed in the chart that the indicator directly reveals the current trend of the market more accurately.


Traders use the color changes or trend changes observed in the SuperTrend indicator line to mark buy and sell signals for their trades. To be more elaborate, traders go long (buy the stock) if the indicator’s line crosses from above to below the closing price line, and similarly, they go short (sell the stock) if the indicator’s line crosses from below to above the closing price line. This SuperTrend strategy is called the crossover strategy. This strategy can be represented as follows:



IF PREV.ST > PREV.CLOSE AND CUR.ST < CUR.CLOSE ==> BUY SIGNAL
IF PREV.ST < PREV.CLOSE AND CUR.ST > CUR.CLOSE ==> SELL SIGNAL

This is the strategy we are going to implement in this article too. Many other strategies can also be implemented based on the SuperTrend indicator but just to make things simple to understand, we are going with the crossover strategy. This concludes our theory part on the SuperTrend indicator. Now, let’s move on to the coding part where we are first going to build the indicator from scratch, build the crossover strategy which we just discussed, then, compare our strategy’s performance with the SPY ETF’s returns in Python. Let’s do some coding! Before moving on, a note on disclaimer: This article’s sole purpose is to educate people and must be considered as an information piece but not as investment advice or so.


Implementation in Python


The coding part is classified into various steps as follows:



1. Importing Packages
2. Extracting Stock Data from Twelve Data
3. SuperTrend Calculation
4. Creating the Crossover Trading Strategy
5. Plotting the Trading Lists
6. Creating our Position
7. Backtesting
8. 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 matplotlib.pyplot as plt
import requests
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 Tesla 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 Tesla 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

tsla = get_historical_data('TSLA', '2020-01-01')
tsla

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 Tesla from the starting of 2020 and stored it into the ‘tsla’ variable.


Step-3: SuperTrend Calculation


In this step, we are going to calculate the values of the SuperTrend indicator by following the methods we discussed before.


Python Implementation:



def get_supertrend(high, low, close, lookback, multiplier):
    
    # ATR
    
    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.ewm(lookback).mean()
    
    # H/L AVG AND BASIC UPPER & LOWER BAND
    
    hl_avg = (high + low) / 2
    upper_band = (hl_avg + multiplier * atr).dropna()
    lower_band = (hl_avg - multiplier * atr).dropna()
    
    # FINAL UPPER BAND
    final_bands = pd.DataFrame(columns = ['upper', 'lower'])
    final_bands.iloc[:,0] = [x for x in upper_band - upper_band]
    final_bands.iloc[:,1] = final_bands.iloc[:,0]
    for i in range(len(final_bands)):
        if i == 0:
            final_bands.iloc[i,0] = 0
        else:
            if (upper_band[i] < final_bands.iloc[i-1,0]) | (close[i-1] > final_bands.iloc[i-1,0]):
                final_bands.iloc[i,0] = upper_band[i]
            else:
                final_bands.iloc[i,0] = final_bands.iloc[i-1,0]
    
    # FINAL LOWER BAND
    
    for i in range(len(final_bands)):
        if i == 0:
            final_bands.iloc[i, 1] = 0
        else:
            if (lower_band[i] > final_bands.iloc[i-1,1]) | (close[i-1] < final_bands.iloc[i-1,1]):
                final_bands.iloc[i,1] = lower_band[i]
            else:
                final_bands.iloc[i,1] = final_bands.iloc[i-1,1]
    
    # SUPERTREND
    
    supertrend = pd.DataFrame(columns = [f'supertrend_{lookback}'])
    supertrend.iloc[:,0] = [x for x in final_bands['upper'] - final_bands['upper']]
    
    for i in range(len(supertrend)):
        if i == 0:
            supertrend.iloc[i, 0] = 0
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 0] and close[i] < final_bands.iloc[i, 0]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 0]
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 0] and close[i] > final_bands.iloc[i, 0]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 1]
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 1] and close[i] > final_bands.iloc[i, 1]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 1]
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 1] and close[i] < final_bands.iloc[i, 1]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 0]
    
    supertrend = supertrend.set_index(upper_band.index)
    supertrend = supertrend.dropna()[1:]
    
    # ST UPTREND/DOWNTREND
    
    upt = []
    dt = []
    close = close.iloc[len(close) - len(supertrend):]

    for i in range(len(supertrend)):
        if close[i] > supertrend.iloc[i, 0]:
            upt.append(supertrend.iloc[i, 0])
            dt.append(np.nan)
        elif close[i] < supertrend.iloc[i, 0]:
            upt.append(np.nan)
            dt.append(supertrend.iloc[i, 0])
        else:
            upt.append(np.nan)
            dt.append(np.nan)
            
    st, upt, dt = pd.Series(supertrend.iloc[:, 0]), pd.Series(upt), pd.Series(dt)
    upt.index, dt.index = supertrend.index, supertrend.index
    
    return st, upt, dt

Output:



Code Explanation: We are first defining a function named ‘get_supertrend’ which takes a stock’s high (‘high’), low (‘low’), close (‘close’), the lookback period (‘lookback), and the multiplier (‘multiplier’) as parameters. The code inside the function can be divided into six parts: ATR calculation, HLA and basic bands calculation, final upper band calculation, final lower band calculation, SuperTrend indicator calculation, and determining the uptrend and downtrend of the indicator.


ATR calculation: To determine the readings of the Average True Range, we are first calculating the three differences and stored them into their respective variables. Then we are combining all three differences into one dataframe using the ‘concat’ function and took the maximum values out of the three collective differences to determine the True Range. Then, using the ‘ewm’ and ‘mean’ function, we are taking the Exponential Moving Average of True Range for a specified number of periods to get the ATR values. Many prefer using SMA for ATR calculation while determining the SuperTrend but I used EMA for more accuracy.


HLA and Basic Bands calculation: Before calculating the basic upper and lower bands, we need to first determine the High Low Average. To calculate the values of HLA, we are first finding the total of the high and low values of a stock, then dividing the total by 2. Using the HLA values which are stored into the ‘hl_avg’ variable, we are determining both the upper and lower bands by following the formula we discussed and stored the values into the ‘upper_band’ and ‘lower_band’ respectively.


Final Upper Band calculation: Before directly moving into calculating the final upper band values, we are first creating a dataframe named ‘final_bands’ to store both the final upper and lower bands. Then just for the sake to fill the dataframe with values, we are subtracting the upper band by itself to store zeros matching the length of the upper band series so that it can be used for iterations. This step of creating the dataframe is optional but highly recommended since it reduces future works of data processing and all similar stuff. After creating the dataframe, we are passing a for-loop to create the conditions we discussed before for determining the values of the final upper band.


Final Lower Band calculation: The code structure for determining the final lower band is most similar to the final upper band calculation but only the conditions change. But now, let’s dive into exploring what’s happening inside the for-loop we are passing to get the values of the final lower band. First, we are passing a for-loop that iterates over the length of the ‘final_bands’ dataframe we created before. Inside the for-loop, we are first defining an if-statement that appends the final lower band’s value as 0 if the current iteration value is zero. This if-statement’s purpose is to fill the first value of the final lower band as zero. After the if-statement, we are defining nested else-statement which appends the final lower band’s value as the basic lower band value if the condition we discussed before gets satisfied, or else, it appends the previous final lower band value.


SuperTrend calculation: We are first creating a dataframe named ‘supertrend’ to store the values of the SuperTrend indicator. Like how we did before just to fill the dataframe with zeros to match the length of the basic bands series, we are doing the same here too to match the final bands series. Then comes the for-loop to determine the values of the SuperTrend indicator which appends the values concerning which condition gets satisfied out of the four. After the for-loop, we are doing some data processing to modify a bit and clean the dataframe.


Uptrend/Downtrend determination: This step is optional as I did only to make the visualization of the indicator a bit easy but you can try doing this too. The main aim of this step is to classify the SuperTrend indicator’s periods into two categories: the SuperTrend being below the closing price (uptrend), the SuperTrend being above the closing price (downtrend). With that being said, let’s dive into the methods involved. We are first defining two empty lists named ‘up’ and ‘down’ in which the values of the uptrend and the downtrend will be appended respectively. We are also reducing the length of the closing price data to match that of the SuperTrend data, only then, the iteration would be possible. Next, we are passing a for-loop to iterate over the length of the SuperTrend data to determine and append both uptrend and downtrend values into their respective variables. The values will be appended concerning which condition gets satisfied that are defined inside the for-loop. If either condition gets satisfied, the values of both uptrend and downtrend will be appended as ‘NaN’ (not defined). Then, we are converting the lists we created to store the uptrend and downtrend into Pandas series as it will be more convenient to work with.


After calculating all these values, we are finally returning the SuperTrend indicator values, and both the uptrend and downtrend readings. Then, we are calling the created function to store the SuperTrend indicator values of Tesla along with the uptrend and downtrend readings with 10 as the lookback period and 3 as the multiplier. From the output being shown, it is observable that whenever the closing price is greater than the SuperTrend indicator, the downtrend readings (‘st_dt’) represent ‘NaN’. Likewise, whenever the closing price is lesser than the SuperTrend indicator, the uptrend readings (‘st_upt’) represent ‘NaN’.


Step-4: Creating the trading strategy


In this step, we are going to implement the discussed SuperTrend indicator crossover trading strategy in python.


Python Implementation:



def implement_st_strategy(prices, st):
    buy_price = []
    sell_price = []
    st_signal = []
    signal = 0
    
    for i in range(len(st)):
        if st[i-1] > prices[i-1] and st[i] < prices[i]:
            if signal != 1:
                buy_price.append(prices[i])
                sell_price.append(np.nan)
                signal = 1
                st_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                st_signal.append(0)
        elif st[i-1] < prices[i-1] and st[i] > prices[i]:
            if signal != -1:
                buy_price.append(np.nan)
                sell_price.append(prices[i])
                signal = -1
                st_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                st_signal.append(0)
        else:
            buy_price.append(np.nan)
            sell_price.append(np.nan)
            st_signal.append(0)
            
    return buy_price, sell_price, st_signal

buy_price, sell_price, st_signal = implement_st_strategy(tsla['close'], tsla['st'])

Code Explanation: First, we are defining a function named ‘implement_wr_strategy’ which takes the stock prices (‘prices’), and the values of the SuperTrend indicator (‘st’) as parameters.

Inside the function, we are creating three empty lists (buy_price, sell_price, and st_signal) in which the values will be appended while creating the trading strategy.


After that, we are implementing the trading strategy through a for-loop. Inside the for-loop, we are passing certain conditions, and if the conditions are satisfied, the respective values will be appended to the empty lists. If the condition to buy the stock gets satisfied, the buying price will be appended to the ‘buy_price’ list, and the signal value will be appended as 1 representing to buy the stock. Similarly, if the condition to sell the stock gets satisfied, the selling price will be appended to the ‘sell_price’ list, and the signal value will be appended as -1 representing to sell the stock.


Finally, we are returning the lists appended with values. Then, we are calling the created function and stored the values into their respective variables. The list doesn’t make any sense unless we plot the values. So, let’s plot the values of the created trading lists.


Step-5: Plotting the trading signals


In this step, we are going to plot the created trading lists to make sense out of them.


Python Implementation:



plt.plot(tsla['close'], linewidth = 2)
plt.plot(tsla['st'], color = 'green', linewidth = 2, label = 'ST UPTREND')
plt.plot(tsla['st_dt'], color = 'r', linewidth = 2, label = 'ST DOWNTREND')
plt.plot(tsla.index, buy_price, marker = '^', color = 'green', markersize = 12, linewidth = 0, label = 'BUY SIGNAL')
plt.plot(tsla.index, sell_price, marker = 'v', color = 'r', markersize = 12, linewidth = 0, label = 'SELL SIGNAL')
plt.title('TSLA ST TRADING SIGNALS')
plt.legend(loc = 'upper left')
plt.show()

Output:



Code Explanation: We are plotting the readings of the SuperTrend indicator along with the buy and sell signals generated by the trading strategy. We can observe that whenever the SuperTrend indicator line crosses from above to below the closing price line and, a green-colored buy signal is plotted in the chart. Similarly, whenever the SuperTrend indicator line crosses from below to above the closing price line, a red-colored sell signal is plotted in the chart.


Step-6: Creating our Position


In this step, we are going to create a list that indicates 1 if we hold the stock or 0 if we don’t own or hold the stock.


Python Implementation:



position = []
for i in range(len(st_signal)):
    if st_signal[i] > 1:
        position.append(0)
    else:
        position.append(1)
        
for i in range(len(tsla['close'])):
    if st_signal[i] == 1:
        position[i] = 1
    elif st_signal[i] == -1:
        position[i] = 0
    else:
        position[i] = position[i-1]
        
close_price = tsla['close']
st = tsla['st']
st_signal = pd.DataFrame(st_signal).rename(columns = {0:'st_signal'}).set_index(tsla.index)
position = pd.DataFrame(position).rename(columns = {0:'st_position'}).set_index(tsla.index)

frames = [close_price, st, st_signal, position]
strategy = pd.concat(frames, join = 'inner', axis = 1)

strategy

Output:



Code Explanation: First, we are creating an empty list named ‘position’. We are passing two for-loops, one is to generate values for the ‘position’ list to just match the length of the ‘signal’ list. The other for-loop is the one we are using to generate actual position values. Inside the second for-loop, we are iterating over the values of the ‘signal’ list, and the values of the ‘position’ list get appended concerning which condition gets satisfied. The value of the position remains 1 if we hold the stock or remains 0 if we sold or don’t own the stock. Finally, we are doing some data manipulations to combine all the created lists into one dataframe.


From the output being shown, we can see that in the first two rows our position in the stock has remained 1 (since there isn’t any change in the SuperTrend indicator signal) but our position suddenly turned to -1 as we sold the stock when the SuperTrend indicator trading signal represents a sell signal (-1). Our position will remain 0 until some changes in the trading signal occur. Now it’s time to do implement some backtesting process!


Step-7: Backtesting


Before moving on, it is essential to know what backtesting is. Backtesting is the process of seeing how well our trading strategy has performed on the given stock data. In our case, we are going to implement a backtesting process for our SuperTrend indicator trading strategy over the Tesla stock data.


Python Implementation:



tsla_ret = pd.DataFrame(np.diff(tsla['close'])).rename(columns = {0:'returns'})
st_strategy_ret = []

for i in range(len(tsla_ret)):
    returns = tsla_ret['returns'][i]*strategy['st_position'][i]
    st_strategy_ret.append(returns)
    
st_strategy_ret_df = pd.DataFrame(st_strategy_ret).rename(columns = {0:'st_returns'})
investment_value = 100000
number_of_stocks = floor(investment_value/tsla['close'][-1])
st_investment_ret = []

for i in range(len(st_strategy_ret_df['st_returns'])):
    returns = number_of_stocks*st_strategy_ret_df['st_returns'][i]
    st_investment_ret.append(returns)

st_investment_ret_df = pd.DataFrame(st_investment_ret).rename(columns = {0:'investment_returns'})
total_investment_ret = round(sum(st_investment_ret_df['investment_returns']), 2)
profit_percentage = floor((total_investment_ret/investment_value)*100)
print(cl('Profit gained from the ST strategy by investing $100k in TSLA : {}'.format(total_investment_ret), attrs = ['bold']))
print(cl('Profit percentage of the ST strategy : {}%'.format(profit_percentage), attrs = ['bold']))

Output:



Profit gained from the ST strategy by investing $100k in TSLA : 55764.2
Profit percentage of the ST strategy : 55%

Code Explanation: First, we are calculating the returns of the Tesla stock using the ‘diff’ function provided by the NumPy package and we have stored it as a dataframe into the ‘tsla_ret’ variable. Next, we are passing a for-loop to iterate over the values of the ‘tsla_ret’ variable to calculate the returns we gained from our SuperTrend indicator trading strategy, and these returns values are appended to the ‘st_strategy_ret’ list. Next, we are converting the ‘st_strategy_ret’ list into a dataframe and stored it into the ‘st_strategy_ret_df’ variable.


Next comes the backtesting process. We are going to backtest our strategy by investing a hundred thousand USD into our trading strategy. So first, we are storing the amount of investment into the ‘investment_value’ variable. After that, we are calculating the number of Tesla stocks we can buy using the investment amount. You can notice that I’ve used the ‘floor’ function provided by the Math package because, while dividing the investment amount by the closing price of Tesla stock, it spits out an output with decimal numbers. The number of stocks should be an integer but not a decimal number. Using the ‘floor’ function, we can cut out the decimals. Remember that the ‘floor’ function is way more complex than the ‘round’ function. Then, we are passing a for-loop to find the investment returns followed by some data manipulation tasks.


Finally, we are printing the total return we got by investing a hundred thousand into our trading strategy and it is revealed that we have made an approximate profit of fifty-five thousand USD in one year. That’s not bad! Now, let’s compare our returns with SPY ETF (an ETF designed to track the S&P 500 stock market index) returns.


Step-8: SPY ETF Comparison


This step is optional but it is highly recommended as we can get an idea of how well our trading strategy performs against a benchmark (SPY ETF). In this step, we are going to extract the data of the SPY ETF using the ‘get_historical_data’ function we created and compare the returns we get from the SPY ETF with our SuperTrend indicator trading strategy returns on Tesla.


Python Implementation:



def get_benchmark(start_date, investment_value):
    spy = get_historical_data('SPY', start_date)['close']
    benchmark = pd.DataFrame(np.diff(spy)).rename(columns = {0:'benchmark_returns'})
    
    investment_value = investment_value
    number_of_stocks = floor(investment_value/spy[-1])
    benchmark_investment_ret = []
    
    for i in range(len(benchmark['benchmark_returns'])):
        returns = number_of_stocks*benchmark['benchmark_returns'][i]
        benchmark_investment_ret.append(returns)

    benchmark_investment_ret_df = pd.DataFrame(benchmark_investment_ret).rename(columns = {0:'investment_returns'})
    return benchmark_investment_ret_df

benchmark = get_benchmark('2020-01-01', 100000)
investment_value = 100000
total_benchmark_investment_ret = round(sum(benchmark['investment_returns']), 2)
benchmark_profit_percentage = floor((total_benchmark_investment_ret/investment_value)*100)
print(cl('Benchmark profit by investing $100k : {}'.format(total_benchmark_investment_ret), attrs = ['bold']))
print(cl('Benchmark Profit percentage : {}%'.format(benchmark_profit_percentage), attrs = ['bold']))
print(cl('ST Strategy profit is {}% higher than the Benchmark Profit'.format(profit_percentage - benchmark_profit_percentage), attrs = ['bold']))

Output:



Benchmark profit by investing $100k : 22445.78
Benchmark Profit percentage : 22%
ST Strategy profit is 33% higher than the Benchmark Profit

Code Explanation: The code used in this step is almost similar to the one used in the previous backtesting step but, instead of investing in Tesla, we are investing in SPY ETF by not implementing any trading strategies. From the output, we can see that our SuperTrend indicator trading strategy has outperformed the SPY ETF by 33%. That’s great!


Final Thoughts!


After a long process of crushing both theory and coding parts, we have successfully learned what the SuperTrend indicator is all about, its calculation, and how to build a simple crossover trading strategy based on it in python. One important thing we managed to accomplish in this article is understanding the complex math behind the indicator and this will help in increasing your understanding of the indicator significantly.


As the SuperTrend indicator has just started gaining momentum, there are a lot more spaces for improvement. One such important space is strategy optimization. I talk about this in almost every article of mine as it should be considered of paramount importance.


For those who don’t know what strategy optimization is, it is the process of tuning the trading algorithm to perform at its best. You can tune the SuperTrend indicator by experimenting with different settings. In this article, we built the indicator with the traditional setting of 14 as the lookback period and 3 as the multiplier but, you can try changing the values and run backtests for each and every change to acknowledge its performance. By doing this will help you reach the optimal setting for the indicator that can outperform the market itself.


We didn't consider doing it in this article as the sole purpose is not on building an optimistic trading strategy but on building a strong intuition on what the SuperTrend indicator is all about. But, it is highly recommended to get your hands dirty on experimenting with the indicator and introduce yourselves to a whole new level of possibilities. With that being said, you’ve reached the end of the article. If you forgot to follow any of the coding parts, don’t worry. I’ve provided the full source code at the end of the article. Hope you learned something useful from this article. Happy programming!


Full code:



# IMPORTING PACKAGES

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

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

# EXTRACTING DATA

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

tsla = get_historical_data('TSLA', '2020-01-01')
print(tsla)
# SUPERTREND CALCULATION
def get_supertrend(high, low, close, lookback, multiplier):
    
    # ATR
    
    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.ewm(lookback).mean()
    
    # H/L AVG AND BASIC UPPER & LOWER BAND
    
    hl_avg = (high + low) / 2
    upper_band = (hl_avg + multiplier * atr).dropna()
    lower_band = (hl_avg - multiplier * atr).dropna()
    
    # FINAL UPPER BAND
    
    final_bands = pd.DataFrame(columns = ['upper', 'lower'])
    final_bands.iloc[:,0] = [x for x in upper_band - upper_band]
    final_bands.iloc[:,1] = final_bands.iloc[:,0]
    
    for i in range(len(final_bands)):
        if i == 0:
            final_bands.iloc[i,0] = 0
        else:
            if (upper_band[i] < final_bands.iloc[i-1,0]) | (close[i-1] > final_bands.iloc[i-1,0]):
                final_bands.iloc[i,0] = upper_band[i]
            else:
                final_bands.iloc[i,0] = final_bands.iloc[i-1,0]
    
    # FINAL LOWER BAND
    
    for i in range(len(final_bands)):
        if i == 0:
            final_bands.iloc[i, 1] = 0
        else:
            if (lower_band[i] > final_bands.iloc[i-1,1]) | (close[i-1] < final_bands.iloc[i-1,1]):
                final_bands.iloc[i,1] = lower_band[i]
            else:
                final_bands.iloc[i,1] = final_bands.iloc[i-1,1]
    
    # SUPERTREND
    
    supertrend = pd.DataFrame(columns = [f'supertrend_{lookback}'])
    supertrend.iloc[:,0] = [x for x in final_bands['upper'] - final_bands['upper']]
    
    for i in range(len(supertrend)):
        if i == 0:
            supertrend.iloc[i, 0] = 0
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 0] and close[i] < final_bands.iloc[i, 0]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 0]
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 0] and close[i] > final_bands.iloc[i, 0]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 1]
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 1] and close[i] > final_bands.iloc[i, 1]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 1]
        elif supertrend.iloc[i-1, 0] == final_bands.iloc[i-1, 1] and close[i] < final_bands.iloc[i, 1]:
            supertrend.iloc[i, 0] = final_bands.iloc[i, 0]
    
    supertrend = supertrend.set_index(upper_band.index)
    supertrend = supertrend.dropna()[1:]
    
    # ST UPTREND/DOWNTREND
    
    upt = []
    dt = []
    close = close.iloc[len(close) - len(supertrend):]

    for i in range(len(supertrend)):
        if close[i] > supertrend.iloc[i, 0]:
            upt.append(supertrend.iloc[i, 0])
            dt.append(np.nan)
        elif close[i] < supertrend.iloc[i, 0]:
            upt.append(np.nan)
            dt.append(supertrend.iloc[i, 0])
        else:
            upt.append(np.nan)
            dt.append(np.nan)
            
    st, upt, dt = pd.Series(supertrend.iloc[:, 0]), pd.Series(upt), pd.Series(dt)
    upt.index, dt.index = supertrend.index, supertrend.index
    
    return st, upt, dt

tsla['st'], tsla['s_upt'], tsla['st_dt'] = get_supertrend(tsla['high'], tsla['low'], tsla['close'], 10, 3)
tsla = tsla[1:]
print(tsla.head())

# SUPERTREND PLOT

plt.plot(tsla['close'], linewidth = 2, label = 'CLOSING PRICE')
plt.plot(tsla['st'], color = 'green', linewidth = 2, label = 'ST UPTREND 10,3')
plt.plot(tsla['st_dt'], color = 'r', linewidth = 2, label = 'ST DOWNTREND 10,3')
plt.legend(loc = 'upper left')
plt.show()

# SUPERTREND STRATEGY

def implement_st_strategy(prices, st):
    buy_price = []
    sell_price = []
    st_signal = []
    signal = 0
    
    for i in range(len(st)):
        if st[i-1] > prices[i-1] and st[i] < prices[i]:
            if signal != 1:
                buy_price.append(prices[i])
                sell_price.append(np.nan)
                signal = 1
                st_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                st_signal.append(0)
        elif st[i-1] < prices[i-1] and st[i] > prices[i]:
            if signal != -1:
                buy_price.append(np.nan)
                sell_price.append(prices[i])
                signal = -1
                st_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                st_signal.append(0)
        else:
            buy_price.append(np.nan)
            sell_price.append(np.nan)
            st_signal.append(0)
            
    return buy_price, sell_price, st_signal

buy_price, sell_price, st_signal = implement_st_strategy(tsla['close'], tsla['st'])

# SUPERTREND SIGNALS

plt.plot(tsla['close'], linewidth = 2)
plt.plot(tsla['st'], color = 'green', linewidth = 2, label = 'ST UPTREND')
plt.plot(tsla['st_dt'], color = 'r', linewidth = 2, label = 'ST DOWNTREND')
plt.plot(tsla.index, buy_price, marker = '^', color = 'green', markersize = 12, linewidth = 0, label = 'BUY SIGNAL')
plt.plot(tsla.index, sell_price, marker = 'v', color = 'r', markersize = 12, linewidth = 0, label = 'SELL SIGNAL')
plt.title('TSLA ST TRADING SIGNALS')
plt.legend(loc = 'upper left')
plt.show()
# GENERATING STOCK POSITION
position = []
for i in range(len(st_signal)):
    if st_signal[i] > 1:
        position.append(0)
    else:
        position.append(1)
        
for i in range(len(tsla['close'])):
    if st_signal[i] == 1:
        position[i] = 1
    elif st_signal[i] == -1:
        position[i] = 0
    else:
        position[i] = position[i-1]
        
close_price = tsla['close']
st = tsla['st']
st_signal = pd.DataFrame(st_signal).rename(columns = {0:'st_signal'}).set_index(tsla.index)
position = pd.DataFrame(position).rename(columns = {0:'st_position'}).set_index(tsla.index)

frames = [close_price, st, st_signal, position]
strategy = pd.concat(frames, join = 'inner', axis = 1)

strategy.head()
print(strategy[20:25])
# BACKTESTING
tsla_ret = pd.DataFrame(np.diff(tsla['close'])).rename(columns = {0:'returns'})
st_strategy_ret = []

for i in range(len(tsla_ret)):
    returns = tsla_ret['returns'][i]*strategy['st_position'][i]
    st_strategy_ret.append(returns)
    
st_strategy_ret_df = pd.DataFrame(st_strategy_ret).rename(columns = {0:'st_returns'})
investment_value = 100000
number_of_stocks = floor(investment_value/tsla['close'][-1])
st_investment_ret = []

for i in range(len(st_strategy_ret_df['st_returns'])):
    returns = number_of_stocks*st_strategy_ret_df['st_returns'][i]
    st_investment_ret.append(returns)

st_investment_ret_df = pd.DataFrame(st_investment_ret).rename(columns = {0:'investment_returns'})
total_investment_ret = round(sum(st_investment_ret_df['investment_returns']), 2)
profit_percentage = floor((total_investment_ret/investment_value)*100)
print(cl('Profit gained from the ST strategy by investing $100k in TSLA : {}'.format(total_investment_ret), attrs = ['bold']))
print(cl('Profit percentage of the ST strategy : {}%'.format(profit_percentage), attrs = ['bold']))
# SPY ETF COMPARISON
def get_benchmark(start_date, investment_value):
    spy = get_historical_data('SPY', start_date)['close']
    benchmark = pd.DataFrame(np.diff(spy)).rename(columns = {0:'benchmark_returns'})
    
    investment_value = investment_value
    number_of_stocks = floor(investment_value/spy[-1])
    benchmark_investment_ret = []
    
    for i in range(len(benchmark['benchmark_returns'])):
        returns = number_of_stocks*benchmark['benchmark_returns'][i]
        benchmark_investment_ret.append(returns)

    benchmark_investment_ret_df = pd.DataFrame(benchmark_investment_ret).rename(columns = {0:'investment_returns'})
    return benchmark_investment_ret_df

benchmark = get_benchmark('2020-01-01', 100000)
investment_value = 100000
total_benchmark_investment_ret = round(sum(benchmark['investment_returns']), 2)
benchmark_profit_percentage = floor((total_benchmark_investment_ret/investment_value)*100)
print(cl('Benchmark profit by investing $100k : {}'.format(total_benchmark_investment_ret), attrs = ['bold']))
print(cl('Benchmark Profit percentage : {}%'.format(benchmark_profit_percentage), attrs = ['bold']))
print(cl('ST Strategy profit is {}% higher than the Benchmark Profit'.format(profit_percentage - benchmark_profit_percentage), attrs = ['bold']))

 

2 comments
bottom of page