Building a Powerful Stock Screener with Python: A Step-by-Step Guide (Part 1)

A guide to creating a stock screener using Python.

A stock screener is a powerful tool that can be used to identify stocks that meet specific criteria. The process of stock screening involves using various metrics and indicators to filter stocks that match certain requirements. A well-designed stock screener can help investors save time and focus on stocks that align with their investment strategies.

In this article, we will explain an object-oriented stock screener Python code. We will go through each part of the code and explain its functionality. We will also provide some insights on how this code can be extended to build more sophisticated stock screeners.

Part 1: Stock class for stock data

The first part of the code defines a Stock class that encapsulates all the data and methods required to retrieve information for a given stock. The constructor of the Stock class initializes the class attributes, including the stock ticker, sector, price, URL, and data.

# Stock class for stock data
class Stock:
    def __init__(self, ticker, sector):
        self.ticker = ticker
        self.sector = sector
        self.price = 0.0
        self.url = f"https://finance.yahoo.com/quote/{self.ticker}/key-statistics?p={self.ticker}"
        self.data = {}
        # Metric aliases pairs
        self.metric_aliases = {
            'Market Cap (intraday)': 'market_cap',
            'Beta (5Y Monthly)': 'beta',
            '52 Week High 3': '52_week_high',
            '52 Week Low 3': '52_week_low',
            '50-Day Moving Average 3': '50_day_ma',
            '200-Day Moving Average 3': '200_day_ma',
            'Avg Vol (3 month) 3': 'avg_vol_3m',
            'Avg Vol (10 day) 3': 'avg_vol_10d',
            'Shares Outstanding 5': 'shares_outstanding',
            'Float 8': 'float',
            '% Held by Insiders 1': 'held_by_insiders',
            '% Held by Institutions 1': 'held_by_institutions',
            'Short Ratio (Jan 30, 2023) 4': 'short_ratio',
            'Payout Ratio 4': 'payout_ratio',
            'Profit Margin': 'profit_margin',
            'Operating Margin (ttm)': 'operating_margin',
            'Return on Assets (ttm)': 'return_on_assets',
            'Return on Equity (ttm)': 'return_on_equity',
            'Revenue (ttm)': 'revenue',
            'Revenue Per Share (ttm)': 'revenue_per_share',
            'Gross Profit (ttm)': 'gross_profit',
            'EBITDA ': 'ebitda',
            'Net Income Avi to Common (ttm)': 'net_income',
            'Diluted EPS (ttm)': 'eps',
            'Total Cash (mrq)': 'total_cash',
            'Total Cash Per Share (mrq)': 'cash_per_share',
            'Total Debt (mrq)': 'total_debt',
            'Total Debt/Equity (mrq)': 'debt_to_equity',
            'Current Ratio (mrq)': 'current_ratio',
            'Book Value Per Share (mrq)': 'book_value_per_share',
            'Operating Cash Flow (ttm)': 'operating_cash_flow',
            'Levered Free Cash Flow (ttm)': 'levered_free_cash_flow'
        }

The class Stock is initialized by the __init__()method which takes two arguments: ticker and sector. The ticker argument represents the stock symbol while sector represents the industry sector of the stock.

In the constructor of the Stock object, some initial values are set up such as the ticker symbol, sector, and initial price, which is set to 0.0. Additionally, the constructor sets the URL to the Yahoo Finance page for the specified ticker symbol. This URL is used later to scrape data from the page. The Stock object also has a data dictionary that stores the metric data scraped from the Yahoo Finance page. Moreover, it has a metric_aliases dictionary that maps the metric names on the page to their respective keys in the data dictionary.

💡 Learn how to analyze stock market long-term data with SMA indicators using Python:

From Data Gathering to Data Analysis: Using Python Code for Financial Data Analysis

👉 To read more such acrticles, sign up for free on Differ.


Moving on, the Stock class has two methods: scrape_data() and get_stock_price(). The scrape_data() method uses Beautiful Soup to scrape metric data from the Yahoo Finance page. On the other hand, the get_stock_price() method is used to scrape the current stock price from the same page.

# Scrape statistics
def scrape_data(self):
    page = requests.get(self.url, headers=get_headers())
    soup = BeautifulSoup(page.content, 'html.parser')

    data = {}

    sections = soup.find_all('section', {'data-test': 'qsp-statistics'})
    for section in sections:
        rows = section.find_all('tr')
        for row in rows:
            cols = row.find_all('td')
            if len(cols) == 2:
                metric = cols[0].text.strip()
                if metric in self.metric_aliases:
                    data[self.metric_aliases[metric]] = cols[1].text.strip()

    self.data = data

def get_headers():
    return {"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36"}

This method performs a series of actions to extract metric data from the Yahoo Finance page corresponding to the given ticker symbol. It first constructs a request to the page using the requests.get() method along with header information provided by the get_headers() function. The response from the page is in the form of HTML content that needs to be parsed. For this, the html.parser parser is used with Beautiful Soup library.

The method then looks for the sections on the page that have a data-test attribute equal to “qsp-statistics”. These sections contain the metric data that we want to extract. Once the relevant sections have been identified, the method proceeds to iterate over each row in each section to extract the metric name and value for that metric. If the metric name is found in the metric_aliases dictionary, the method stores the corresponding metric value in the data dictionary using the appropriate key. Finally, the data dictionary is assigned to the data attribute of the Stock object, which can then be accessed later to obtain the metric values for the stock.

The get_stock_price() method is used to scrape the current stock price from the Yahoo Finance page.

# Scrape price
def get_stock_price(self):
    try:
        url = f'https://finance.yahoo.com/quote/{self.ticker}'
        response = requests.get(url, headers=get_headers())
        soup = BeautifulSoup(response.content, 'html.parser')
        data = soup.find('fin-streamer', {'data-symbol': self.ticker})
        price = float(data['value'])
        self.price = price

    except:
        print(f'Price not available for {self.ticker}')
        self.price = 0.0

This method first constructs a new URL using the ticker symbol and the base URL for the Yahoo Finance page. The method searches for a fin-streamer element on the page with a data-symbol attribute equal to the specified ticker symbol. This element contains the current stock price, which is extracted using the value attribute and converted to a float. Finally, the method assigns the price to the price attribute of the Stock object. If an error occurs during the scraping process, the method sets the price to 0.0 and prints an error message.

That’s a brief overview of the Stock class and its methods. In the next section, we will look at how this class is used to create a stock screener.

Part 2: Stock Screener class

The next part of the code contains the StockScreener class that has two main methods: scrape_data and apply_filters.

# Stocks Screener Class
class StockScreener:
    def __init__(self, stocks, filters):
        self.stocks = stocks
        self.filters = filters

    # Add data to stocks
    def add_data(self):
        for stock in self.stocks:
            stock.scrape_data()
            stock.get_stock_price()

    # Select stocks that pass all filters
    def apply_filters(self):
        filtered_stocks = []
        for stock in self.stocks:
            passed_all_filters = True
            for filter_func in self.filters:
                if not filter_func(stock):
                    passed_all_filters = False
                    break
            if passed_all_filters:
                filtered_stocks.append(stock)
        return filtered_stocks

In the StockScreener class, the scrape_data method is responsible for populating the data attribute and setting the price attribute of each Stock object in the stocks list. This is achieved by iterating through the list of Stock objects and calling their scrape_data and get_stock_price methods.

After this, the apply_filters method is called, which applies a set of filters to the stocks list and returns a new list containing only the Stock objects that pass all filters. These filters are passed to the constructor of the StockScreener class as a list of functions. Each function in the list takes a Stock object and returns a boolean value indicating whether the Stock object passes the filter or not.

The next step is to define the filters themselves, in order to make it possible to filter by all stock attributes: sector, price, and metric. The first filter, filter_sector, takes a Stock object and a sector string. It returns True if the Stock object’s sector attribute matches the sector string.

def filter_sector(stock, sector):
    return stock.sector == sector

The second filter filter_price a Stock object, a min_price float, and a max_price float, and returns True if the Stock object's price attribute is between min_price and max_price, inclusive.

def filter_price(stock, min_price, max_price):
    return min_price <= stock.price <= max_price

The last filter function, filter_metric takes a Stock object, a metric string (Yahoo Finance statistics), an operator string a value string and returns True if the condition is met.

def filter_metric(stock, metric, operator, value):
    if metric not in stock.data:
        return False

    # Convert value to same units as metric, if necessary
    if 'B' in stock.data[metric]:
        stock.data[metric] = stock.data[metric].replace('B', '')
        value = float(value) / 1e9
    elif 'M' in stock.data[metric]:
        stock.data[metric] = stock.data[metric].replace('M', '')
        value = float(value) / 1e6
    elif '%' in stock.data[metric]:
        stock.data[metric] = stock.data[metric].replace('%', '')
        value = float(value)
    else:
        value = float(value)

    # Check condition according to operator
    if operator == '>':
        return float(stock.data[metric]) > value
    elif operator == '>=':
        return float(stock.data[metric]) >= value
    elif operator == '<':
        return float(stock.data[metric]) < value
    elif operator == '<=':
        return float(stock.data[metric]) <= value
    elif operator == '==':
        return float(stock.data[metric]) == value
    else:
        return False

In order to filter by statistics we have to do some data processing to get the values in the correct format .The function converts the value string to the appropriate units if necessary and compares the value of the metric key in the data dictionary to the value according to the operator. If the metric key is not present in the data dictionary, filter_metric returns False.

Overall, the StockScreener class provides a convenient way to filter a list of Stock objects based on various criteria. The scrape_data method can be used to populate the data attribute and price attribute of each Stock object before applying filters. The apply_filters method allows for complex filtering based on multiple criteria. By defining custom filter functions, users can easily add new filters or modify existing ones to suit their needs.

Part 3: Get SP500 tickers and sectors

Now that we have our Stock and StockScreener classes defined, we can use them to build a simple stock screener application that retrieves data on S&P 500 stocks and applies filters to find stocks that match specific criteria.

We begin by using the requests and BeautifulSoup libraries to scrape data from the Wikipedia page listing the S&P 500 companies. We extract the ticker symbol, company name, and sector for each company, and store this information in a list of dictionaries called sp500.

# Get sp500 ticker and sector
url = 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies'
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')

table = soup.find('table', {'class': 'wikitable sortable'})
rows = table.find_all('tr')[1:]  # skip the header row

sp500 = []

for row in rows:
    cells = row.find_all('td')
    ticker = cells[0].text.strip()
    company = cells[1].text.strip()
    sector = cells[3].text.strip()
    sp500.append({'ticker': ticker, 'company': company, 'sector': sector})

Next, we define a function called get_sp500_stocks() that creates a Stock object for each company in the sp500 list, and assigns the stock's sector to the sector argument of the Stock constructor.

def get_sp500_stocks():
    sp500_stocks = [Stock(stock['ticker'], stock['sector']) for stock in sp500]
    return sp500_stocks

This function returns a list of Stock objects representing the S&P 500 companies, which we can then use to build a StockScreener object.

Part 4: Using the Screener

Let’s walk through an example of using the StockScreener class to filter a list of stocks. First, we create an instance of the class and pass in a list of two stocks and a set of filters. These filters are defined as lambda functions that take a Stock object as input and return a True or False value depending on whether the stock passes the filter criteria.

In this specific example, the filters are checking whether each stock belongs to the ‘Interactive Media & Services’ sector, has a price between 50 and 200, and has more than 3 billion outstanding shares.

To populate the stock data, we call the add_data()method, which in turn calls the scrape_data()and get_stock_price()methods on each stock object.

Once the stock data has been added, we apply the filters by calling the apply_filters()method on the StockScreener object. This method loops through all the stocks and applies each filter in turn. Any stock that fails any of the filters is rejected, while any stock that passes all the filters is added to the filtered_stocks list.

# Run example with 2 stocks
filters = [lambda stock: filter_sector(stock, 'Interactive Media & Services'),
           lambda stock: filter_price(stock, 60, 200),
           lambda stock: filter_metric(stock, 'shares_outstanding', '>', 3*1e9)]

sp500_stocks = [Stock('GOOGL', 'Interactive Media & Services'), Stock('GOOG', 'Interactive Media & Services')]
screener = StockScreener(sp500_stocks, filters)
screener.add_data()
filtered_stocks = screener.apply_filters()

In the example below, we define a new set of filters that select stocks in the ‘Asset Management & Custody Banks’ sector with a price between 50 and 200 and a profit margin greater than 10%. We use the get_sp500_stocks() function to create a list of Stock objects for all the S&P 500 companies and pass it to a new instance of the StockScreener class. We again add data to the stocks and apply the filters to get a list of filtered stocks.

# Run screener for all sp500 tickers
filters = [lambda stock: filter_sector(stock, 'Asset Management & Custody Banks'),
           lambda stock: filter_price(stock, 50, 200),
           lambda stock: filter_metric(stock, 'profit_margin', '>', 10)]

sp500_stocks = get_sp500_stocks()
screener = StockScreener(sp500_stocks, filters)
screener.add_data()
filtered_stocks = screener.apply_filters()

This is just a simple example of how the StockScreener class can be used to filter stocks based on various criteria. Depending on your needs, you can define your own filters and add them to the filters list to perform custom filtering.

How to build more sophisticated stock screeners

The current implementation of the StockScreener class provides a basic framework for filtering stocks based on a few simple criteria such as sector, stock price, and certain financial metrics. However, there are many other factors that investors may consider when evaluating stocks.

To build a more sophisticated stock screener, we can extend the existing code in several ways. First, we can add more filter functions to the StockScreener class that capture additional criteria that investors may care about. For example, we could add a filter that screens for stocks with a high dividend yield or low price-to-earnings ratio. We could also add filters that screen for stocks with low debt-to-equity ratios, high return on equity, or other financial ratios.

Second, we could integrate more data sources into the Stock class to provide a richer set of data for screening. For example, we could scrape data from financial news sources, social media sentiment analysis, or other alternative data sources to supplement the financial metrics that are currently being used.

Third, we could incorporate machine learning algorithms into the screening process to automatically identify patterns in the data that may be predictive of future stock performance. For example, we could use a classification algorithm to identify stocks that are likely to outperform the market based on a range of criteria.

Finally, we could create a user interface that allows investors to interactively select and adjust filters based on their specific investment goals and risk preferences. This could involve building a web application that integrates with the existing codebase, allowing investors to specify their criteria, view the results of their screening, and adjust their criteria as needed. Tell me in the comments if you want me to write about this.

Overall, there are many opportunities to extend and enhance the existing codebase to create a more sophisticated and powerful stock screening tool. By leveraging the power of Python and machine learning, we can build a tool that helps investors make more informed decisions and achieve better investment outcomes.

Conclusion

In conclusion, we have explored how to build a simple stock screener in Python using web scraping and object-oriented programming principles. The article covered the creation of a Stock class to store stock data and a StockScreener class to apply filters to a list of stocks. The stock screener was then demonstrated by filtering a list of S&P 500 stocks based on sector, price, and financial metrics. This implementation can be extended to include more sophisticated filters and data sources, providing a powerful tool for stock analysis and selection.

Enjoyed this article?

Share it with your network to help others discover it

Continue Learning

Discover more articles on similar topics