How to Implement the Momentum Factor Effect in Stocks with QuantConnect
The momentum factor strategy builds a long-short equity portfolio by ranking stocks on their 12-month returns (excluding the most recent month to avoid microstructure bias) and rebalancing monthly into equal-weight quantiles.
The momentum factor effect is one of the most persistent anomalies in equity markets, where stocks with high past returns continue to outperform those with poor returns. In the paperswithbacktest/awesome-systematic-trading repository, you will find a complete implementation that runs on QuantConnect’s Lean engine. This algorithm demonstrates institutional-grade practices for universe selection, rolling window calculations, and transaction cost modeling.
Strategy Architecture Overview
The implementation follows a classic long-short equity framework. The algorithm selects the 500 largest-cap U.S. equities, computes total return momentum over a 12-month lookback period, and constructs dollar-neutral portfolios by going long the top quantile and short the bottom quantile.
Key components include:
- Universe Filtering: Coarse selection by dollar volume, fine selection by market cap and exchange
- Momentum Estimation:
RollingWindowstoring 252 daily closes (12 months × 21 trading days) - Signal Construction: Total return calculation skipping the most recent month to mitigate reversal effects
- Execution: Monthly rebalancing with custom fee models and 10x leverage simulation
Step-by-Step Implementation
Universe Construction with Coarse and Fine Selection
The algorithm uses QuantConnect’s two-tier universe selection to minimize computational overhead. In CoarseSelectionFunction, the system filters for the top 500 securities by dollar volume that have fundamental data and trade on U.S. markets.
def CoarseSelectionFunction(self, coarse):
# Update rolling windows with today's price
for stock in coarse:
if stock.Symbol in self.data:
self.data[stock.Symbol].Add(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
# Pick the top-cap stocks
selected = [c.Symbol for c in sorted(
[c for c in coarse if c.HasFundamentalData and c.Market == 'usa'],
key=lambda c: c.DollarVolume, reverse=True)[:self.coarse_count]]
For new symbols entering the universe, the algorithm initializes a RollingWindow and preloads historical data using self.History(symbol, self.period, Resolution.Daily) to ensure immediate calculation readiness.
Computing 12-Month Momentum with RollingWindows
The momentum metric follows the academic definition of 12-1 month momentum (12 months of returns excluding the most recent month). The implementation stores 252 trading days (12 × 21) in a RollingWindow[float] for each security.
In FineSelectionFunction, the algorithm calculates total return:
def FineSelectionFunction(self, fine):
# Filter out zero-cap stocks and keep US exchanges only
fine = [x for x in fine if x.MarketCap != 0 and
x.SecurityReference.ExchangeId in ("NYS","NAS","ASE")]
# Momentum = total return over the rolling window
perf = {x.Symbol: self.data[x.Symbol][0] /
self.data[x.Symbol][self.period-1] - 1 for x in fine}
This approach avoids look-ahead bias by using only historical closing prices stored in the rolling window.
Quantile-Based Portfolio Construction
After computing momentum scores, the algorithm ranks securities and splits them into five equal-weight quantiles. The top quantile receives long positions, while the bottom quantile receives short positions.
if len(perf) >= self.quantile:
sorted_perf = sorted(perf.items(),
key=lambda kv: kv[1], reverse=True)
q = int(len(sorted_perf) / self.quantile)
long = [s for s, _ in sorted_perf[:q]]
short = [s for s, _ in sorted_perf[-q:]]
# Equal weight within each side
for s in long: self.weight[s] = 1 / len(long)
for s in short: self.weight[s] = -1 / len(short)
Position sizing uses inverse weighting by the number of securities in each leg, maintaining dollar neutrality between long and short exposures.
Monthly Rebalancing Schedule
The strategy rebalances at the start of each month using QuantConnect’s scheduling API. This ensures all rolling windows are fully populated with fresh data before selection occurs.
def Initialize(self):
# Monthly rebalance trigger
symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.Schedule.On(self.DateRules.MonthStart(symbol),
self.TimeRules.AfterMarketOpen(symbol),
self.Selection)
def Selection(self):
self.selection_flag = True
The selection_flag boolean coordinates between the universe selection methods and the OnData execution handler, ensuring trades occur only after the universe has been fully refreshed.
Complete QuantConnect Algorithm Code
The full implementation is available in static/strategies/momentum-factor-effect-in-stocks.py within the repository. Below is the complete algorithm incorporating universe selection, momentum calculation, and execution logic:
from AlgorithmImports import *
class MomentumFactorEffectinStocks(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
# Universe settings
self.coarse_count = 500
self.period = 12 * 21 # 12 months * 21 trading days
self.quantile = 5
self.selection_flag = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction,
self.FineSelectionFunction)
# Monthly rebalance trigger
symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.Schedule.On(self.DateRules.MonthStart(symbol),
self.TimeRules.AfterMarketOpen(symbol),
self.Selection)
def CoarseSelectionFunction(self, coarse):
for stock in coarse:
if stock.Symbol in self.data:
self.data[stock.Symbol].Add(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
selected = [c.Symbol for c in sorted(
[c for c in coarse if c.HasFundamentalData and c.Market == 'usa'],
key=lambda c: c.DollarVolume, reverse=True)[:self.coarse_count]]
for symbol in selected:
if symbol not in self.data:
self.data[symbol] = RollingWindow[float](self.period)
history = self.History(symbol, self.period, Resolution.Daily)
if history.empty: continue
for close in history.loc[symbol].close:
self.data[symbol].Add(close)
return [s for s in selected if self.data[s].IsReady]
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0 and
x.SecurityReference.ExchangeId in ("NYS","NAS","ASE")]
perf = {x.Symbol: self.data[x.Symbol][0] /
self.data[x.Symbol][self.period-1] - 1 for x in fine}
if len(perf) >= self.quantile:
sorted_perf = sorted(perf.items(),
key=lambda kv: kv[1], reverse=True)
q = int(len(sorted_perf) / self.quantile)
long = [s for s, _ in sorted_perf[:q]]
short = [s for s, _ in sorted_perf[-q:]]
for s in long: self.weight[s] = 1 / len(long)
for s in short: self.weight[s] = -1 / len(short)
return list(self.weight.keys())
def OnData(self, data):
if not self.selection_flag: return
self.selection_flag = False
for s in [p for p, v in self.Portfolio.items() if v.Invested]:
if s not in self.weight: self.Liquidate(s)
for s, w in self.weight.items():
if s in data and data[s]:
self.SetHoldings(s, w)
self.weight.clear()
def Selection(self):
self.selection_flag = True
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
Key Implementation Details
Transaction Costs and Leverage: The algorithm applies a CustomFeeModel charging 0.5 basis points per share traded, providing realistic cost assumptions for institutional execution. While the example uses 10x leverage via SetHoldings, you can adjust this by modifying the weight multipliers or using SetLeverage on individual securities.
Data Warm-up: The initialization logic ensures all RollingWindow objects contain exactly 252 data points before inclusion in the portfolio. This prevents momentum calculations based on incomplete histories during the startup period.
Exchange Filtering: The fine selection explicitly filters for NYSE (NYS), NASDAQ (NAS), and AMEX (ASE) securities, eliminating OTC and pink sheet stocks that may exhibit different momentum dynamics or liquidity constraints.
Summary
- The momentum factor effect in stocks is implemented in QuantConnect by creating a 12-month rolling window of daily prices for each security.
- Coarse and fine selection filters the universe to 500 liquid, large-cap U.S. equities before computing signals.
- The strategy goes long the top quintile and short the bottom quintile, rebalancing monthly using
DateRules.MonthStart. - Full source code is available at
static/strategies/momentum-factor-effect-in-stocks.pyin thepaperswithbacktest/awesome-systematic-tradingrepository.
Frequently Asked Questions
Why skip the most recent month when calculating momentum?
Skipping the most recent month (the "12-1" formation period) avoids short-term reversal effects caused by microstructure noise and liquidity shocks. This convention, established in academic finance literature, ensures the momentum signal captures persistent price trends rather than temporary spikes or bid-ask bounce.
How does the RollingWindow work in QuantConnect?
The RollingWindow[float] is a fixed-size circular buffer that maintains the last n data points. When new prices arrive via CoarseSelectionFunction, they are added with .Add(), automatically ejecting the oldest value. This provides O(1) access to historical prices without repeated history API calls, making it efficient for universe-wide calculations.
What is the difference between CoarseSelectionFunction and FineSelectionFunction?
CoarseSelectionFunction receives lightweight data (price, volume, dollar volume) for the entire equity universe, allowing rapid filtering by liquidity. FineSelectionFunction receives fundamental data (market cap, exchange classification) only for symbols returned by the coarse filter. This two-tier approach minimizes memory usage and computation time by deferring expensive fundamental lookups to a smaller candidate set.
How can I backtest this strategy locally?
You can run this algorithm locally using the QuantConnect Lean open-source engine. Clone the paperswithbacktest/awesome-systematic-trading repository, navigate to static/strategies/momentum-factor-effect-in-stocks.py, and execute it within your Lean environment using the QuantConnect data feed or local CSV data. Ensure you have sufficient RAM for the 500-security universe and 12-month rolling windows.
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 →