While several free online stock screeners exist, such as Yahoo Finance, Finviz, and Trading View, they often come with limitations or reserve certain features for paid subscriptions. It’s frustrating when you need a specific tool for your investment strategy, and it’s either missing or locked behind a paywall. Why pay for a full subscription when you only require one particular feature? Ever thought about building your own solution? Hint: you need to have some basic knowledge of Python…
So let’s dive into making our custom stock screener. We will be using Python and yfinance, a cool Python module, to grab stock info from Yahoo Finance and stash it neatly in an Excel sheet. And guess what? It won’t cost you a dime! To get it on your machine, just type this in your terminal:
pip install yfinance
Also, you might want to have a look at my 101 guide for yfinance module:
Next up, we need a list of stocks for our screener. Unfortunately, yfinance doesn’t hand out lists, but no worries! You can grab any stock list online. For this tutorial, I got the SP500 list from here:
https://datahub.io/core/s-and-p-500-companies
Now, the code. It’s on GitHub with extra nuggets of wisdom at https://github.com/phitzi/Custom_Stock_Screener/tree/main in the form of a Jupyter Notebook. There you will find more data processing, like cleaning the NaNs, converting epoch dates to human reading ones, sorting columns, etc.
A few imports and readings before we get started:
import numpy as np
import pandas as pd
import yfinance as yf
import os
from datetime import datetime
#create a data folder - it will be handy
data_directory = os.getcwd() + "/data/"
#import the stocks file in a df
stocks = pd.read_csv(data_directory + "sp500.csv")
Then, in a loop for all the stocks, we pull in data using finance.
yf_info = []
for index, row in stocks.iterrows():
try:
ticker = yf.Ticker(row["Symbol"])
yf_info.append(ticker.info)
except Exception as e:
print(f'Something went wrong with symbol {row["Symbol"]} with error {e}')
screener = pd.DataFrame(yf_info)
If you are wondering what the ticker.info looks like, you can check an example at my GitHub https://github.com/phitzi/Custom_Stock_Screener/blob/main/data/info_example_AAPL.json

While yfinance already includes the most important ratios, you can always create your custom columns to be able to analyze them later in Excel. I will put here some ideas that use different techniques and feel free to add more in the comments for other users to benefit.
1/ Sometimes, those big capitalization numbers need a bit of categorizing for clarity. The other option is to count the zeros. Let’s make it more meaningful. We will categorize based on their Market Cap from Nano to Mega. Since we are talking about S&P500 stocks most of them will be large. However, if you expand your list of stocks to process, you will find the kids also 😉
# Define the bins and labels for each capitalization category
bins = [0, 50_000_000, 300_000_000, 2_000_000_000, 10_000_000_000, 200_000_000_000, float('inf')]
labels = ['nano', 'micro', 'small', 'mid', 'large', 'mega']
# Create a new column with the categorized values
screener['CapCategory'] = pd.cut(screener['marketCap'], bins=bins, labels=labels, right=False)
2/ Ever wondered how close a stock’s current price is to its 52-week low or high? We’ll whip up a ratio for that, making it easy to spot the high flyers and the low riders.
screener['52WeeksCurrentPosition'] = (screener['currentPrice'] - screener['fiftyTwoWeekLow']) / (screener['fiftyTwoWeekHigh'] -screener['fiftyTwoWeekLow'])
3/ Here’s a fun one: calculating the average age of a company’s top dogs (C-level officers). Tesla’s got the youth, while TJX is rocking the seniority. Well done, Elon!
def get_company_officers(row):
# we check in case is not a list to return NaN and not throw an error
if not isinstance(row['companyOfficers'], list):
return None, None
return len(row['companyOfficers']), round(np.mean([obj['age'] for obj in row['companyOfficers'] if 'age' in obj and obj['age'] is not None]), 2)
screener[['CountOfCompanyOfficers','AvgAgeOfCompanyOfficers']] = screener.apply(lambda row: pd.Series(get_company_officers(row)), axis=1)
After this coding fiesta, save your data frame as a CSV in the data folder.
formatted_datetime = datetime.now().strftime('%Y%m%d%H%M')
screener.to_csv(f'{data_directory}{formatted_datetime} Custom Screener.csv')
Now you have the CSV file, which you can manage through Excel with filters, pivots, vlookups, and all those things that make us love Excel.
Various possibilities await exploration, so please share any additional analyses you consider important in the comments.
Come along on my data-driven investing journey! Follow me on Medium, Twitter (currently known as X), and Github. I’m all about simplicity and growth through coding, writing, and trying to understand data 😉
Thanks for reading!