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
- The strategy measures 12‑month price momentum using ROC indicators for ten sector ETFs.
- Monthly rebalancing selects the top three performers and allocates equal capital to each.
- The
OnROCUpdatedmethod ensures rebalancing occurs only on the first trading day of each new month. - A custom fee model simulates 0.5 bps commissions to improve backtest realism.
- Full source code is available at
paperswithbacktest/awesome-systematic-trading/static/strategies/sector-momentum-rotational-system.py.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →