How to Backtest Momentum Factor Strategies Across Asset Classes Using QuantConnect Lean

You can backtest momentum factor strategies across equities, ETFs, commodities, and currencies by inheriting from QCAlgorithm in the QuantConnect Lean engine, utilizing RollingWindow or ROC indicators for 12-month lookback calculations, and scheduling monthly rebalances via Schedule.On or calendar checks.

The awesome-systematic-trading repository provides ready-to-run implementations of classic momentum factors using the QuantConnect Lean backtesting framework. Each strategy is encapsulated as a single Python class that demonstrates how to calculate momentum signals, select universes, and execute trades across diverse asset classes from a unified codebase.

Architecture of a Momentum Backtest

Every strategy in paperswithbacktest/awesome-systematic-trading follows a consistent pattern. Algorithms inherit from QCAlgorithm and import base classes via AlgorithmImports at the top of each file (see static/strategies/momentum-factor-effect-in-stocks.py line 10). The Initialize method sets the cash amount, start date, and a warm-up period—typically self.SetWarmUp(12 * 21)—to ensure rolling-window indicators are populated before the first trade.

Most implementations attach a CustomFeeModel that charges 0.5 bps per trade value, making results realistic without external broker dependencies (as shown in static/strategies/momentum-factor-effect-in-stocks.py lines 17‑21).

Equity Momentum: The UMD (Up-Minus-Down) Factor

The classic equity momentum strategy ranks stocks by their past 12-month return, then goes long the top quantile and short the bottom quantile.

Universe Selection with Coarse and Fine Filters

For equity-wide studies, the algorithm uses Coarse/Fine selection to filter for liquid stocks with fundamental data. In static/strategies/momentum-factor-effect-in-stocks.py, the CoarseSelectionFunction (lines 28‑34) first warms up RollingWindow[float] objects for each candidate symbol, storing the last 252 trading days of prices. The FineSelectionFunction then computes the 12-month return as window[0] / window[period-1] - 1 and splits the universe into long and short buckets.

Quantile-Based Weight Construction

The strategy divides the filtered universe into five quantiles. It assigns equal weights of 1/len(long) to the top performers and -1/len(short) to the bottom performers, creating a market-neutral portfolio (lines 81‑94). The OnData method (lines 97‑112) liquidates any holdings that fall out of the target list and calls SetHoldings(symbol, weight) to establish new positions.


# File: static/strategies/momentum-factor-effect-in-stocks.py

from AlgorithmImports import *

class MomentumFactorEffectinStocks(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100_000)
        self.period = 12 * 21               # 12‑month look‑back (trading days)

        self.quantile = 5                   # split into 5‑quantile buckets

        self.coarse_count = 500             # top‑500 by dollar volume

        self.selection_flag = False
        self.data = {}
        self.weight = {}
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
        self.Schedule.On(self.DateRules.MonthStart("SPY"),
                         self.TimeRules.AfterMarketOpen("SPY"),
                         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 = [x.Symbol for x 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 s in selected:
            if s not in self.data:
                self.data[s] = RollingWindow[float](self.period)
                hist = self.History(s, self.period, Resolution.Daily)
                for _, row in hist.loc[s].iterrows():
                    self.data[s].Add(row["close"])
        return [s for s in selected if self.data[s].IsReady]

    def FineSelectionFunction(self, fine):
        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  = [sym for sym, _ in sorted_perf[:q]]
            short = [sym for sym, _ 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 held in [p.Key for p in self.Portfolio if p.Value.Invested]:
            if held not in self.weight: self.Liquidate(held)
        for sym, w in self.weight.items():
            if sym in data and data[sym]: self.SetHoldings(sym, 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"))

Asset-Class Momentum: ETF Rotational System

For cross-asset allocation, the repository provides a simpler rotational model that ranks static ETF universes by momentum and holds the top three.

Using ROC Instead of Rolling Windows

In static/strategies/asset-class-momentum-rotational-system.py (lines 24‑25), the algorithm uses the built-in ROC (Rate-of-Change) indicator rather than manual RollingWindow management. Lean automatically handles the 12-month lookback: self.ROC(sym, self.period, Resolution.Daily).

Simple Calendar-Based Rebalancing

Instead of Schedule.On, this implementation checks self.Time.month against a stored recent_month variable to trigger rebalancing once per calendar month (lines 32‑36). After skipping the warm-up phase via self.IsWarmingUp, it ranks symbols by their current ROC value, liquidates any holding not in the new top-three list, and equal-weights the survivors with SetHoldings(sym, 1.0 / len(long_assets)).


# File: static/strategies/asset-class-momentum-rotational-system.py

from AlgorithmImports import *

class MomentumAssetAllocationStrategy(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100_000)
        self.period = 12 * 21                      # 12‑month ROC

        self.symbols = ["SPY", "EFA", "IEF", "VNQ", "GSG"]
        self.data = {}
        for sym in self.symbols:
            self.AddEquity(sym, Resolution.Daily)
            self.data[sym] = self.ROC(sym, self.period, Resolution.Daily)
        self.recent_month = -1                     # force first rebalance

    def OnData(self, data):
        if self.IsWarmingUp: return
        if self.Time.month == self.recent_month: return
        self.recent_month = self.Time.month
        ranked = sorted([(sym, ind.Current.Value) for sym, ind in self.data.items()
                         if ind.IsReady and sym in data and data[sym]],
                        key=lambda kv: kv[1], reverse=True)
        long_assets = [sym for sym, _ in ranked[:3]]
        for held in [p.Key.Value for p in self.Portfolio if p.Value.Invested]:
            if held not in long_assets: self.Liquidate(held)
        for sym in long_assets:
            self.SetHoldings(sym, 1.0 / len(long_assets))

Extending to Commodities, Currencies, and Multi-Factor Models

The same architectural primitives apply to other asset classes. The repository includes:

Each file inherits from QCAlgorithm, sets a warm-up period, and schedules monthly rebalances, allowing you to benchmark against the Sharpe ratios documented in the repository’s README.

Running the Backtest Locally

To execute any strategy:

  1. Clone the repository: git clone https://github.com/paperswithbacktest/awesome-systematic-trading.git
  2. Install the Lean CLI: pip install lean
  3. Run the backtest: lean backtest static/strategies/momentum-factor-effect-in-stocks.py

Lean automatically downloads required data, applies the custom fee model, and generates performance reports including Sharpe ratio and maximum drawdown.

Summary

  • Inherit from QCAlgorithm and use AlgorithmImports to access Lean base classes for any momentum strategy.
  • Choose your indicator: Use RollingWindow for equity universes requiring manual price history, or ROC for simpler asset-class rotation.
  • Schedule monthly rebalancing via Schedule.On for explicit control or calendar checks for lightweight implementations.
  • Apply transaction costs by attaching a CustomFeeModel to avoid inflated backtest returns.
  • Validate against published metrics in the README, such as the equity UMD strategy’s reported Sharpe of 0.594.

Frequently Asked Questions

What is the difference between RollingWindow and ROC for momentum calculation?

RollingWindow stores a fixed-length array of historical prices that you manually populate and query, giving you full control over the calculation (e.g., window[0] / window[-1] - 1). ROC is a built-in Lean indicator that automatically computes the rate-of-change over the specified period and exposes the current value via .Current.Value. Use RollingWindow when you need custom logic or warm-up control; use ROC for cleaner code when standard momentum suffices.

How do I adjust the lookback period for momentum signals?

Change the self.period variable defined in Initialize. The repository standard uses 12 * 21 (approximately 252 trading days), but you can set any integer value. Ensure you also adjust self.SetWarmUp(self.period) so indicators are ready before trading begins.

Can I combine multiple asset class momentum strategies into one portfolio?

Yes. Because each strategy is a self-contained class inheriting from QCAlgorithm, you can either import multiple strategy classes into a single master algorithm and allocate capital between them, or run them sequentially with the Lean CLI and merge the equity curves offline. The repository’s value-and-momentum-factors-across-asset-classes.py demonstrates how to rank assets using multiple factors within a single algorithm.

What fee model is used in these backtests?

The strategies attach a CustomFeeModel that charges 0.5 basis points (0.00005) multiplied by the trade value. This is implemented as a nested class inside the algorithm (see lines 17‑21 in momentum-factor-effect-in-stocks.py) and applied to each security to approximate realistic transaction costs without requiring external broker fee tables.

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 →