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: RollingWindow storing 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.py in the paperswithbacktest/awesome-systematic-trading repository.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →