Backtesting a basic yet effective strategy in Python
Introduction
In the vast world of stock market strategies, there’s always a hunt for the next big method that can yield impressive returns with minimal risk. After experimenting with various technical indicators and complex algorithms, I found myself intrigued by a more fundamental approach — one that focuses on core financial metrics of companies rather than just their price movements.
That’s when I came across the idea of combining the Price-to-Book (P/B) Ratio with Return on Equity (ROE) to uncover undervalued stocks with strong financial performance. The results were quite compelling, and in this article, I’ll walk you through how I implemented this strategy using Python, sourcing data from EODHD’s APIs, and ultimately achieved a strategy that outperformed the market.
So, without further ado, let’s dive into the details of this fundamental trading strategy and see how you can implement it too!
Understanding the Strategy
Before we dive into the code and backtesting, it’s crucial to understand the two key metrics that form the backbone of this strategy:
1. Price-to-Book (P/B) Ratio:
The Price-to-Book (P/B) Ratio compares a company’s market value to its book value. A low P/B ratio may indicate that a stock is undervalued, or it could signal underlying issues with the company’s fundamentals. However, when paired with a high ROE, it often points to undervalued companies with strong financials.
2. Return on Equity (ROE):
Return on Equity (ROE) measures a company’s profitability relative to shareholders’ equity. A high ROE indicates that the company is generating significant returns on the equity invested by shareholders, which is a positive signal for investors.
Our Fundamental Trading Strategy
The basic idea behind the strategy is to identify companies that are potentially undervalued by the market but are still managing to generate high returns on equity. The mechanics of the strategy are simple:
Stock Selection: We screen for stocks within the S&P 500 with a P/B ratio of less than 2 and an ROE greater than 15%.
Investment Thesis: The selected stocks are likely undervalued but efficient in generating profits. By investing in these stocks, we anticipate that the market will eventually correct the pricing mismatch, leading to price appreciation.
Trading Logic: We take a long position in these selected stocks and monitor the portfolio’s performance over time.
Now, let’s get our hands dirty and implement this strategy in Python!
Step-1: Importing Packages
The first and foremost step is to import all the required packages into our Python environment. In this article, we’ll be using just three packages that are:
Pandas — for data formatting, clearing, manipulating, and other related purposes
Matplotlib — for creating charts and different kinds of visualizations
Requests — for making API calls in order to extract data
The following code imports all the above-mentioned packages into our Python environment:
# IMPORTING PACKAGES
import pandas as pd
import requests
import matplotlib.pyplot as plt
If you haven’t installed any of the imported packages, make sure to do so using the pip command in your terminal.
Step-2: Extracting S&P 500 Tickers
In this article, we will be picking stocks based on our screening criteria from the S&P 500 Index. In order to do that, we first need the list of stocks included in the index. Gone are the days of extracting the tickers from Wikipedia’s page using web scraping processes as now we have more accessible and diverse APIs. The following code extracts the list of tickers available in the index using EODHD’s Fundamental data API:
api_key = '<YOUR API KEY>'
sp500_json = requests.get(f'https://eodhd.com/api/fundamentals/GSPC.INDX?api_token={api_key}&fmt=json').json()
stocks = []
for i in range(len(sp500_json['Components'])):
ticker = sp500_json['Components'][f'{i}']
stocks.append(ticker)
print(f"Total S&P 500 stocks: {len(stocks)}")
We start by defining our API key (Make sure to replace <YOUR EODHD API KEY> with your secret API key which you obtain by creating an EODHD developer account). Then, we make an API request to fetch the fundamental data of the S&P 500 index itself, represented by the ticker GSPC.INDX.
The JSON response from the API contains a 'Components' field, which holds the list of all S&P 500 tickers. We loop through this list, extracting each ticker and appending it to the stocks list.
Finally, we print the total number of S&P 500 stocks retrieved, ensuring that our list is complete and ready for further processing in the strategy.
Step-3: Extracting Fundamental Data
In this step, we are going to extract the necessary fundamental data for each stock in the S&P 500 list using the EODHD’s Fundamental data API endpoint. Specifically, we are interested in the Price-to-Book (P/B) ratio and the Return on Equity (ROE), which are key metrics for our strategy.
# EXTRACTING FUNDAMENTAL DATA FOR S&P 500 STOCKS
api_key = '<YOUR EODHD API KEY>'
fundamentals_df = pd.DataFrame(columns = ['stock', 'pb', 'roe'])
fundamentals_df['stock'] = stocks
for i in range(len(fundamentals_df)):
stock = fundamentals_df["stock"][i]
try:
f_json = requests.get(f'https://eodhd.com/api/fundamentals/{stock}?api_token={api_key}&fmt=json').json()
fundamentals_df['pb'][i] = f_json['Valuation']['PriceBookMRQ']
fundamentals_df['roe'][i] = f_json['Highlights']['ReturnOnEquityTTM']
print(f'{i}. {stock} finished')
except:
print(f'{i}. {stock} ERROR')
fundamentals_df.head()
We start creating an empty DataFrame fundamentals_df with columns for the stock ticker, P/B ratio, and ROE.
By iterating over each stock ticker, we make an API request to fetch its fundamental data. The requests.get() function retrieves the JSON response, from which we extract the P/B ratio (PriceBookMRQ) and ROE (ReturnOnEquityTTM). These values are then assigned to the respective columns in our DataFrame.
The try-except block ensures that any errors during the API call, such as missing data, connectivity issues, etc.
The final dataframe looks like this:
Step-4: Picking & Sorting Stocks based on Screening Criteria
Now that we have the fundamental data for each stock, the next step is to filter and identify the top-performing stocks based on our screening criteria: a P/B ratio less than 2 and an ROE greater than 15%.
# PICKING & SORTING STOCKS BASED ON SCREENING CONDITION
fundamentals_df['pb'], fundamentals_df['roe'] = fundamentals_df['pb'].astype(float), fundamentals_df['roe'].astype(float)
top10_stocks = fundamentals_df[(fundamentals_df.pb < 2) & (fundamentals_df.roe > 0.15)].nlargest(10, 'roe')
top10_stocks
First, we convert the P/B ratio and ROE columns to float types to ensure numerical comparisons can be made. We then apply our screening criteria by filtering the DataFrame for stocks with a P/B ratio below 2 and an ROE above 15%. The nlargest() function is used to select the top 10 stocks with the highest ROE among those that meet these conditions. This gives us a list of potentially undervalued yet profitable stocks, ready for further analysis or investment.
According to our screening criteria, these are the top 10 stocks along with their P/B ratio and ROE:
Step-5: Extracting Historical Data
After identifying our top 10 stocks based on the P/B ratio and ROE, the next step is to gather their historical price data which can be extracted using EODHD’s end-of-day historical data endpoint. This data will be used to backtest our strategy.
# EXTRACTING HISTORICAL DATA
historical_data = pd.DataFrame(columns = top10_stocks.stock)
for stock in historical_data.columns:
hist_json = requests.get(f'https://eodhd.com/api/eod/{stock}?from=2020-01-01&period=d&api_token={api_key}&fmt=json').json()
hist_df = pd.DataFrame(hist_json)
date, adj_close = hist_df.date, hist_df.adjusted_close
historical_data[stock], historical_data['date'] = adj_close, date
historical_data = historical_data.set_index('date')
historical_data.index = pd.to_datetime(historical_data.index)
historical_data
We begin by creating an empty DataFrame historical_data with columns corresponding to each of our top 10 stocks.
For each stock, we make an API request to the API to retrieve its daily historical price data starting from January 1, 2020. The data is stored in a DataFrame, with the adjusted closing prices being saved under the respective stock column.
We also capture the dates, which are set as the DataFrame’s index and converted to datetime format for accurate time series analysis. This prepares our data for the next steps, where we will calculate returns and evaluate the strategy's performance.
The final dataframe looks like this:
Step-6: Calculating Portfolio Returns
With the historical price data in place, the next step is to calculate the daily returns of our portfolio. These returns will allow us to evaluate the performance of our strategy.
# CALCULATING RETURNS OF THE PORTFOLIO
rets_df = historical_data.pct_change().dropna()
rets_df['total_ret'] = rets_df.sum(axis = 1)
rets_df.tail()
We calculate the daily returns for each stock in our portfolio using the pct_change() function, which computes the percentage change between consecutive days. The dropna() function is applied to remove any rows with missing data, ensuring that our calculations are accurate.
To determine the overall portfolio performance, we sum the daily returns across all stocks and store the result in a new column called total_ret. This provides a single series representing the daily returns of our entire portfolio, which we can now use to analyze the effectiveness of our strategy.
Here’s a glimpse of the newly created rets_df dataframe:
Step-7: Visualizing Cumulative Returns
With the portfolio’s daily returns calculated, the final step is to visualize the cumulative returns. This will help us understand the overall performance of the strategy over time.
# CUMULATIVE RETURNS
rets_df['total_ret'].cumsum().plot()
plt.title('STRATEGY PERFORMANCE')
We use the cumsum() function to calculate the cumulative sum of the portfolio's daily returns. This gives us the cumulative return over time, reflecting how the portfolio's value has grown (or shrunk) from the start of the backtesting period. The plot() function is then used to create a very simple line chart to quickly assess the performance of the strategy.
The output of the above code is nothing but intriguing:
Here’s an interpretation of the graph:
Early Decline (2020): The strategy experienced a significant drop, with cumulative returns falling below -7.5%. This likely reflects broader market volatility during early 2020.
Strong Recovery (2020–2021): The portfolio quickly rebounded, moving into positive territory and continuing to grow steadily through 2021, showing that the strategy capitalized on the market recovery.
Stability and Mild Volatility (2022–2024): From 2022 onwards, the portfolio shows a more stable performance, with cumulative returns fluctuating between 5 and 10. While growth slows, the strategy maintains a positive return, indicating resilience.
Overall, the strategy demonstrates a strong recovery after initial losses and maintains consistent positive returns through the period.
Conclusion
Through this journey, we’ve explored a fundamental trading strategy that combines the Price-to-Book (P/B) ratio with Return on Equity (ROE) to uncover undervalued stocks that still demonstrate strong financial performance. By leveraging Python and the reliable API endpoints provided by EODHD, we implemented and backtested this strategy, revealing its potential to outperform the market.
The results were compelling — despite the early challenges reflected in 2020’s market downturn, the strategy demonstrated a strong recovery and sustained growth. The portfolio’s cumulative returns showed resilience, particularly during periods of market instability, which highlights the robustness of focusing on fundamental strengths.
While this strategy has shown promise, it’s essential to remember that no approach is foolproof. Continuous monitoring, adjustments based on market conditions, and a solid risk management plan are crucial to maintain long-term success. That said, this strategy offers a straightforward yet effective way to identify and invest in potentially undervalued gems within the market.
I hope this article has provided you with valuable insights and a practical approach to fundamental trading. If you have any thoughts or suggestions, feel free to share them in the comments. Thank you very much for your time.
Comentários