Let’s find out the answer with Python
The use of technical indicators has never plunged over time but making a profit using those is uncertain due to one of its main drawbacks which are revealing false trading signals. This is much worse than you think because you might tend to enter the market at the wrong time by following these signals and face heavy losses. But this doesn't mean technical indicators are obsolete. We just have to change some ways on how we use it. One of the best measures to reduce the number of false signals is by adding another indicator to the trading strategy that is contrary in nature to the other indicator.
In this article, we are going to use two of the famous technical indicators which are the Average Directional Index (ADX), and the Relative Strength Index (RSI) to build a trading strategy and test it on real-world market data using Python to see whether it makes a substantial amount of profit or not. Without further ado, let’s dive into the article!
Average Directional Index (ADX)
ADX is a technical indicator that is widely used in measuring the strength of the market trend. Now, the ADX doesn’t measure the direction of the trend, whether it’s bullish or bearish, but just represents how strong the trend is. So, to identify the direction of the trend, ADX is combined with a Positive Directional Index (+DI) and a Negative Directional Index (-DI). As the name suggests, the +DI measures the bullish or positive trend of the market, similarly, the -DI measures the bearish or negative trend of the market. The values of all the components are bound between 0 to 100, hence acting as an oscillator. The traditional setting of ADX is 14 as the lookback period.
To calculate the values of ADX with 14 as the lookback period, first, the Positive (+DM) and Negative Directional Movement (-DM) is determined. The +DM is calculated by finding the difference between the current high and the previous high, and similarly, the -DM is calculated by finding the difference between the previous low and the current low. It can be represented as follows:
+DM = CURRENT HIGH - PREVIOUS HIGH
-DM = PREVIOUS LOW - CURRENT LOW
Then, an ATR with 14 as the lookback period is calculated. Now, using the calculated directional movement and the ATR values, the Positive Directional Index (+DI) and the Negative Directional Index (-DI) are calculated. To determine the values of +DI, the value received by taking the Exponential Moving Average (EMA) of the Positive Directional Movement (+DM) with 14 as the lookback period is divided by the previously calculated 14-day ATR and then, multiplied by 100. This same applies to determining the -DI too but instead of taking the 14-day EMA of +DM, the Negative Directional Movement (-DM) is taken into account. The formula to calculate both the +DI and the -DI can be represented as follows:
+DI 14 = 100 * [ EMA 14 ( +DM ) / ATR 14 ]
-DI 14 = 100 * [ EMA 14 ( -DM ) / ATR 14 ]
The next step is to use the + DI and -DI to calculate the Directional Index. It can be determined by dividing the absolute value of the difference between the +DI and -DI by the absolute value of the total of + DI and -DI multiped by 100. The formula to calculate the Directional Index can be represented as follows:
DI 14 = | (+DI 14) - (-DI 14) | / | (+DI 14) + (-DI 14) | * 100
The final step is to calculate the ADX itself by utilizing the determined Directional Index values. The ADX is calculated by multiplying the previous Directional Index value with 13 (lookback period -1) and adding it with the Directional Index, then multiplied by 100. The formula to calculate the values of ADX can be represented as follows:
ADX 14 = [ ( PREV DI 14 * 13 ) + DI 14 ] * 100
The ADX cannot be used as it is but needs to be smoothed with a customized moving average created by the founder of the indicator, Welles Wilder himself. That’s the whole process of calculating the values of ADX. Now, let’s analyze a chart where Apple’s stock prices are plotted along with its ADX 14 reading.
The above chart is divided into two panels: the upper panel with the closing prices of Apple and the lower panel with the components of ADX. Along with the components, a grey dashed line is plotted which is nothing but the threshold for the ADX plotted at a level of 35. As I said before, the ADX doesn’t track the direction of the trend but instead, the strength and it can be seen several times in the chart where the ADX line increases when the market shows a strong trend (either up or down) and decreases when the market bound to consolidate. This is the same case with both the directional index lines too. We could see that the +DI line increases when the market shows a sturdy uptrend and decreases during a downtrend and vice-versa for the -DI line.
ADX is not only used to quantify the strength of a market trend but also becomes a handy tool to identify ranging markets (markets where the stock moves back and forth between specific high and low levels showing zero momentum). Whenever the lines move closer to each other, the market is observed to be ranging, similarly, the wider the space between the lines, the more the markets are trending. Those who are introduced to the chart of ADX for the very first time might confuse themselves since the movement of each line is indirectly proportional to the movement of the market.
Relative Strength Index (RSI)
Before moving on, let’s first gain an understanding of what an Oscillator means in the stock trading space. An oscillator is a technical tool that constructs a trend-based indicator whose values are bound between a high and low band. Traders use these bands along with the constructed trend-based indicator to identify the market state and make potential buy and sell trades. Also, oscillators are widely used for short-term trading purposes but there are no restrictions in using them for long-term investments.
Founded and developed by J. Welles Wilder (the founder of ADX too) in 1978, the Relative Strength Index is a momentum oscillator that is used by traders to identify whether the market is in the state of overbought or oversold. Before moving on, let’s explore what overbought and oversold is. A market is considered to be in the state of overbought when an asset is constantly bought by traders moving it to an extremely bullish trend and bound to consolidate. Similarly, a market is considered to be in the state of oversold when an asset is constantly sold by traders moving it to a bearish trend and tends to bounce back.
Being an oscillator, the values of RSI are bound between 0 to 100. The traditional way to evaluate a market state using the Relative Strength Index is that an RSI reading of 70 or above reveals a state of overbought, and similarly, an RSI reading of 30 or below represents the market is in the state of oversold. These overbought and oversold can also be tuned concerning which stock or asset you choose. For example, some assets might have constant RSI readings of 80 and 20. So in that case, you can set the overbought and oversold levels to be 80 and 20 respectively. The standard setting of RSI is 14 as the lookback period.
RSI might sound more similar to Stochastic Oscillator in terms of value interpretation but the way it’s being calculated is quite different. There are three steps involved in the calculation of RSI.
Calculating the Exponential Moving Average (EMA) of the gain and loss of an asset: A word on Exponential Moving Average. EMA is a type of Moving Average (MA) that automatically allocates greater weighting (nothing but importance) to the most recent data point and lesser weighting to data points in the distant past. In this step, we will first calculate the returns of the asset and separate the gains from losses. Using these separated values, the two EMAs for a specified number of periods are calculated.
Calculating the Relative Strength of an asset: The Relative Strength of an asset is determined by dividing the Exponential Moving Average of the gain of an asset from the Exponential Moving Average of the loss of an asset for a specified number of periods. It can be mathematically represented as follows:
RS = GAIN EMA / LOSS EMA
where,
RS = Relative Strength
GAIN EMA = Exponential Moving Average of the gains
LOSS EMA = Exponential Moving Average of the losses
Calculating the RSI values: In this step, we will calculate the RSI itself by making use of the Relative Strength values we calculated in the previous step. To calculate the values of RSI of a given asset for a specified number of periods, there is a formula that we need to follow:
RSI = 100.0 - (100.0 / (1.0 + RS))
where,
RSI = Relative Strength Index
RS = Relative Strength
That’s the whole process of calculating the RSI. Like how we analyzed a chart of ADX to build a solid understanding of the indicator, we will do the same for RSI too.
The above chart is separated into two panels: The above panel with the closing price of Apple and the lower panel with the calculated RSI 14 values of Apple. While analyzing the panel plotted with the RSI values, it can be seen that the trend and movement of the calculated values follow the same as the closing price of Apple. So, we can consider that RSI is a directional indicator. Some indicators are non-directional meaning that their movement will be inversely proportional to the actual stock movement and this can sometimes confuse traders and be hard to understand too.
While observing the RSI chart, we could be able to see that the plot of RSI reveals trend reversals even before the market does. Simply speaking, the RSI shows a downtrend or an uptrend right before the actual market does. This shows that RSI is a leading indicator. A leading indicator is nothing but an indicator that takes into account the current value of a data series to predict future movements. RSI being a leading indicator helps in warning the traders about potential trend reversions before in time. The opposite of leading indicators is called lagging indicators. Lagging indicators are indicators that represent the current value by taking into account the historical values of a data series.
Trading Strategy
Now that we have built some basic intuitions on both the ADX and RSI indicator. Let’s discuss the trading strategy that we are going to implement in this article. The strategy is going to be quite unique. We go long (buy the stock) if the current ADX reading is above 35, the current RSI reading is below 50, and the current +DI is lesser than the current -DI. Similarly, we go short (sell the stock) if the current ADX reading is above 35, the current RSI reading is above 50, and the current +DI is greater than the current -DI. Our strategy is basically a combination of an inversed ADX conventional strategy and the RSI line crossover strategy with some minor tweaks. The strategy can be represented as follows:
ADX > 35 AND RSI < 50 AND +DI < -DI ==> BUY SIGNAL
ADX > 35 AND RSI > 50 AND +DI > -DI ==> SELL SIGNAL
That’s it! This concludes our theory part and let’s move on to the programming part where we will use Python to first build the indicators from scratch, construct the discussed trading strategy, backtest the strategy on Apple stock data, and finally compare the results with that of SPY ETF. 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. ADX Calculation
4. RSI Calculation
5. Creating the Trading Strategy
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 pandas as pd
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 Apple with Twelve Data’s API endpoint.
Step-2: Extracting Stock Data from Twelve Data
In this step, we are going to pull the historical stock data of Apple 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
aapl = get_historical_data('AAPL', '2010-01-01')
aapl.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 Apple from the starting of 2010 and stored it into the ‘aapl’ variable.
Step-3: ADX Calculation
In this step, we are going to calculate the values of ADX by following the method we discussed before.
Python Implementation:
# ADX CALCULATION
def get_adx(high, low, close, lookback):
plus_dm = high.diff()
minus_dm = low.diff()
plus_dm[plus_dm < 0] = 0
minus_dm[minus_dm > 0] = 0
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.rolling(lookback).mean()
plus_di = 100 * (plus_dm.ewm(alpha = 1/lookback).mean() / atr)
minus_di = abs(100 * (minus_dm.ewm(alpha = 1/lookback).mean() / atr))
dx = (abs(plus_di - minus_di) / abs(plus_di + minus_di)) * 100
adx = ((dx.shift(1) * (lookback - 1)) + dx) / lookback
adx_smooth = adx.ewm(alpha = 1/lookback).mean()
return plus_di, minus_di, adx_smooth
aapl['plus_di'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[0]).rename(columns = {0:'plus_di'})
aapl['minus_di'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[1]).rename(columns = {0:'minus_di'})
aapl['adx'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[2]).rename(columns = {0:'adx'})
aapl = aapl.dropna()
aapl.tail()
Output:
Code Explanation: We are first defining a function named ‘get_adx’ that takes a stock’s high (‘high’), low (‘low’), and close data (‘close’) along with the lookback period (‘lookback’) as parameters.
Inside the function, we are first calculating and storing the + DM and -DM into the ‘plus_dm’ and ‘minus_dm’ respectively. Then comes the ATR calculation where we are first calculating the three differences and defined a variable ‘tr’ to store the highest values among the determined differences, then, we calculated and stored the values of ATR into the ‘atr’ variable.
Using the calculated directional movements and ATR values, we are calculating the + DI and -DI and stored them into the ‘plus_di’ and ‘minus_di’ variables respectively. With the help of the previously discussed formula, we are calculating the Directional Index values and stored them into the ‘dx’ variable, and applied those values into the ADX formula to calculate the Average Directional Index values. Then, we defined a variable ‘adx_smooth’ to store the smoothed values of ADX. Finally, we are returning and calling the function to obtain the + DI, -DI, and ADX values of Apple with 14 as the lookback period.
Step-4: RSI Calculation
In this step, we are going to calculate the values of RSI with 14 as the lookback period using the RSI formula we discussed before.
Python Implementation:
# RSI CALCULATION
def get_rsi(close, lookback):
ret = close.diff()
up = []
down = []
for i in range(len(ret)):
if ret[i] < 0:
up.append(0)
down.append(ret[i])
else:
up.append(ret[i])
down.append(0)
up_series = pd.Series(up)
down_series = pd.Series(down).abs()
up_ewm = up_series.ewm(com = lookback - 1, adjust = False).mean()
down_ewm = down_series.ewm(com = lookback - 1, adjust = False).mean()
rs = up_ewm/down_ewm
rsi = 100 - (100 / (1 + rs))
rsi_df = pd.DataFrame(rsi).rename(columns = {0:'rsi'}).set_index(close.index)
rsi_df = rsi_df.dropna()
return rsi_df[3:]
aapl['rsi_14'] = get_rsi(aapl['close'], 14)
aapl = aapl.dropna()
aapl.tail()
Output:
Code Explanation: Firstly, we are defining a function named ‘get_rsi’ that takes the closing price of a stock (‘close’) and the lookback period (‘lookback’) as parameters. Inside the function, we are first calculating the returns of the stock using the ‘diff’ function provided by the Pandas package and stored it into the ‘ret’ variable. This function basically subtracts the current value from the previous value. Next, we are passing a for-loop on the ‘ret’ variable to distinguish gains from losses and append those values to the concerning variable (‘up’ or ‘down’).
Then, we are calculating the Exponential Moving Averages for both the ‘up’ and ‘down’ using the ‘ewm’ function provided by the Pandas package and stored them into the ‘up_ewm’ and ‘down_ewm’ variable respectively. Using these calculated EMAs, we are determining the Relative Strength by following the formula we discussed before and stored it into the ‘rs’ variable.
By making use of the calculated Relative Strength values, we are calculating the RSI values by following its formula. After doing some data processing and manipulations, we are returning the calculated Relative Strength Index values in the form of a Pandas dataframe. Finally, we are calling the created function to store the RSI values of Apple with 14 as the lookback period.
Step-5: Creating the Trading Strategy:
In this step, we are going to implement the discussed Average Directional Index and Relative Strength Index combined trading strategy in python.
Python Implementation:
# TRADING STRATEGY
def adx_rsi_strategy(prices, adx, pdi, ndi, rsi):
buy_price = []
sell_price = []
adx_rsi_signal = []
signal = 0
for i in range(len(prices)):
if adx[i] > 35 and pdi[i] < ndi[i] and rsi[i] < 50:
if signal != 1:
buy_price.append(prices[i])
sell_price.append(np.nan)
signal = 1
adx_rsi_signal.append(signal)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
adx_rsi_signal.append(0)
elif adx[i] > 35 and pdi[i] > ndi[i] and rsi[i] > 50:
if signal != -1:
buy_price.append(np.nan)
sell_price.append(prices[i])
signal = -1
adx_rsi_signal.append(signal)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
adx_rsi_signal.append(0)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
adx_rsi_signal.append(0)
return buy_price, sell_price, adx_rsi_signal
buy_price, sell_price, adx_rsi_signal = adx_rsi_strategy(aapl['close'], aapl['adx'], aapl['plus_di'], aapl['minus_di'], aapl['rsi_14'])
Code Explanation: First, we are defining a function named ‘adx_rsi_strategy’ which takes the stock prices (‘prices’), ADX readings (‘adx’), +DI readings (‘pdi’), -DI readings (‘ndi’), and the Relative Strength Index readings (‘rsi’) as parameters.
Inside the function, we are creating three empty lists (buy_price, sell_price, and adx_rsi_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.
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
position = []
for i in range(len(adx_rsi_signal)):
if adx_rsi_signal[i] > 1:
position.append(0)
else:
position.append(1)
for i in range(len(aapl['close'])):
if adx_rsi_signal[i] == 1:
position[i] = 1
elif adx_rsi_signal[i] == -1:
position[i] = 0
else:
position[i] = position[i-1]
adx = aapl['adx']
pdi = aapl['plus_di']
ndi = aapl['minus_di']
rsi = aapl['rsi_14']
close_price = aapl['close']
adx_rsi_signal = pd.DataFrame(adx_rsi_signal).rename(columns = {0:'adx_rsi_signal'}).set_index(aapl.index)
position = pd.DataFrame(position).rename(columns = {0:'adx_rsi_position'}).set_index(aapl.index)
frames = [close_price, adx, pdi, ndi, rsi, adx_rsi_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 four rows our position in the stock has remained 1 (since there isn’t any change in the trading signal) but our position suddenly turned to 0 as we sold the stock when the trading signal represents a buy signal (-1). Our position will remain -1 until some changes in the trading signal occur. Now it’s time to do implement some backtesting processes!
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 ADX and RSI combined trading strategy over the Apple stock data.
Python Implementation:
# BACKTESTING
aapl_ret = pd.DataFrame(np.diff(aapl['close'])).rename(columns = {0:'returns'})
adx_rsi_strategy_ret = []
for i in range(len(aapl_ret)):
returns = aapl_ret['returns'][i]*strategy['adx_rsi_position'][i]
adx_rsi_strategy_ret.append(returns)
adx_rsi_strategy_ret_df = pd.DataFrame(adx_rsi_strategy_ret).rename(columns = {0:'adx_rsi_returns'})
investment_value = 100000
number_of_stocks = floor(investment_value/aapl['close'][0])
adx_rsi_investment_ret = []
for i in range(len(adx_rsi_strategy_ret_df['adx_rsi_returns'])):
returns = number_of_stocks*adx_rsi_strategy_ret_df['adx_rsi_returns'][i]
adx_rsi_investment_ret.append(returns)
adx_rsi_investment_ret_df = pd.DataFrame(adx_rsi_investment_ret).rename(columns = {0:'investment_returns'})
total_investment_ret = round(sum(adx_rsi_investment_ret_df['investment_returns']), 2)
profit_percentage = floor((total_investment_ret/investment_value)*100)
print(cl('Profit gained from the ADX RSI strategy by investing $100k in AAPL : {}'.format(total_investment_ret), attrs = ['bold']))
print(cl('Profit percentage of the ADX RSI strategy : {}%'.format(profit_percentage), attrs = ['bold']))
Output:
Profit gained from the ADX RSI strategy by investing $100k in AAPL : 1531413.65
Profit percentage of the ADX RSI strategy : 1531%
Code Explanation: First, we are calculating the returns of the Apple stock using the ‘diff’ function provided by the NumPy package and we have stored it as a dataframe into the ‘aapl_ret’ variable. Next, we are passing a for-loop to iterate over the values of the ‘aapl_ret’ variable to calculate the returns we gained from our RVI trading strategy, and these returns values are appended to the ‘adx_rsi_strategy_ret’ list. Next, we are converting the ‘adx_rsi_strategy_ret’ list into a dataframe and stored it into the ‘adx_rsi_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 Apple 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 Apple 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 one million and five hundred thousand USD in around ten-and-a-half years with a profit percentage of 1531%. That’s great! 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 trading strategy returns on Apple.
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[0])
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('2010-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('ADX RSI Strategy profit is {}% higher than the Benchmark Profit'.format(profit_percentage - benchmark_profit_percentage), attrs = ['bold']))
Output:
Benchmark profit by investing $100k : 284153.94
Benchmark Profit percentage : 284%
ADX RSI Strategy profit is 1247% 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 Apple, we are investing in SPY ETF by not implementing any trading strategies. From the output, we can see that our trading strategy has outperformed the SPY ETF by 1247%. That’s awesome!
Final Thoughts!
After an extensive process of crushing both theory and coding parts, we have successfully learned what the Average Directional Index and the Relative Strength Index are all about, and combined these two indicators to build a trading strategy and it is now clear that the strategy really is a profitable one.
Let’s now talk about improvements. There are a lot of spaces left in this article to be improved but the most important aspect is to evaluate the trading strategy with metrics. This is a crucial step that needs to be done before taking the strategy to the real-world market because it helps us in understanding the performance more clearly and provides us with insights that are beyond just representing the profitability.
The reason for not discussing this aspect in this article is because strategy evaluation is a whole new chapter in the trading arena and cannot be fitted in just a small section of an article. 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 new and useful from this article.
Hang on! If you’re interested in algorithmic trading with Python, it is highly recommended to take the online program Algorithmic Trading for Everyone which is a specialization conducted by Quantra (a python platform for quantitative finance). This program is well-structured and designed in a fashion that makes both beginners and advanced users enjoy and follow along with the concepts more easily and efficiently.
Full code:
# IMPORTING PACKAGES
import numpy as np
import pandas as pd
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 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
aapl = get_historical_data('AAPL', '2010-01-01')
aapl.tail()
# ADX CALCULATION
def get_adx(high, low, close, lookback):
plus_dm = high.diff()
minus_dm = low.diff()
plus_dm[plus_dm < 0] = 0
minus_dm[minus_dm > 0] = 0
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.rolling(lookback).mean()
plus_di = 100 * (plus_dm.ewm(alpha = 1/lookback).mean() / atr)
minus_di = abs(100 * (minus_dm.ewm(alpha = 1/lookback).mean() / atr))
dx = (abs(plus_di - minus_di) / abs(plus_di + minus_di)) * 100
adx = ((dx.shift(1) * (lookback - 1)) + dx) / lookback
adx_smooth = adx.ewm(alpha = 1/lookback).mean()
return plus_di, minus_di, adx_smooth
aapl['plus_di'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[0]).rename(columns = {0:'plus_di'})
aapl['minus_di'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[1]).rename(columns = {0:'minus_di'})
aapl['adx'] = pd.DataFrame(get_adx(aapl['high'], aapl['low'], aapl['close'], 14)[2]).rename(columns = {0:'adx'})
aapl = aapl.dropna()
aapl.tail()
# ADX PLOT
plot_data = aapl[aapl.index >= '2020-01-01']
ax1 = plt.subplot2grid((11,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((11,1), (6,0), rowspan = 5, colspan = 1)
ax1.plot(plot_data['close'], linewidth = 2, color = '#ff9800')
ax1.set_title('AAPL CLOSING PRICE')
ax2.plot(plot_data['plus_di'], color = '#26a69a', label = '+ DI 14', linewidth = 3, alpha = 0.3)
ax2.plot(plot_data['minus_di'], color = '#f44336', label = '- DI 14', linewidth = 3, alpha = 0.3)
ax2.plot(plot_data['adx'], color = '#2196f3', label = 'ADX 14', linewidth = 3)
ax2.axhline(35, color = 'grey', linewidth = 2, linestyle = '--')
ax2.legend()
ax2.set_title('AAPL ADX 14')
plt.show()
# RSI CALCULATION
def get_rsi(close, lookback):
ret = close.diff()
up = []
down = []
for i in range(len(ret)):
if ret[i] < 0:
up.append(0)
down.append(ret[i])
else:
up.append(ret[i])
down.append(0)
up_series = pd.Series(up)
down_series = pd.Series(down).abs()
up_ewm = up_series.ewm(com = lookback - 1, adjust = False).mean()
down_ewm = down_series.ewm(com = lookback - 1, adjust = False).mean()
rs = up_ewm/down_ewm
rsi = 100 - (100 / (1 + rs))
rsi_df = pd.DataFrame(rsi).rename(columns = {0:'rsi'}).set_index(close.index)
rsi_df = rsi_df.dropna()
return rsi_df[3:]
aapl['rsi_14'] = get_rsi(aapl['close'], 14)
aapl = aapl.dropna()
aapl.tail()
# RSI PLOT
plot_data = aapl[aapl.index >= '2020-01-01']
ax1 = plt.subplot2grid((11,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((11,1), (6,0), rowspan = 5, colspan = 1)
ax1.plot(plot_data['close'], linewidth = 2.5)
ax1.set_title('AAPL STOCK PRICES')
ax2.plot(plot_data['rsi_14'], color = 'orange', linewidth = 2.5)
ax2.axhline(30, linestyle = '--', linewidth = 1.5, color = 'grey')
ax2.axhline(70, linestyle = '--', linewidth = 1.5, color = 'grey')
ax2.set_title('AAPL RSI 14')
plt.show()
# RSI ADX PLOT
plot_data = aapl[aapl.index >= '2020-01-01']
ax1 = plt.subplot2grid((19,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((19,1), (7,0), rowspan = 5, colspan = 1)
ax3 = plt.subplot2grid((19,1), (14,0), rowspan = 5, colspan = 1)
ax1.plot(plot_data['close'], linewidth = 2.5)
ax1.set_title('AAPL STOCK PRICES')
ax2.plot(plot_data['rsi_14'], color = 'orange', linewidth = 2.5)
ax2.axhline(30, linestyle = '--', linewidth = 1.5, color = 'grey')
ax2.axhline(70, linestyle = '--', linewidth = 1.5, color = 'grey')
ax2.set_title('AAPL RSI 14')
ax3.plot(plot_data['plus_di'], color = '#26a69a', label = '+ DI 14', linewidth = 3, alpha = 0.3)
ax3.plot(plot_data['minus_di'], color = '#f44336', label = '- DI 14', linewidth = 3, alpha = 0.3)
ax3.plot(plot_data['adx'], color = '#2196f3', label = 'ADX 14', linewidth = 3)
ax3.axhline(35, color = 'grey', linewidth = 2, linestyle = '--')
ax3.legend()
ax3.set_title('AAPL ADX 14')
plt.show()
# TRADING STRATEGY
def adx_rsi_strategy(prices, adx, pdi, ndi, rsi):
buy_price = []
sell_price = []
adx_rsi_signal = []
signal = 0
for i in range(len(prices)):
if adx[i] > 35 and pdi[i] < ndi[i] and rsi[i] < 50:
if signal != 1:
buy_price.append(prices[i])
sell_price.append(np.nan)
signal = 1
adx_rsi_signal.append(signal)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
adx_rsi_signal.append(0)
elif adx[i] > 35 and pdi[i] > ndi[i] and rsi[i] > 50:
if signal != -1:
buy_price.append(np.nan)
sell_price.append(prices[i])
signal = -1
adx_rsi_signal.append(signal)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
adx_rsi_signal.append(0)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
adx_rsi_signal.append(0)
return buy_price, sell_price, adx_rsi_signal
buy_price, sell_price, adx_rsi_signal = adx_rsi_strategy(aapl['close'], aapl['adx'], aapl['plus_di'], aapl['minus_di'], aapl['rsi_14'])
plt.plot(aapl['close'])
plt.plot(aapl.index, buy_price, marker = '^', markersize = 10, color = 'green')
plt.plot(aapl.index, sell_price, marker = 'v', markersize = 10, color = 'r')
# POSITION
position = []
for i in range(len(adx_rsi_signal)):
if adx_rsi_signal[i] > 1:
position.append(0)
else:
position.append(1)
for i in range(len(aapl['close'])):
if adx_rsi_signal[i] == 1:
position[i] = 1
elif adx_rsi_signal[i] == -1:
position[i] = 0
else:
position[i] = position[i-1]
adx = aapl['adx']
pdi = aapl['plus_di']
ndi = aapl['minus_di']
rsi = aapl['rsi_14']
close_price = aapl['close']
adx_rsi_signal = pd.DataFrame(adx_rsi_signal).rename(columns = {0:'adx_rsi_signal'}).set_index(aapl.index)
position = pd.DataFrame(position).rename(columns = {0:'adx_rsi_position'}).set_index(aapl.index)
frames = [close_price, adx, pdi, ndi, rsi, adx_rsi_signal, position]
strategy = pd.concat(frames, join = 'inner', axis = 1)
strategy.tail()
strategy[45:50]
# BACKTESTING
aapl_ret = pd.DataFrame(np.diff(aapl['close'])).rename(columns = {0:'returns'})
adx_rsi_strategy_ret = []
for i in range(len(aapl_ret)):
returns = aapl_ret['returns'][i]*strategy['adx_rsi_position'][i]
adx_rsi_strategy_ret.append(returns)
adx_rsi_strategy_ret_df = pd.DataFrame(adx_rsi_strategy_ret).rename(columns = {0:'adx_rsi_returns'})
investment_value = 100000
number_of_stocks = floor(investment_value/aapl['close'][0])
adx_rsi_investment_ret = []
for i in range(len(adx_rsi_strategy_ret_df['adx_rsi_returns'])):
returns = number_of_stocks*adx_rsi_strategy_ret_df['adx_rsi_returns'][i]
adx_rsi_investment_ret.append(returns)
adx_rsi_investment_ret_df = pd.DataFrame(adx_rsi_investment_ret).rename(columns = {0:'investment_returns'})
total_investment_ret = round(sum(adx_rsi_investment_ret_df['investment_returns']), 2)
profit_percentage = floor((total_investment_ret/investment_value)*100)
print(cl('Profit gained from the ADX RSI strategy by investing $100k in AAPL : {}'.format(total_investment_ret), attrs = ['bold']))
print(cl('Profit percentage of the ADX RSI 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[0])
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('2010-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('ADX RSI Strategy profit is {}% higher than the Benchmark Profit'.format(profit_percentage - benchmark_profit_percentage), attrs = ['bold']))
Good One Nikhil
Matured approach. Good Nikhil