How to Implement Sector Momentum Rotational Systems in QuantConnect
A sector momentum rotational system ranks sector ETFs by 12‑month price momentum, holds an equal‑weighted basket of the top three performers, and rebalances monthly to capture persistent sector trends.
The sector momentum rotational system is a quantitative strategy that exploits the tendency of strong‑performing sectors to maintain outperformance over intermediate time horizons. This article breaks down a complete implementation found in the paperswithbacktest/awesome-systematic-trading repository, demonstrating how to code this rotational logic using QuantConnect's Lean engine.
Core Architecture of the Rotational System
The implementation in sector-momentum-rotational-system.py follows a modular design centered on four primary components. Each element handles a specific aspect of the rotational workflow, from momentum calculation to monthly execution.
Algorithm Class (SectorMomentumAlgorithm): This is the main QCAlgorithm that manages the strategy lifecycle through Initialize, OnData, and OnROCUpdated methods. It configures the backtest environment, loads the universe of ten sector ETFs, and maintains the data dictionary storing momentum indicators.
Momentum Indicator (self.ROC): The system calculates the 12‑month Rate‑of‑Change (approximately 252 trading days) for each ETF using self.ROC(symbol, self.period, Resolution.Daily). These indicators are stored in self.data and updated daily to track relative strength.
Monthly Rebalance Trigger (OnROCUpdated): Rather than using a simple scheduled event, the algorithm detects calendar month transitions by monitoring the first ETF's ROC updates. When self.Time.month changes from the stored self.recent_month, it sets self.rebalance_flag = True.
Rebalance Logic (OnData): When the flag is active, the algorithm sorts all ETFs by their current ROC value, identifies the top three performers, liquidates any holdings outside this group, and calls SetHoldings to allocate exactly one‑third of the portfolio to each selected ETF.
Custom Fee Model (CustomFeeModel): To simulate realistic trading costs, the implementation applies a 0.5 basis point commission per share through a custom FeeModel subclass that calculates fees as price × quantity × 0.00005.
Step-by-Step Implementation Guide
Initialization and Warm-Up
The Initialize method sets the stage by defining the backtest parameters and loading the sector universe. The algorithm targets ten diverse sector ETFs: VNQ (Real Estate), XLK (Technology), XLE (Energy), XLV (Health Care), XLF (Financials), XLI (Industrials), XLB (Materials), XLY (Consumer Discretionary), XLP (Consumer Staples), and XLU (Utilities).
class SectorMomentumAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100_000)
# 12 months ≈ 252 trading days
self.period = 12 * 21
self.SetWarmUp(self.period)
self.symbols = [
"VNQ", "XLK", "XLE", "XLV", "XLF",
"XLI", "XLB", "XLY", "XLP", "XLU",
]
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 month-change detection
self.data[self.symbols[0]].Updated += self.OnROCUpdated
self.recent_month = -1
self.rebalance_flag = False
The warm‑up period ensures the ROC indicators have sufficient historical data before generating signals, preventing premature trades based on incomplete momentum calculations.
Monthly Signal Detection
The OnROCUpdated method acts as a coarse timer, triggering rebalancing only when the calendar month changes. This approach ensures trades occur at the beginning of each month while leveraging the indicator's update mechanism.
def OnROCUpdated(self, sender, updated):
if self.recent_month != self.Time.month:
self.recent_month = self.Time.month
self.rebalance_flag = True
Rebalancing Logic
The core rotational logic executes in OnData when rebalance_flag is active. The algorithm filters for ready indicators, ranks symbols by momentum, and executes the portfolio transition.
def OnData(self, data):
if self.IsWarmingUp:
return
if self.rebalance_flag:
self.rebalance_flag = False
# Rank 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 positions not in top 3
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 allocation
weight = 1.0 / len(top_three)
for sym in top_three:
self.SetHoldings(sym, weight)
Commission Structure and Realism
The CustomFeeModel class ensures backtests account for transaction costs, which significantly impact rotational strategies with monthly turnover.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))
Running the Backtest
To deploy this sector momentum rotational system:
- Copy the complete
sector-momentum-rotational-system.pyfile from thepaperswithbacktest/awesome-systematic-tradingrepository. - Upload to the QuantConnect IDE or a local Lean CLI environment.
- Adjust the
SetStartDateandSetCashparameters inInitializeto match your desired backtest period and initial capital. - Execute the backtest to generate performance metrics—the original implementation reports a Sharpe ratio of approximately 0.40 with monthly rebalancing across the ten‑sector universe.
Summary
- Sector momentum rotational systems select the top three performing sector ETFs based on 12‑month price momentum and rebalance monthly to maintain exposure to strength.
- The implementation uses QuantConnect's
ROCindicator with a 252‑day lookback period to measure relative momentum across ten sector ETFs. - Monthly rebalancing is triggered by detecting calendar month changes in the
OnROCUpdatedevent handler rather than using scheduled universes. - Equal‑weighting (33.3% per position) reduces concentration risk while maintaining pure momentum exposure.
- The
CustomFeeModelapplies realistic 0.5 basis point commissions per share to ensure backtest accuracy.
Frequently Asked Questions
What lookback period works best for sector momentum?
The 12‑month (252 trading day) lookback period is standard for sector momentum strategies because it captures persistent trends while filtering out short‑term noise. The implementation in sector-momentum-rotational-system.py uses self.period = 12 * 21 to approximate one year of trading data.
Why equal‑weight the top three sectors rather than cap‑weight?
Equal‑weighting (1.0 / len(top_three)) ensures each selected sector contributes equally to portfolio performance regardless of market capitalization. This approach prevents a single large sector from dominating risk exposure when it exhibits strong momentum.
How does the algorithm handle the warm‑up period?
The SetWarmUp(self.period) call instructs QuantConnect to pipe historical data through the indicators without executing trades until the 252‑day ROC series is fully populated. During this phase, IsWarmingUp returns true, blocking the rebalance logic in OnData.
Can I modify this for different sector ETFs or international markets?
Yes. Replace the symbols list in Initialize with your target ETF universe—such as international sector ETFs or country indices—while maintaining the same ROC indicator structure. Ensure sufficient historical data exists for the 12‑month calculation period.
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 →