How to Implement a Low-Volatility Factor Strategy in Stocks: A QuantConnect Implementation Guide
To implement a low-volatility factor strategy in stocks, build a dynamic universe of liquid large-cap equities, compute the standard deviation of weekly returns over three years for each candidate, and hold an equal-weighted long-only portfolio of the lowest-volatility quartile, rebalancing monthly.
The low-volatility factor strategy capitalizes on the empirical anomaly where less volatile stocks often deliver superior risk-adjusted returns compared to high-risk counterparts. According to the paperswithbacktest/awesome-systematic-trading repository, this defensive equity approach is implemented as a QCAlgorithm subclass in static/strategies/low-volatility-factor-effect-in-stocks.py, utilizing a two-stage universe selection process and rolling-window volatility calculations.
Architecture Overview
The implementation follows a structured monthly workflow that separates universe construction from execution. The algorithm defines a coarse-fine universe selection pattern to filter for liquidity, stores three years of daily price history in rolling windows, and triggers rebalancing at month-end to capture the lowest-volatility quartile.
| Component | Implementation Details | Source Location |
|---|---|---|
| Universe Definition | Dynamic coarse and fine selection callbacks | Lines 33, 41-89 |
| Volatility Calculation | 3-year weekly return standard deviation | Lines 111-128 |
| Rebalancing Schedule | Monthly execution after market open | Line 34 |
| Portfolio Weighting | Equal-weight allocation to selected securities | Line 104 |
Step 1: Universe Selection and Data Preparation
The strategy begins by defining a liquid investment universe through QuantConnect's coarse-fine selection framework.
Coarse Selection for Liquidity
The CoarseSelectionFunction (lines 41-72) filters the entire US equity market for investable candidates. It updates price buffers for all symbols daily and, when the monthly selection flag is active, returns up to 3,000 securities with fundamental data and USD pricing.
For each newly encountered symbol, the algorithm initializes a SymbolData instance and pre-fills it with historical daily closes (lines 60-71). This establishes a rolling window of 252 trading days (12 * 21), representing approximately three years of data required for volatility calculation.
Fine Selection for Volatility Ranking
The FineSelectionFunction (lines 74-89) refines the coarse universe by market capitalization and volatility metrics. It first drops securities with zero market cap (line 75), then limits the candidate pool to the top 3,000 by market cap (lines 78-81).
For each remaining symbol, the algorithm retrieves the stored price series via self.data[x.Symbol].volatility() and computes the volatility metric. It then sorts symbols by volatility descending, calculates the quartile boundary (int(len(sorted_by_vol) / 4)), and stores the lowest-volatility 25% in self.long (lines 85-88).
Step 2: Calculating Three-Year Weekly Volatility
The volatility computation resides in the SymbolData class (lines 111-128), which maintains a RollingWindow[float] of daily closing prices.
The volatility() method (lines 121-127) groups the stored daily closes into weekly chunks (5 trading days per week), calculates weekly percentage returns, and returns the standard deviation of those returns. This weekly granularity smooths daily noise while maintaining sensitivity to medium-term price swings:
class SymbolData:
def __init__(self, symbol, lookback):
self.Symbol = symbol
self.window = RollingWindow[float](lookback) # 12*21 days
def volatility(self):
# Groups into weeks (5 days) and calculates std dev of weekly returns
closes = list(self.window)
if len(closes) < 60: # Ensure sufficient data
return float('inf')
weekly_returns = []
for i in range(0, len(closes)-5, 5):
week_return = (closes[i+5] - closes[i]) / closes[i]
weekly_returns.append(week_return)
return np.std(weekly_returns) if weekly_returns else float('inf')
Step 3: Portfolio Execution and Rebalancing
Trade execution occurs in the OnData method (lines 91-107), which responds to price updates but only acts when self.selection_flag is true (set monthly by the scheduled event).
The logic follows three steps:
- Liquidate exiting positions: Sells any holdings that no longer appear in the
self.longlist - Calculate target weight: Determines equal-weight allocation (1/N) for the selected securities
- Open new positions: Submits market orders for each symbol in the low-volatility quartile (line 104)
The rebalancing schedule is set in Initialize (line 34) using Schedule.On to trigger at month-end, ensuring the portfolio updates immediately after the volatility ranking completes.
Key Implementation Details
Transaction Costs: The algorithm applies a CustomFeeModel (lines 129-133) charging 0.5 basis points per trade, simulating realistic execution friction without complicating the backtest logic.
Defensive Coding: The implementation guards against insufficient data by returning float('inf') for volatility when the rolling window contains fewer than 60 observations, effectively excluding illiquid or newly listed securities from selection.
Running the Strategy in QuantConnect
To deploy this low-volatility factor strategy, create a new algorithm in the QuantConnect IDE and paste the complete implementation from low-volatility-factor-effect-in-stocks.py. The class inherits from QCAlgorithm and requires no additional configuration:
# In QuantConnect Research or Algorithm Lab
class LowVolatilityFactorEffectStocks(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2000, 1, 1)
self.SetCash(100000)
self.UniverseSettings.Resolution = Resolution.Daily
# Add SPY for scheduling benchmark
self.AddEquity("SPY", Resolution.Daily)
# Rolling window length: ~3 years (12 months * 21 trading days)
self.lookback = 12 * 21
# Universe selection
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd("SPY"),
self.TimeRules.AfterMarketOpen("SPY", 1),
self.Selection)
self.coarse_count = 3000
self.selection_flag = False
self.data = {}
Execution Steps:
- Import the full class definition from the repository file into the QuantConnect editor
- Ensure the
AlgorithmImportsnamespace is available (provided by the platform) - Run backtests starting from 2000 to capture multiple market cycles
- Monitor the universe count and turnover metrics to verify the quartile selection logic
Summary
- Universe Construction: Use
CoarseSelectionFunctionandFineSelectionFunctionto filter 3,000 liquid US large-cap equities monthly - Volatility Metric: Compute the standard deviation of weekly returns over a three-year (252-day) rolling window via the
SymbolDataclass - Security Selection: Target the lowest-volatility quartile (bottom 25%) of the filtered universe
- Portfolio Management: Maintain equal-weight allocations, liquidate positions that fall out of the low-volatility set, and rebalance monthly using
Schedule.On - Implementation Source: Reference
paperswithbacktest/awesome-systematic-trading/static/strategies/low-volatility-factor-effect-in-stocks.pyfor the complete production code
Frequently Asked Questions
How does the strategy define and calculate volatility?
The strategy defines volatility as the standard deviation of weekly returns calculated over a rolling three-year window. In the SymbolData class (lines 121-127), daily closing prices are grouped into five-day weeks, percentage returns are computed for each week, and numpy.std calculates the dispersion. This weekly approach reduces noise from daily price fluctuations while capturing medium-term price variability.
Why does the algorithm use a 12*21 rolling window length?
The calculation 12 * 21 (252 trading days) approximates three years of historical data assuming 21 trading days per month. This lookback period provides sufficient sample size (approximately 156 weekly observations) to calculate statistically robust volatility estimates while remaining responsive to changing market conditions. The window updates daily in the CoarseSelectionFunction (lines 42-49).
What is the difference between coarse and fine selection in this implementation?
Coarse selection (lines 41-72) filters the entire US equity database for price and dollar volume metrics, requiring fundamental data availability and USD denomination. Fine selection (lines 74-89) receives the coarse candidates and applies additional filters like market capitalization thresholds before computing the volatility metrics. This two-stage process optimizes computational efficiency by running expensive calculations only on the most liquid securities.
How often does the portfolio rebalance and why monthly?
The portfolio rebalances monthly using Schedule.On configured for month-end (line 34). Monthly rebalancing strikes a balance between capturing recent volatility regime changes and minimizing transaction costs. Frequent rebalancing (weekly or daily) would increase turnover and fees, while quarterly rebalancing might delay entry into defensive positions during rapidly changing volatility environments.
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 →