🚀 AI One-Click Generation of quantconnect Strategy Code

How to Use Fundamental Data for Universe Selection in QuantConnect

quantconnect | Published on: 2025/12/11
The following content is generated by EasyQuant .

Problem Description

How do I select stocks to trade based on their fundamental data, like P/E ratio?

Solution

To select stocks based on fundamental data like the P/E ratio in QuantConnect, you must use the Universe Selection mechanism. This involves two steps:

  1. Coarse Selection: Filters the entire market (thousands of stocks) based on price, volume, and whether they have fundamental data. This is fast and reduces the search space.
  2. Fine Selection: Filters the remaining stocks based on specific corporate fundamentals (e.g., ValuationRatios.PERatio).

Below is a complete algorithm that selects the top 10 liquid stocks with the lowest P/E ratios (Value Investing approach) and rebalances the portfolio daily.

# region imports
from AlgorithmImports import *
# endregion

class FundamentalSelectionAlgorithm(QCAlgorithm):

    def initialize(self):
        # 1. Set Strategy Settings
        self.set_start_date(2020, 1, 1)
        self.set_end_date(2023, 1, 1)
        self.set_cash(100000)

        # 2. Set Universe Settings
        # We need Daily resolution to access fundamental data efficiently
        self.universe_settings.resolution = Resolution.DAILY
        
        # 3. Add Universe Selection
        # We pass two functions: one for coarse selection (price/volume) 
        # and one for fine selection (fundamentals)
        self.add_universe(self.coarse_selection_function, self.fine_selection_function)
        
        # Variables to hold our target symbols
        self._target_symbols = []

    def coarse_selection_function(self, coarse):
        """
        Filters the universe by price, volume, and fundamental data availability.
        """
        # Filter for stocks that have fundamental data, price > 5, and decent volume
        selected = [
            x for x in coarse 
            if x.has_fundamental_data and x.price > 5 and x.dollar_volume > 1000000
        ]
        
        # Sort by dollar volume (descending) to get the most liquid stocks
        sorted_by_dollar_vol = sorted(selected, key=lambda x: x.dollar_volume, reverse=True)
        
        # Return the top 200 symbols to be passed to the Fine Selection function
        return [x.symbol for x in sorted_by_dollar_vol[:200]]

    def fine_selection_function(self, fine):
        """
        Filters the coarse list based on P/E Ratio.
        """
        # Filter: Ensure P/E ratio is positive (exclude companies losing money)
        # 'valuation_ratios' contains fields like PERatio, PBRatio, etc.
        filtered = [x for x in fine if x.valuation_ratios.pe_ratio > 0]
        
        # Sort: Ascending order (Lowest P/E first -> Value Strategy)
        sorted_by_pe = sorted(filtered, key=lambda x: x.valuation_ratios.pe_ratio)
        
        # Select the top 10 stocks
        self._target_symbols = [x.symbol for x in sorted_by_pe[:10]]
        
        return self._target_symbols

    def on_data(self, data):
        """
        Trading Logic: Rebalance portfolio to the target symbols.
        """
        # If we have no targets yet, return
        if not self._target_symbols:
            return

        # 1. Liquidate securities that are no longer in our target list
        for symbol in self.portfolio.keys():
            if self.portfolio[symbol].invested and symbol not in self._target_symbols:
                self.liquidate(symbol)

        # 2. Buy/Rebalance target securities
        # We allocate 100% of equity equally among the target symbols (10% each if 10 stocks)
        count = len(self._target_symbols)
        if count > 0:
            for symbol in self._target_symbols:
                if self.securities[symbol].is_tradable:
                    self.set_holdings(symbol, 1.0 / count)

    def on_securities_changed(self, changes):
        """
        Event fired when securities are added or removed from the universe.
        Useful for logging.
        """
        for security in changes.removed_securities:
            self.log(f"Removed {security.symbol} from Universe")
            
        for security in changes.added_securities:
            self.log(f"Added {security.symbol} to Universe")

Key Components Explained

  1. self.add_universe(coarse, fine):
    This registers the two filtering steps. The engine runs coarse first. The symbols returned by coarse are populated with fundamental data and passed to fine.

  2. coarse_selection_function:

    • x.has_fundamental_data: Crucial check. If this is False, the stock will not appear in the Fine step.
    • x.dollar_volume: We sort by liquidity to ensure we can easily enter and exit positions. We limit the output to 200 to keep the Fine selection fast.
  3. fine_selection_function:

    • x.valuation_ratios.pe_ratio: This accesses the Morningstar fundamental data.
    • Logic: We filter for pe_ratio > 0 to avoid companies with negative earnings, then sort ascending to find "cheap" stocks relative to earnings.
  4. on_data:
    This method runs daily (since resolution is Daily). It checks the self._target_symbols list updated by the universe selection and adjusts the portfolio holdings to match.

Common Fundamental Properties

You can access other fundamental data in the fine object similarly:

  • Market Cap: x.market_cap
  • Price to Book: x.valuation_ratios.pb_ratio
  • Earnings Per Share: x.earning_reports.basic_eps.twelve_months
  • Return on Equity: x.operation_ratios.roe.value

Q&A

Q: How often does the Universe Selection run?
A: By default, Universe Selection runs once per day at midnight. The on_data method will then receive the data for the selected securities during market hours.

Q: Can I combine P/E ratio with other metrics?
A: Yes. In the fine_selection_function, you can create complex logic. For example:

filtered = [x for x in fine if x.valuation_ratios.pe_ratio < 15 and x.valuation_ratios.pb_ratio < 1.5]

Q: Why do I need the Coarse selection? Can't I just use Fine?
A: QuantConnect requires a Coarse selection to filter the 8,000+ available US equities down to a smaller number (usually < 500) before loading the heavy fundamental data objects. This ensures the backtest runs efficiently.

Q: What happens if a stock is delisted?
A: The on_securities_changed event will fire with the stock in changes.removed_securities. The logic in on_data handles this by liquidating positions that are no longer in the target list.