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

> Backtest momentum factor strategies across asset classes like equities, ETFs, and commodities. Learn to use QuantConnect Lean with ROC and RollingWindow for 12-month lookbacks and monthly rebalances.

- Repository: [Papers With Backtest/awesome-systematic-trading](https://github.com/paperswithbacktest/awesome-systematic-trading)
- Tags: how-to-guide
- Published: 2026-08-02

---

**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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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.

```python

# 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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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))`.

```python

# 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:

- **Commodities**: [`static/strategies/momentum-effect-in-commodities.py`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/static/strategies/momentum-effect-in-commodities.py) uses `self.ROC` on commodity futures or ETFs.
- **FX**: [`static/strategies/currency-momentum-factor.py`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/static/strategies/currency-momentum-factor.py) applies momentum ranking to currency pairs.
- **Value + Momentum**: [`static/strategies/value-and-momentum-factors-across-asset-classes.py`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/static/strategies/value-and-momentum-factors-across-asset-classes.py) demonstrates dual-factor ranking, combining momentum with valuation metrics.

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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/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`](https://github.com/paperswithbacktest/awesome-systematic-trading/blob/main/momentum-factor-effect-in-stocks.py)) and applied to each security to approximate realistic transaction costs without requiring external broker fee tables.