How to Implement a Sector Momentum Rotational System in QuantConnect

A sector momentum rotational system ranks sector ETFs by 12‑month price momentum, holds an equally‑weighted portfolio of the top three performers, and rebalances monthly to capture persistent relative strength across business cycles.

The sector momentum rotational system is a quantitative strategy that exploits the persistence of relative strength among sector ETFs. According to the implementation in the paperswithbacktest/awesome-systematic-trading repository, this approach selects the strongest sectors based on 12‑month Rate‑of‑Change (ROC) and rotates capital monthly into the top three performers.

Core Architecture of the Sector Momentum Strategy

Algorithm Class and Initialization

The SectorMomentumAlgorithm class inherits from QCAlgorithm and defines the lifecycle methods Initialize, OnData, and OnROCUpdated. In Initialize, the algorithm sets a warm‑up period of approximately 252 trading days (self.period = 12 * 21) to populate the momentum indicators before trading begins.

The strategy tracks ten sector ETFs: VNQ, XLK, XLE, XLV, XLF, XLI, XLB, XLY, XLP, and XLU. Each symbol receives a daily ROC calculation stored in the self.data dictionary.

Momentum Indicator Setup

The algorithm utilizes self.ROC(symbol, self.period, Resolution.Daily) to compute 12‑month momentum for each ETF. These indicators attach to the daily price stream and update automatically. The system subscribes to the first symbol’s Updated event to detect calendar transitions efficiently.

Monthly Rebalance Trigger

The OnROCUpdated method detects month changes by comparing self.Time.month against a stored self.recent_month variable. When the month changes, it sets self.rebalance_flag = True, signaling that rebalancing should occur on the next data tick.

Step-by-Step Implementation

1. Initialize the Algorithm and Attach Indicators

class SectorMomentumAlgorithm(QCAlgorithm):
    def Initialize(self):
        self.SetStartDate(2000, 1, 1)
        self.SetCash(100_000)

        # period = 12 months (≈252 trading days)

        self.period = 12 * 21
        self.SetWarmUp(self.period)

        # Ten sector ETFs

        self.symbols = [
            "VNQ", "XLK", "XLE", "XLV", "XLF",
            "XLI", "XLB", "XLY", "XLP", "XLU",
        ]

        # Attach ROC indicator and custom fee model

        self.data = {}
        for symbol in self.symbols:
            equity = self.AddEquity(symbol, Resolution.Daily)
            equity.SetFeeModel(CustomFeeModel())
            equity.SetLeverage(5)
            self.data[symbol] = self.ROC(symbol, self.period, Resolution.Daily)

        # Subscribe to the first ROC to know when a new month begins

        self.data[self.symbols[0]].Updated += self.OnROCUpdated
        self.recent_month = -1
        self.rebalance_flag = False

2. Detect Month Changes

def OnROCUpdated(self, sender, updated):
    if self.recent_month != self.Time.month:
        self.recent_month = self.Time.month
        self.rebalance_flag = True

3. Execute Monthly Rebalance Logic

def OnData(self, data):
    if self.IsWarmingUp:
        return

    if self.rebalance_flag:
        self.rebalance_flag = False

        # Rank ETFs by 12‑month ROC

        ranked = sorted(
            [(sym, roc) for sym, roc in self.data.items()
             if roc.IsReady and sym in data and data[sym]],
            key=lambda tup: tup[1].Current.Value,
            reverse=True,
        )
        top_three = [sym for sym, _ in ranked[:3]]

        # Liquidate losers

        for holding in [h for h in self.Portfolio if self.Portfolio[h].Invested]:
            if holding not in top_three:
                self.Liquidate(holding)

        # Equal‑weight winners

        weight = 1.0 / len(top_three)
        for sym in top_three:
            self.SetHoldings(sym, weight)

4. Model Realistic Transaction Costs

class CustomFeeModel(FeeModel):
    def GetOrderFee(self, parameters):
        fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
        return OrderFee(CashAmount(fee, "USD"))

Key Implementation Details

Security Selection and Ranking

The algorithm filters for ready indicators using roc.IsReady and sorts the universe by Current.Value in descending order. It selects the top three symbols and liquidates any existing holdings not in this ranked list. This ensures the portfolio always holds the highest‑momentum sectors while eliminating relative underperformers.

Position Sizing and Execution

Winning sectors receive equal weight allocations calculated as weight = 1.0 / len(top_three), typically 33.33% per position. The SetHoldings method executes these target allocations proportionally to portfolio value, while Liquidate closes positions in falling sectors immediately.

Transaction Cost Modeling

The CustomFeeModel applies a realistic commission of 0.5 basis points per share using the formula price × quantity × 0.00005. This accounts for friction costs that erode momentum profits, particularly important given the strategy’s monthly turnover.

Source Code Reference

The complete implementation resides in sector-momentum-rotational-system.py within the static/strategies/ directory of the repository. This file contains the full SectorMomentumAlgorithm class and can be deployed directly to QuantConnect or local Lean environments without modification.

Summary

Frequently Asked Questions

Why use a 12‑month lookback period for momentum?

The 12‑month (approximately 252 trading days) lookback captures the "intermediate-term" momentum anomaly documented in academic literature. This period is long enough to filter out short‑term noise while identifying persistent trends in sector performance that typically last several months.

How does the algorithm handle the monthly rebalancing timing?

The OnROCUpdated callback monitors the ROC indicator of the first symbol in the universe. When self.Time.month differs from the stored self.recent_month, it triggers self.rebalance_flag. The actual execution occurs in the next OnData iteration, ensuring all indicators are updated with the latest closing prices before ranking and trading.

What is the impact of the CustomFeeModel on strategy performance?

The CustomFeeModel charges 0.5 basis points per share traded, which translates to roughly 0.005% of trade value. While sector ETFs are relatively liquid, monthly rotation among three positions generates approximately 36 round‑trip trades annually, making realistic commission modeling essential for accurate Sharpe ratio estimation (reported at approximately 0.40 in the repository documentation).

Can this rotational system be adapted for other asset classes?

Yes. The architecture supports any liquid instruments with price history. Replace the sector ETF symbols in self.symbols with asset classes such as international equities, fixed income ETFs, or commodity pools. Ensure you adjust self.period if using different timeframe momentum (e.g., 6‑month for faster rotation) and maintain sufficient warm‑up data for indicator calculation.

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 →