Algorithmic Trading with the Disparity Index in Python

Coding and Backtesting a powerful indicator in Python



There are an extensive amount of technical indicators out there for trading purposes but traders being so picky will end up choosing only a handful of them and the indicator we are going to discuss today definitely adds to this list. Behold, the Disparity Index. In this article, we will first discuss what the Disparity Index is all about, the math behind the indicator, and then, we will move on to the coding part where we will use Python to first build the indicator from scratch, construct a simple trading strategy based on it, backtest the strategy on Google stocks and compare the returns to those of the SPY ETF (an ETF designed to track the movements of the S&P 500 market index).


Disparity Index


The Disparity Index is a momentum indicator that measures the distance between the current closing price of a stock and its moving average for a specified number of periods and interprets the readings in the form of percentages. Unlike other momentum oscillators, the Disparity Index does not bound between certain levels and hence an unbounded oscillator.


Traders often use the Disparity Index to determine the current momentum of a market. Upward momentum in the market can be observed if the readings of the Disparity Index are above zero, and similarly, the market is considered to in a downward momentum if the readings of the indicator are below zero.


The calculation of the Disparity Index is pretty much straightforward. First, we have to find the difference between the closing of the price of a stock and the moving average for a specified number of periods and divide the difference by the moving average, then multiply it by 100. The calculation of the Disparity Index with a typical setting of 14 as the lookback period can be represented as follows:



DI 14 = [ C.PRICE - MOVING  AVG 14 ] / [ MOVING AVG 14 ] * 100

where,
DI 14 = 14-day Disparity Index
MOVING AVG 14 = 14-day Moving Average
C.PRICE = Closing price of the stock


That’s the whole process of calculating the readings of the Disparity Index. Now, let’s analyze a chart where Google’s closing price data is plotted along with its 14-day Disparity Index.



The above chart is separated into two panels: the upper panel with the closing prices of Google and the lower panel with the readings of the 14-day Disparity Index. From the above chart, we could see that whenever the readings of the Disparity Index is above the zero-line, a green-colored histogram is plotted representing a positive or an upward momentum in the market, and similarly, whenever the Disparity Index goes below the zero-line, a red-colored histogram is plotted representing a negative or downward momentum. This is one way of using the Disparity Index. The other way traders use this indicator is to detect ranging markets (markets where the prices oscillate back and forth between certain limits showing neither positive nor negative price movements). Sometimes, it can be seen that the bars of the Disparity Index go back and forth on either side of the zero-line indicating the market is ranging or consolidating. This feature of the indicator comes in handy to traders while making trades (for me personally).


To my knowledge, there two trading strategies applied based on the Disparity Index. The first one is the Breakout strategy where traders assume two extreme levels plotted on either side of the plot, and this strategy reveals a buy signal whenever the Disparity Index goes below the lower level, similarly, a sell signal is generated whenever the Disparity Index touches above the upper level. These thresholds vary from one asset to another since the Disparity Index is an unbounded oscillator. The second one is the Zero-line cross strategy which reveals a buy signal whenever the Disparity Index goes from below to above the zero-line, and a sell signal is generated whenever the Disparity Index goes from above to below the zero-line.


In this article, we are going to implement the second strategy which is the Zero-line cross strategy but since the Disparity Index is prone to revealing a lot of false signals, we are going to tune the traditional crossover strategy. Our tuned strategy reveals a buy signal only if the past four readings are below the zero-line and the current reading is above the zero-line. Similarly, a sell is generated only if the past four readings are above the zero-line and the current reading is below the zero-line. By doing this would drastically reduce the number of false signals generated by the strategy and hence boosts its performance. Our tuned Zero-line crossover trading strategy can be represented as follows:



IF PREV.4 DIs < ZERO-LINE AND CURR.DI > ZERO-LINE ==> BUY SIGNAL
IF PREV.4 DIs > ZERO-LINE AND CURR.DI < ZERO-LINE ==> SELL SIGNAL


This concludes our theory part on Disparity Index. Now, let’s move on to the programming part where we are first going to build the indicator from scratch, build the tuned Zero-line crossover strategy which we just discussed, then, compare our strategy’s performance with that of SPY ETF 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. Disparity Index Calculation
4. Creating the Tuned Zero-line 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:



# IMPORTING PACKAGES

import numpy as np
import requests
import pandas as pd
import matplotlib.pyplot as plt
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 Google 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 Google 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:



# EXTRACTING STOCK 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

googl = get_historical_data('GOOGL', '2020-01-01')
googl.tail()


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


Step-3: Disparity Index Calculation


In this step, we are going to calculate the readings of the Disparity Index by following the formula we discussed before.


Python Implementation:



# DISPARITY INDEX CALCULATION

def get_di(data, lookback):
    ma = data.rolling(lookback).mean()
    di = ((data - ma) / ma) * 100
    return di

googl['di_14'] = get_di(googl['close'], 14)
googl = googl.dropna()
googl.tail()


Output:



Code Explanation: First, we are defining a function named ‘get_di’ that takes a stock’s closing price (‘data’) and the lookback period as parameters. Inside the function, we are first calculating the Moving Average of the closing price data for a specified number of lookback periods. Then, we are substituting the determined values into the Disparity Index formula to calculating the readings. Finally, we are returning and calling the created function to store the 14-day Disparity Index readings of Google.


Step-4: Creating the trading strategy


In this step, we are going to implement the discussed Disparity Index tuned Zero-line crossover trading strategy in python.


Python Implementation:



# DISPARITY INDEX STRATEGY

def implement_di_strategy(prices, di):
    buy_price = []
    sell_price = []
    di_signal = []
    signal = 0
    
    for i in range(len(prices)):
        if di[i-4] < 0 and di[i-3] < 0 and di[i-2] < 0 and di[i-1] < 0 and di[i] > 0:
            if signal != 1:
                buy_price.append(prices[i])
                sell_price.append(np.nan)
                signal = 1
                di_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                di_signal.append(0)
        elif di[i-4] > 0 and di[i-3] > 0 and di[i-2] > 0 and di[i-1] > 0 and di[i] < 0:
            if signal != -1:
                buy_price.append(np.nan)
                sell_price.append(prices[i])
                signal = -1
                di_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                di_signal.append(0)
        else:
            buy_price.append(np.nan)
            sell_price.append(np.nan)
            di_signal.append(0)
            
    return buy_price, sell_price, di_signal

buy_price, sell_price, di_signal = implement_di_strategy(googl['close'], googl['di_14'])


Code Explanation: First, we are defining a function named ‘implement_di_strategy’ which takes the stock prices (‘prices’), and the readings of the Disparity Index (‘di’) as parameters.

Inside the function, we are creating three empty lists (buy_price, sell_price, and di_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:



# DISPARITY INDEX TRADING SIGNALS PLOT

ax1 = plt.subplot2grid((11,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((11,1), (6,0), rowspan = 5, colspan = 1)
ax1.plot(googl['close'], linewidth = 2, color = '#1976d2')
ax1.plot(googl.index, buy_price, marker = '^', markersize = 12, linewidth = 0, label = 'BUY SIGNAL', color = 'green')
ax1.plot(googl.index, sell_price, marker = 'v', markersize = 12, linewidth = 0, label = 'SELL SIGNAL', color = 'r')
ax1.legend()
ax1.set_title('GOOGL CLOSING PRICES')
for i in range(len(googl)):
    if googl.iloc[i, 5] >= 0:
        ax2.bar(googl.iloc[i].name, googl.iloc[i, 5], color = '#26a69a')
    else:    
        ax2.bar(googl.iloc[i].name, googl.iloc[i, 5], color = '#ef5350')
ax2.set_title('GOOGL DISPARITY INDEX 14')
plt.show()


Output:



Code Explanation: We are plotting the readings of the Disparity Index along with the buy and sell signals generated by the tuned Zer-line crossover trading strategy. We can observe that whenever the previous four readings of the Disparity Index are below the zero-line and the current reading is above the zero-line, a green-colored buy signal is plotted in the chart. Similarly, whenever the previous four readings of the Disparity Index are above the zero-line and the current reading is below the zero-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:



# STOCK POSITION

position = []
for i in range(len(di_signal)):
    if di_signal[i] > 1:
        position.append(0)
    else:
        position.append(1)
        
for i in range(len(googl['close'])):
    if di_signal[i] == 1:
        position[i] = 1
    elif di_signal[i] == -1:
        position[i] = 0
    else:
        position[i] = position[i-1]
        
close_price = googl['close']
di = googl['di_14']
di_signal = pd.DataFrame(di_signal).rename(columns = {0:'di_signal'}).set_index(googl.index)
position = pd.DataFrame(position).rename(columns = {0:'di_position'}).set_index(googl.index)

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

strategy.head()


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 three rows our position in the stock has remained 1 (since there isn’t any change in the Disparity Index signal) but our position suddenly turned to -1 as we sold the stock when the Disparity Index 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 Disparity Index trading strategy over the Google stock data.


Python Implementation:



# BACKTESTING

googl_ret = pd.DataFrame(np.diff(googl['close'])).rename(columns = {0:'returns'})
di_strategy_ret = []

for i in range(len(googl_ret)):
    returns = googl_ret['returns'][i]*strategy['di_position'][i]
    di_strategy_ret.append(returns)
    
di_strategy_ret_df = pd.DataFrame(di_strategy_ret).rename(columns = {0:'di_returns'})
investment_value = 100000
number_of_stocks = floor(investment_value/googl['close'][0])
di_investment_ret = []

for i in range(len(di_strategy_ret_df['di_returns'])):
    returns = number_of_stocks*di_strategy_ret_df['di_returns'][i]
    di_investment_ret.append(returns)

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


Output:



Profit gained from the DI strategy by investing $100k in GOOGL : 39933.68
Profit percentage of the DI strategy : 39%


Code Explanation: First, we are calculating the returns of the Google stock using the ‘diff’ function provided by the NumPy package and we have stored it as a dataframe into the ‘googl_ret’ variable. Next, we are passing a for-loop to iterate over the values of the ‘googl_ret’ variable to calculate the returns we gained from our Disparity Index trading strategy, and these returns values are appended to the ‘di_strategy_ret’ list. Next, we are converting the ‘di_strategy_ret’ list into a dataframe and stored it into the ‘di_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 Google 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 Google 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 thirty-eight 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 will extract the SPY ETF data using the ‘get_historical_data’ function we created and compare the returns we get from the SPY ETF with our Disparity Index tuned zero-line crossover trading strategy returns on Google.


You might have observed that in all of my algorithmic trading articles, I’ve compared the strategy results not with the S&P 500 market index itself but with the SPY ETF and this is because most of the stock data providers (like Twelve Data) don’t provide the S&P 500 index data. So, I have no other choice than to go with the SPY ETF. If you’re fortunate to get the S&P 500 market index data, it is recommended to use it for comparison rather than any ETF.


Python Implementation:



# 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('DI Strategy profit is {}% higher than the Benchmark Profit'.format(profit_percentage - benchmark_profit_percentage), attrs = ['bold']))


Output:



Benchmark profit by investing $100k : 23026.52
Benchmark Profit percentage : 23%
DI Strategy profit is 16% 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 Google, we are investing in SPY ETF by not implementing any trading strategies. From the output, we can see that our Disparity Index tuned zero-line crossover trading strategy has outperformed the SPY ETF by 16%. That’s great!


Final Thoughts!


After an exhaustive process of crushing both theory and programming parts, we have successfully learned what the Disparity Index is all about, the mathematics behind it, and finally, how to build the indicator from scratch construct a tuned zero-line crossover trading strategy based on it with Python. Even though we managed to build a strategy that makes a profit, there are still a lot of spaces where this article can be improved and one such important space is constructing a more realistic strategy.


The procedures, steps, and code we implemented in this article can be suitable for virtual trading but the real-world market doesn't behave the same way and to thrive in such conditions, it is essential to build our strategy by considering more external factors like risks involved in each trade, commission or fees charged for each transaction, and most importantly market sentiment.


The process isn't finished even after building a realistic strategy but it is necessary to know whether the strategy is even a profitable one or just a waste of time. In order to determine the profitability and the performance of the strategy, one has to backtest and evaluate it with various metrics. And this would be the next space where the article can be improved.

If you’re done with these improvisations, you’re set to apply them in the real-world market and the certainty of you being profitable is high. 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. Hope you learned something new and useful from this article.


Full code:



# IMPORTING PACKAGES

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

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


# EXTRACTING STOCK 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

googl = get_historical_data('GOOGL', '2020-01-01')
print(googl.tail())


# DISPARITY INDEX CALCULATION

def get_di(data, lookback):
    ma = data.rolling(lookback).mean()
    di = ((data - ma) / ma) * 100
    return di

googl['di_14'] = get_di(googl['close'], 14)
googl = googl.dropna()
print(googl.tail())


# DISPARITY INDEX PLOT

ax1 = plt.subplot2grid((11,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((11,1), (6,0), rowspan = 5, colspan = 1)
ax1.plot(googl['close'], linewidth = 2, color = '#1976d2')
ax1.set_title('GOOGL CLOSING PRICES')
for i in range(len(googl)):
    if googl.iloc[i, 5] >= 0:
        ax2.bar(googl.iloc[i].name, googl.iloc[i, 5], color = '#26a69a')
    else:    
        ax2.bar(googl.iloc[i].name, googl.iloc[i, 5], color = '#ef5350')
ax2.set_title('GOOGL DISPARITY INDEX 14')
plt.show()


# DISPARITY INDEX STRATEGY

def implement_di_strategy(prices, di):
    buy_price = []
    sell_price = []
    di_signal = []
    signal = 0
    
    for i in range(len(prices)):
        if di[i-4] < 0 and di[i-3] < 0 and di[i-2] < 0 and di[i-1] < 0 and di[i] > 0:
            if signal != 1:
                buy_price.append(prices[i])
                sell_price.append(np.nan)
                signal = 1
                di_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                di_signal.append(0)
        elif di[i-4] > 0 and di[i-3] > 0 and di[i-2] > 0 and di[i-1] > 0 and di[i] < 0:
            if signal != -1:
                buy_price.append(np.nan)
                sell_price.append(prices[i])
                signal = -1
                di_signal.append(signal)
            else:
                buy_price.append(np.nan)
                sell_price.append(np.nan)
                di_signal.append(0)
        else:
            buy_price.append(np.nan)
            sell_price.append(np.nan)
            di_signal.append(0)
            
    return buy_price, sell_price, di_signal

buy_price, sell_price, di_signal = implement_di_strategy(googl['close'], googl['di_14'])


# DISPARITY INDEX TRADING SIGNALS PLOT

ax1 = plt.subplot2grid((11,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((11,1), (6,0), rowspan = 5, colspan = 1)
ax1.plot(googl['close'], linewidth = 2, color = '#1976d2')
ax1.plot(googl.index, buy_price, marker = '^', markersize = 12, linewidth = 0, label = 'BUY SIGNAL', color = 'green')
ax1.plot(googl.index, sell_price, marker = 'v', markersize = 12, linewidth = 0, label = 'SELL SIGNAL', color = 'r')
ax1.legend()
ax1.set_title('GOOGL CLOSING PRICES')
for i in range(len(googl)):
    if googl.iloc[i, 5] >= 0:
        ax2.bar(googl.iloc[i].name, googl.iloc[i, 5], color = '#26a69a')
    else:    
        ax2.bar(googl.iloc[i].name, googl.iloc[i, 5], color = '#ef5350')
ax2.set_title('GOOGL DISPARITY INDEX 14')
plt.show()


# STOCK POSITION

position = []
for i in range(len(di_signal)):
    if di_signal[i] > 1:
        position.append(0)
    else:
        position.append(1)
        
for i in range(len(googl['close'])):
    if di_signal[i] == 1:
        position[i] = 1
    elif di_signal[i] == -1:
        position[i] = 0
    else:
        position[i] = position[i-1]
        
close_price = googl['close']
di = googl['di_14']
di_signal = pd.DataFrame(di_signal).rename(columns = {0:'di_signal'}).set_index(googl.index)
position = pd.DataFrame(position).rename(columns = {0:'di_position'}).set_index(googl.index)

frames = [close_price, di, di_signal, position]
strategy = pd.concat(frames, join = 'inner'