How to Build an Efficient Trading Strategy with Fundamental Data
- Nikhil Adithyan
- Jan 29
- 7 min read
Building a data-driven investment strategy by analyzing fundamentals and historical performance

Written in collaboration with Filippos Tzimopoulos
In algorithmic trading, technical data often takes center stage, with strategies relying heavily on price patterns, indicators, and market trends. However, incorporating fundamental data can significantly enhance decision-making and long-term profitability.
Fundamental metrics such as a company’s financial health, profitability, and debt levels provide a deeper understanding of its intrinsic value, offering insights that technical analysis alone cannot capture. By blending fundamental analysis with historical performance, traders can create robust, data-driven strategies that identify undervalued stocks with strong growth potential.
This approach ensures a more comprehensive evaluation, aligning algorithmic precision with sound investment principles for sustainable success.
What to expect from this article
Accessing Financial Data: Learn how to retrieve the last 10 years’ balance sheet data for approximately 2,000 stocks.
Calculating Key Ratios: Understand how to compute essential fundamental metrics like Debt-to-Asset, Return on Equity (ROE), and Return on Assets (ROA) to evaluate a company’s financial health and performance.
Analyzing Historical Prices: Discover how to gather and analyze the last 10 years of historical stock prices to assess market trends and performance.
Building a Stock Selection Strategy: Explore how to rank and select top-performing stocks based on fundamental ratios for investment.
Visualizing Results: Learn how to draw an equity curve to track portfolio performance and derive actionable conclusions from the strategy.
Let’s code!
We need historical fundamental data to backtest our premise, and we will use EODHD APIs for that reason.
But first, let’s do our imports and set our token for the APIs:
import pandas as pd
import requests
import requests_cache
from datetime import datetime
import matplotlib.pyplot as plt
from tqdm import tqdm
import math
token = '<YOUR API TOKEN>'
Now, we will need to set our universe of stocks. The more the merrier, so let’s use EODHD’s Exchanges API and get all the stocks traded in the New York Stock Exchange (NYSE).
EXCHANGE_CODE = 'US'
url = f'https://eodhd.com/api/exchange-symbol-list/{EXCHANGE_CODE}'
querystring = {"api_token":token,"fmt":"json"}
response = requests.get(url, params=querystring).json()
df_symbols = pd.DataFrame.from_dict(response)
df_symbols = df_symbols[(df_symbols['Exchange'] == 'NYSE') & (df_symbols['Type'] == 'Common Stock')]

This will return more than 2200 stocks, which we will convert to a dictionary to make it easier to loop through while keeping all the information.
symbols = df_symbols.set_index('Code').to_dict(orient='index')
Data and metrics
Now is the time for some real action, so let us explain the code.
We will loop through all the stocks, and for each stock:
We will get the fundamentals from the API and skip stocks that have less than 5B capitalization (penny stocks and small ones will most probably distort our results, but we still have around 900 stocks to analyze)
Get the historic yearly balance sheets that the EODHD’s Fundamental Data API provides
For each balance sheet, we will calculate the Debt to Assets Ratio, the Return on Equity, and the Return on Assets
Also, for each stock, we will get the daily prices of the last 10 years
In the end, we will have two data frames. One that will keep all the balance sheets and one that will have the closing prices of all the stocks for the last ten years
Before we move further to the code, let’s see why this ratios are essential:
Debt-to-Assets Ratio: Measures a company’s financial leverage, indicating the proportion of assets financed by debt, which is crucial for assessing financial stability.
Return on Equity (ROE): Reflects profitability by showing how effectively a company generates returns for shareholders from their invested capital.
Return on Assets (ROA): It evaluates operational efficiency by determining how well a company uses its assets to generate earnings, highlighting management effectiveness.
total_balance_sheets_df = pd.DataFrame()
start_date = '2015-01-01'
total_prices_df = pd.DataFrame()
for symbol in tqdm(symbols):
url = f'https://eodhd.com/api/fundamentals/{symbol}.{EXCHANGE_CODE}'
querystring = {"api_token":token,"fmt":"json"}
try:
response = requests.get(url, params=querystring).json()
symbols[symbol]['Fundamentals'] = response
# if currency not USD skip
if response.get('General', {}).get('CurrencyCode', None) != 'USD':
continue
# if capitalization is less than 5 bil skip
if float(response.get('Highlights', {}).get('MarketCapitalization', 0)) < 5_000_000_000:
continue
balance_sheets = response.get("Financials", {}).get("Balance_Sheet", {}).get("yearly", {})
balance_sheets_df = pd.DataFrame(balance_sheets).T
cols_to_numeric = ['totalLiab', 'totalAssets' , 'retainedEarnings', 'totalStockholderEquity' ]
balance_sheets_df[cols_to_numeric] = balance_sheets_df[cols_to_numeric].apply(pd.to_numeric, errors='coerce')
if balance_sheets_df.empty:
continue
if 'totalLiab' in balance_sheets_df.columns and 'totalAssets' in balance_sheets_df.columns:
balance_sheets_df['DebtToAssets'] = balance_sheets_df['totalLiab'] / balance_sheets_df['totalAssets']
else:
balance_sheets_df['DebtToAssets'] = None
if 'totalStockholderEquity' in balance_sheets_df.columns and 'retainedEarnings' in balance_sheets_df.columns:
balance_sheets_df['ROE'] = balance_sheets_df['retainedEarnings'] / balance_sheets_df['totalStockholderEquity']
else:
balance_sheets_df['ROE'] = None
if 'totalAssets' in balance_sheets_df.columns and 'retainedEarnings' in balance_sheets_df.columns:
balance_sheets_df['ROA'] = balance_sheets_df['retainedEarnings'] / balance_sheets_df['totalAssets']
else:
balance_sheets_df['ROA'] = None
balance_sheets_df['symbol'] = symbol
balance_sheets_df.reset_index(inplace=True)
total_balance_sheets_df = pd.concat([total_balance_sheets_df, balance_sheets_df], axis=0)
except Exception as e:
continue
df_symbol = pd.DataFrame()
try:
url = f'https://eodhd.com/api/eod/{symbol}.{EXCHANGE_CODE}'
querystring = {"api_token":token,"fmt":"json", "from":start_date}
response = requests.get(url, params=querystring).json()
df_symbol = pd.DataFrame(response).set_index('date')
df_symbol.rename(columns={'close': symbol}, inplace=True)
except Exception as e:
pass
if total_prices_df.empty:
total_prices_df = pd.DataFrame(df_symbol[symbol])
else:
total_prices_df = total_prices_df.join(df_symbol[symbol], how='outer')
Now we need to do some data fine-tuning, so we should convert the fields filing-date and date to datetime. Note that the “filing date” is the day the company provides the results for, while the “date” is the day of submission. For example, a company submits its balance sheet for the end of year (31 Dec 2024 — “filing date”) on the 20th of January (“date”). Also, we will fill possible price gaps by replacing them with the last value (forward fill).
total_balance_sheets_df['filing_date'] = pd.to_datetime(total_balance_sheets_df['filing_date'])
total_balance_sheets_df['date'] = pd.to_datetime(total_balance_sheets_df['date'])
total_prices_df.fillna(method='ffill', inplace=True)
As we discussed, the prices dataframe has one column for each stock with the closing price. To accommodate the logic to calculate the return for the stocks invested, we need for each stock to create two more columns:
One with the suffix he _pct so we keep the daily return of each stock (we will use it later when we calculate the final result)
and one with the suffix _invested. This will be a boolean column with a default value of false. It will be set to true when we invest in the specific stock on a specific day.
for column in total_prices_df.columns:
if column != 'date': # Skip the 'date' column
total_prices_df[f'{column}_pct'] = total_prices_df[column].pct_change()
# Add new columns with the same name and suffix '_invested' with value False
for column in total_prices_df.columns:
if not column.endswith('_pct') and column != 'date': # Avoid duplicate for '_pct' and 'date'
total_prices_df[f'{column}_invested'] = False
total_prices_df.index = pd.to_datetime(total_prices_df.index)
Now, the fun begins! We will set our algorithm’s start date to 10 years ago. Our strategy will be that every 1st of each month, we will get the last balance sheets for all the stocks of the last three months and pick the best 10 to invest for the specific month.
Let us, of course, explain how we select the stocks we will be investing in each month. After filtering for the specific 3 months, we will normalize the three metrics to have equivalent values (normalized) and find the mean value. Then, we just sort them and select the first 10. Note that Debt to Equity needs to be inverted since the smaller, the better, opposite to the other 2, the larger, the better (ROA and ROE). Practically, it is straightforward to consider the three metrics equally weighted.
At the end of each loop for each month, we will update the prices dataframe, with the columns with the suffix _invested as True for the stocks that are the best 10 for each month.
# Initialize start date and end date
start_date = datetime(2015, 2, 1)
end_date = datetime(2025, 1, 1)
assets_to_invest = 10
# Generate a list of the 1st of each month from start_date to end_date
current_date = start_date
first_of_months = []
while current_date <= end_date:
first_of_months.append(current_date)
# Move to the 1st of the next month
next_month = current_date.month % 12 + 1
next_year = current_date.year + (current_date.month // 12)
current_date = datetime(next_year, next_month, 1)
for date in first_of_months:
# Calculate the range: from three months before to the current date
three_months_before = date - pd.DateOffset(months=12)
# Filter rows within this range
filtered_df = total_balance_sheets_df[(total_balance_sheets_df['date'] >= three_months_before) & (total_balance_sheets_df['date'] < date)]
# Sort by Date descending and remove duplicates based on Symbol
filtered_df = filtered_df.sort_values(by='date', ascending=False).drop_duplicates(subset='symbol', keep='first')
# Perform Min-Max normalization manually for each column
def min_max_normalize(column):
return (column - column.min()) / (column.max() - column.min())
# Normalize the relevant columns
filtered_df['Norm_DebtToAssets'] = min_max_normalize(filtered_df['DebtToAssets'])
filtered_df['Norm_ROE'] = min_max_normalize(filtered_df['ROE'])
filtered_df['Norm_ROA'] = min_max_normalize(filtered_df['ROA'])
# Invert the normalized Debt to Assets Ratio (since lower is better)
filtered_df['Norm_DebtToAssets_Inverted'] = 1 - filtered_df['Norm_DebtToAssets']
# Calculate the equal weighted score
filtered_df['Equal_Weighted_Score'] = filtered_df[['Norm_DebtToAssets', 'Norm_ROE', 'Norm_ROA']].mean(axis=1)
sorted_data = filtered_df.sort_values(by='Equal_Weighted_Score', ascending=False)
# Keep only the first rows after sorting
limited_df = sorted_data.head(assets_to_invest)
# Update the prices df with invested for the specific month
month_to_invest = date.month
year_to_invest = date.year
for symbol in limited_df['symbol'].tolist():
total_prices_df.loc[(total_prices_df.index.month == month_to_invest) & (total_prices_df.index.year == year_to_invest), f'{symbol}_invested'] = True
This will end up in a dataframe that will have the stocks we invest for each row for the specific month as true. Following that, we will loop (using the function apply) for each day of the prices and calculate the average daily return for that day for the stocks that are True. Here, we assume that we invest equally in each stock.
# Identify columns ending with '_invested' and '_pct'
invested_cols = [col for col in total_prices_df.columns if col.endswith('_invested')]
pct_cols = [col.replace('_invested', '_pct') for col in invested_cols]
# Calculate the total_pct_change for each row
def calculate_total_pct_change(row):
# Filter columns where '_invested' is True and remove NaN values
invested_pct_values = [
row[pct_col] for pct_col, invested_col in zip(pct_cols, invested_cols)
if row[invested_col] and not math.isnan(row[pct_col])
]
# Return the average of those values, or 0 if no investments are True
ret = sum(invested_pct_values) / len(invested_pct_values) if invested_pct_values else 0
return ret
total_prices_df['total_pct_change'] = total_prices_df.apply(calculate_total_pct_change, axis=1)
Now that we have the final row of our daily returns, the calculation of the equity curve is a one-liner using the Cumprod function with an initial capital of 100.
total_prices_df['equity_curve'] = (1 + total_prices_df['total_pct_change']).cumprod() * 100
Let’s see the returns now by plotting the equity curve.
plt.figure(figsize=(10, 6)) # Set the figure size
plt.plot(total_prices_df.index, total_prices_df['equity_curve'], label='Equity Curve', color='blue', linewidth=2)
# Add labels and title
plt.title('Equity Curve', fontsize=16)
plt.xlabel('Date', fontsize=14)
plt.ylabel('Equity Value', fontsize=14)
# Add grid and legend
plt.grid(True, linestyle='--', alpha=0.6)
plt.legend(fontsize=12)
# Rotate x-axis labels for better readability
plt.xticks(rotation=45)
# Show the plot
plt.tight_layout() # Adjust layout to prevent clipping
plt.show()

Conclusion and what‘s next
Considering the strategy’s simplicity, the result is very promising! This proves that you don’t need to be a finance expert to use the fundamental stock data to benefit from them.
There are a lot of parameters that you can try to fine-tune to find the best balance in terms of return vs risk. We encourage you to copy the code and investigate the results by:
Using different financial metrics
Trying out different weights per metric to rank the top ones
Filter to sectors or different capitalizations
With that being said, you’ve reached the end of the article. Hope you learned something new and useful today. Thank you very much for your time.
Disclaimer: While we explore the exciting world of investing in this article, it’s crucial to note that the information provided is for educational purposes only. I’m not a financial advisor, and the content here doesn’t constitute financial advice. Always do your research and consider consulting with a professional before making any investment decisions.