Leveraging Delisted Stocks for Backtesting a 3-EMA Strategy Using Python
Learn how this strategy compares to the buy/hold approach and discover the potential for improved trading decisions.
The Exponential Moving Average (EMA) is a popular technical indicator in the trading community, but it is often not utilized to its full potential. Many traders rely on generic strategies such as the EMA 50 & 200 crossover. While not inherently bad, its widespread use has reduced its effectiveness. Today, we will explore a different EMA crossover strategy to see if it can outperform the traditional buy/hold strategy.
However, there’s a twist. We will backtest this strategy on delisted stocks. Why? Because delisted stocks tend to have more volatile price movements compared to listed stocks, allowing us to test our strategy across various market conditions.
In this article, we will start by extracting historical data for delisted stocks using EODHD’s end-of-day data API endpoint. Next, we will obtain the EMA data using EODHD’s technical indicator API. Finally, we will construct the trading strategy and backtest it on a delisted stock. We will also compare the performance of our EMA crossover strategy with the buy/hold strategy for validation.
Let’s dive into the details!
The Trading Strategy
Before diving into the code, it’s crucial to understand our EMA crossover trading strategy. This strategy uses three EMAs with different lookback periods and generates signals based on specific crossover criteria.
Here’s how the strategy works:
- Enter the market if: EMA 10 crosses above EMA 30 and EMA 10 is greater than EMA 50
- Exit the market if: EMA 10 crosses below EMA 30 and EMA 9 is lower than EMA 50
This straightforward crossover strategy aims to capture price movement directions and provide trading signals accordingly.
Now that we have a clear understanding of the trading strategy, let’s move on to the coding part to implement this idea into a practical strategy.
Importing Packages
First, we need to import all the necessary packages into our Python environment. For this article, we’ll be using the following packages:
- Pandas — for data formatting, cleaning, manipulation, and other related tasks
- Matplotlib — for creating charts and visualizations
- eodhd — for extracting historical and technical indicator data
- Termcolor — for customizing the standard output shown in Jupyter notebook
- Math — for various mathematical functions and operations
Here’s the code to import these packages into our Python environment:
# IMPORTING PACKAGES
import pandas as pd
from eodhd import APIClient
import math
from termcolor import colored as cl
import matplotlib.pyplot as plt
plt.rcParams['figure.figsize'] = (20,10)
plt.style.use('fivethirtyeight')
If you haven’t installed any of these packages, make sure to do so using the pip command in your terminal.
API Key Activation
The next step is to activate your API key to use the EODHD stock screener API, which is included in the All-In-One and EOD+Intraday plans.
If you don’t have an EODHD API key, visit website, complete the registration process to create an account, and navigate to the Dashboard page to find your secret API key. Ensure that your API key remains confidential.
Here’s the code to activate your API key:
# API KEY ACTIVATION
api_key = '<YOUR API KEY>'
client = APIClient(api_key)
This code is straightforward. The first line stores the secret EODHD API key in the `api_key` variable. The second line uses the `APIClient` class provided by the `eodhd` package to activate the API key and stores the response in the `client` variable.
Remember to replace `<YOUR API KEY>` with your secret EODHD API key. For better security, you can also use environmental variables to store the API key instead of hardcoding it.
Getting the List of Delisted Stocks
Let’s extract the list of delisted stocks available on EODHD using the `get_list_of_tickers` function from the eodhd package. Here’s how you can do it:
# GETTING LIST OF DELISTED STOCKS
tickers = client.get_list_of_tickers(delisted = 1, code = ‘NASDAQ’)
tickers_df = pd.DataFrame(tickers)
tickers_df = tickers_df.fillna(‘None’)
tickers_df = tickers_df[tickers_df.Isin != ‘None’]
tickers_df
The `get_list_of_tickers` function has two parameters. The first is `delisted`, which should be set to 1 to fetch delisted stocks or 0 for listed stocks. The second parameter is `code`, where you specify the exchange of interest.
After retrieving the list of tickers, we convert the response into a Pandas dataframe. The final output, after some data cleaning and manipulation, looks like this:
One aspect I particularly appreciate about this function is that it not only provides a list of tickers but also includes additional information such as the stock’s name, exchange, current status, type, etc., which can be very useful in various scenarios.
Extracting Historical Data
For backtesting our strategy, we’ll focus on a delisted stock: AveXis, which is now known as Novartis Gene Therapies. AveXis was a biotechnology company based in Dallas, Texas, founded in 2012 and acquired by Novartis in 2018. The following code uses EODHD’s end-of-day data API endpoint to extract the historical data of AveXis:
# EXTRACTING HISTORICAL DATA
hist_json = client.get_eod_historical_stock_market_data(symbol = 'AVXS.US', period = 'd')
df = pd.DataFrame(hist_json)
df = df.set_index('date')
df
This code is straightforward. First, we use the `get_eod_historical_stock_market_data` function provided by the eodhd package to retrieve the historical data of AveXis from 2016, the year it went public.
Then, we convert the JSON response into a Pandas dataframe and perform some basic data preprocessing. The final output is a dataframe with the historical data:
The data covers just over two years, reflecting the short period AveXis was traded before its acquisition.
Extracting EMA Values
While some people prefer calculating the EMA values by manually coding the mathematical formulas, we will use a more streamlined approach by obtaining the EMA values through APIs. To do this, we will use EODHD’s technical indicator API endpoint. The following code uses this specific endpoint to retrieve the EMA values for AveXis:
# EXTRACTING EMA DATA
ema10 = pd.DataFrame(client.get_technical_indicator_data(ticker = 'AVXS.US', function = 'ema', period = 10)).set_index('date').rename(columns = {'ema':'ema10'})
ema30 = pd.DataFrame(client.get_technical_indicator_data(ticker = 'AVXS.US', function = 'ema', period = 30)).set_index('date').rename(columns = {'ema':'ema30'})
ema50 = pd.DataFrame(client.get_technical_indicator_data(ticker = 'AVXS.US', function = 'ema', period = 50)).set_index('date').rename(columns = {'ema':'ema50'})
adj_close = df[['adjusted_close']]
avxs_df = ema50.join([ema10, ema30, adj_close])
avxs_df.index = pd.to_datetime(avxs_df.index)
avxs_df.tail()
The code might seem complex at first glance, but it’s quite straightforward. In the first three lines, we are retrieving the EMA values with different lookback periods, each stored in separate dataframes. Finally, we combine these dataframes along with the stock’s adjusted close price into a single dataframe. Here is the final output:
Now that we have all the necessary data, let’s proceed to construct and backtest our 3-EMA crossover trading strategy.
Backtesting the Strategy
Now comes the most crucial and engaging part of the article: backtesting our trading strategy. The following code implements a straightforward backtesting system and reveals the results of our trading strategy:
# BACKTESTING THE STRATEGY
def implement_strategy(df, investment):
in_position = False
equity = investment
for i in range(1, len(df)):
if df['ema10'][i-1] < df['ema30'][i-1] and df['ema10'][i] > df['ema30'][i] and df['ema10'][i] > df['ema50'][i] and in_position == False:
no_of_shares = math.floor(equity/df.adjusted_close[i])
equity -= (no_of_shares * df.adjusted_close[i])
in_position = True
print(cl('BUY: ', color = 'green', attrs = ['bold']), f'{no_of_shares} Shares are bought at ${df.adjusted_close[i]} on {str(df.index[i])[:10]}')
elif df['ema10'][i-1] > df['ema30'][i-1] and df['ema10'][i] < df['ema30'][i] and df['ema10'][i] < df['ema50'][i] and in_position == True:
equity += (no_of_shares * df.adjusted_close[i])
in_position = False
print(cl('SELL: ', color = 'red', attrs = ['bold']), f'{no_of_shares} Shares are bought at ${df.adjusted_close[i]} on {str(df.index[i])[:10]}')
if in_position == True:
equity += (no_of_shares * df.adjusted_close[i])
print(cl(f'\nClosing position at {df.adjusted_close[i]} on {str(df.index[i])[:10]}', attrs = ['bold']))
in_position = False
earning = round(equity - investment, 2)
roi = round(earning / investment * 100, 2)
print('')
print(cl(f'EARNING: ${earning} ; ROI: {roi}%', attrs = ['bold']))
implement_strategy(avxs_df, 100000)
I won’t delve deeply into the mechanics of this code as it can be quite detailed. In essence, the program executes trades based on whether specific conditions are met. It enters the market when our entry condition is satisfied and exits when the exit condition is met. Here are the trades executed by our program followed by the backtesting results:
The results are quite intriguing. Over the span of two years, our program executed only three trades. This could be seen as both positive and negative, depending on the perspective. Ultimately, the focus should be on the returns of our strategy. Our 3-EMA trading strategy generated $664K with an ROI of 664.83%. This is indeed impressive news!
Buy/Hold Returns Comparison
An effective trading strategy should not only be profitable but should also outperform the buy/hold strategy. For those unfamiliar, the buy/hold strategy involves purchasing a stock and holding onto it regardless of market conditions for an extended period.
If our strategy surpasses the buy/hold strategy, it indicates that we have developed a strong trading strategy that is almost ready for real-world deployment. Conversely, if it doesn’t, we either need to make significant adjustments or discard it altogether. Essentially, this step will determine whether we proceed with our strategy.
The following code implements the buy/hold strategy and calculates the returns:
# BUY/HOLD STRATEGY RETURNS
bh_roi = round(list(avxs_df['adjusted_close'].pct_change().cumsum())[-1],4)*100
print(cl(f'BUY/HOLD STRATEGY ROI: {round(bh_roi,2)}%', attrs = ['bold']))
This code calculates the stock returns for the given data, and here is the output:
After comparing the buy/hold strategy results with our 3-EMA strategy, we see that our strategy outperformed the buy/hold strategy by 381% in terms of ROI. That’s incredible! It’s nearly 2.3 times the returns of the buy/hold strategy. We can now confidently say that we have developed a robust trading strategy.
Conclusion
To recap, we began by extracting a list of delisted stocks from EODHD and selected one for backtesting our strategy. We then obtained historical data and the EMA values with different lookback periods for that stock using EODHD’s end-of-day API endpoint and technical indicator API endpoint. After gathering the necessary data, we constructed and backtested our 3-EMA trading strategy. Finally, we compared our strategy’s results with the buy/hold strategy to validate its effectiveness.
There is still plenty of room for improvement that this article does not cover. For instance, implementing a proper risk management system could lead to a more systematic trading approach. Additionally, using machine learning models to optimize the strategy parameters could be beneficial. Although these aspects are not practically explored in this article, they are worth considering if you plan to deploy the strategy.
With that, we’ve reached the end of the article. I hope you found this information useful and insightful. Thank you for your time.
The original article was published in the EODHD Academy by Nikhil Adithyan.
Please note that this article is for informational purposes only and should not be taken as financial advice. We do not bear responsibility for any trading decisions made based on the content of this article. Readers are advised to conduct their own research or consult with a qualified financial professional before making any investment decisions.
For those eager to delve deeper into such insightful articles and broaden their understanding of different strategies in financial markets, we invite you to follow our account and subscribe for email notifications.
Stay tuned for more valuable articles that aim to enhance your data science skills and market analysis capabilities.