Implementing Universe Selection (Top N Stocks) in Backtesting: A QuantConnect Guide
Limit your backtest universe to the N largest stocks by market capitalization using QuantConnect's two-stage Coarse-Fine selection pattern, where FineSelectionFunction sorts by MarketCap and returns the top N symbols on your rebalance schedule.
The paperswithbacktest/awesome-systematic-trading repository demonstrates how to implement universe selection across dozens of factor-based strategies. Every example follows a reusable architecture that filters thousands of securities down to a manageable top-N list before applying alpha models, ensuring your backtest remains computationally efficient and replicable.
The Two-Stage Universe Selection Pattern
QuantConnect separates universe selection into coarse and fine filters. The coarse stage eliminates securities without fundamental data, while the fine stage performs metric-based sorting to isolate the top N stocks.
This pattern appears consistently in the repository's strategy implementations. For example, in static/strategies/value-book-to-market-factor.py (lines 38-57), the algorithm first establishes a self.coarse_count = 3000 parameter, then uses a boolean self.selection_flag to trigger rebalancing only on scheduled dates.
Step-by-Step Implementation
Setting the Universe Size and Schedule
Define your universe size as an instance variable and attach a monthly schedule to trigger selection. The flag mechanism prevents unnecessary calculations on non-rebalance days.
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.coarse_count = 3000 # N - top 3000 stocks
self.selection_flag = False
self.month = 12
self.AddUniverse(self.CoarseSelection, self.FineSelection)
# Trigger selection at month-end
self.Schedule.On(self.DateRules.MonthEnd(self.symbol),
self.TimeRules.AfterMarketOpen(self.symbol),
self.Selection)
def Selection(self):
if self.month == 12:
self.selection_flag = True
self.month = (self.month % 12) + 1
Filtering with CoarseSelection
The coarse filter acts as a pre-screener. Return Universe.Unchanged when selection_flag is False to maintain the current universe; otherwise, return symbols that have fundamental data.
def CoarseSelection(self, coarse):
if not self.selection_flag:
return Universe.Unchanged
# Keep only securities with fundamental data and US listings
return [c.Symbol for c in coarse
if c.HasFundamentalData and c.Market == "usa"]
Selecting Top N by Market Cap in FineSelection
The fine selection function receives FineFundamental objects containing MarketCap and valuation ratios. Sort these descending by market capitalization and slice to your coarse_count.
def FineSelection(self, fine):
# Filter out securities without required ratios and specific exchanges
filtered = [x for x in fine
if x.ValuationRatios.PBRatio != 0
and x.SecurityReference.ExchangeId in ["NYS", "NAS", "ASE"]]
# Sort by market cap descending, keep top N
sorted_by_market_cap = sorted(filtered,
key=lambda x: x.MarketCap,
reverse=True)
top_by_market_cap = sorted_by_market_cap[:self.coarse_count]
# Continue with factor-based sorting (e.g., P/B for value)
return [x.Symbol for x in top_by_market_cap]
Complete Code Examples
Basic Top-N Universe Selection
This standalone implementation demonstrates the minimal logic required to select the 1000 largest stocks by market cap and rebalance monthly.
class TopNUniverse(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2020, 1, 1)
self.SetCash(100000)
self.universe_size = 1000
self.selection_flag = False
self.AddUniverse(self.CoarseSelection, self.FineSelection)
self.Schedule.On(self.DateRules.MonthEnd(),
self.TimeRules.AfterMarketOpen("SPY"),
self.TriggerSelection)
def TriggerSelection(self):
self.selection_flag = True
def CoarseSelection(self, coarse):
if not self.selection_flag:
return Universe.Unchanged
return [c.Symbol for c in coarse if c.HasFundamentalData]
def FineSelection(self, fine):
top = sorted(fine, key=lambda x: x.MarketCap, reverse=True)[:self.universe_size]
return [x.Symbol for x in top]
Top-N with Factor Sorting (Value/HML)
Adapted from value-book-to-market-factor.py, this example selects the top 3000 by market cap, then constructs a High-Minus-Low (HML) portfolio by sorting on Price-to-Book ratio.
class ValueHML(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.coarse_count = 3000
self.selection_flag = False
self.AddUniverse(self.CoarseSelection, self.FineSelection)
self.Schedule.On(self.DateRules.MonthEnd("SPY"),
self.TimeRules.AfterMarketOpen("SPY"),
self.EnableSelection)
def EnableSelection(self):
self.selection_flag = True
def CoarseSelection(self, coarse):
if not self.selection_flag:
return Universe.Unchanged
return [c.Symbol for c in coarse if c.HasFundamentalData]
def FineSelection(self, fine):
# Step 1: Top N by market cap
top = sorted(fine, key=lambda x: x.MarketCap, reverse=True)[:self.coarse_count]
# Step 2: Sort by P/B ratio (ascending = value)
sorted_by_pb = sorted(top, key=lambda x: x.ValuationRatios.PBRatio)
quintile = len(sorted_by_pb) // 5
self.long = [s.Symbol for s in sorted_by_pb[:quintile]]
self.short = [s.Symbol for s in sorted_by_pb[-quintile:]]
return self.long + self.short
Rebalancing Logic in OnData
After universe selection, execute trades by liquidating securities that fell out of the universe and rebalancing the new constituents.
def OnData(self, data):
if not self.selection_flag:
return
self.selection_flag = False
# Liquidate positions no longer in universe
for symbol in self.Portfolio.Keys:
if self.Portfolio[symbol].Invested and symbol not in (self.long + self.short):
self.Liquidate(symbol)
# Equal-weight rebalancing
for symbol in self.long:
self.SetHoldings(symbol, 1.0 / len(self.long))
for symbol in self.short:
self.SetHoldings(symbol, -1.0 / len(self.short))
Key Implementation Details from the Source
The selection flag architecture is critical for performance. According to the source code in paperswithbacktest/awesome-systematic-trading, strategies like Momentum Factor (static/strategies/momentum-factor-effect-in-stocks.py) and Asset Growth (static/strategies/asset-growth-effect.py) use this exact pattern, varying only the secondary sorting metric after the top-N filter.
Key parameters to adjust:
self.coarse_count: Controls universe size (commonly 500, 1000, or 3000)ExchangeIdfiltering: Restricts to specific exchanges (NYS, NAS, ASE) to avoid illiquid OTC securitiesDateRules: Most strategies useMonthEnd()but you can substituteWeekStart()or custom dates
The Short-Term Reversal strategy (static/strategies/short-term-reversal-in-stocks.py) demonstrates a two-step filter: first selecting the 500 most liquid stocks, then applying the top-N market cap logic, showing how these patterns compose.
Summary
- Use two-stage selection:
CoarseSelectionfilters for fundamental data availability;FineSelectionsorts byMarketCapand returns the top N. - Implement a boolean flag: Trigger universe updates only on rebalance dates using
self.Schedule.Oncombined with aselection_flagcheck to optimize backtest speed. - Access market cap directly: The
fineobjects exposeMarketCapas a property—no manual calculation required. - Reference working implementations: Study
value-book-to-market-factor.pyfor the canonical 3000-stock universe pattern andmomentum-factor-effect-in-stocks.pyfor smaller universe variants.
Frequently Asked Questions
How do I change the rebalance frequency from monthly to weekly?
Replace self.DateRules.MonthEnd() with self.DateRules.WeekStart() in your schedule registration. Update your flag logic in the trigger function to reset weekly instead of using modulo 12 arithmetic on months.
Can I filter by metrics other than market capitalization?
Yes. After slicing the top N by market cap, sort the resulting list by any ValuationRatios property such as PBRatio, PERatio, or EVToEBITDA. The Asset Growth strategy modifies this to sort by FinancialStatements.BalanceSheet.TotalAssets growth rates.
Why does my algorithm return Universe.Unchanged?
Returning Universe.Unchanged prevents the backtester from rebuilding the universe on every tick, which is computationally expensive. Only return a new symbol list when self.selection_flag is True to indicate a rebalance date has arrived.
How many securities should I include in my top-N universe?
The repository examples range from 500 (momentum strategies requiring liquidity) to 3000 (value strategies requiring broader coverage). For institutional replication, match the universe size to your benchmark—Russell 1000 for large-cap, Russell 3000 for total market.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →