Here is a FactorMinVariance portfolio model that calculates the minimum variance of a portfolio based on a set of factors. When I try to trade based on the weights, I can see that QuantConnect is not trading based on my weights but rather something different.
Code: from AlgorithmImports import *
import numpy as np
import pandas as pd
from sklearn.covariance import LedoitWolf
from datetime import timedelta
class FactorMinVariancePortfolio(QCAlgorithm):
"""
Factor-based long-only minimum variance portfolio.
Trades 4 ETFs using factor exposures from SPY and BND.
Uses Ledoit-Wolf shrinkage for factor covariance estimation.
Rebalances roughly every 100 days using a 1-year lookback of daily data.
"""
def initialize(self):
"""
Initialize the algorithm, including assets, factors, portfolio settings, and parameters.
"""
self.set_start_date(2018, 1, 1)
self.set_cash(100000)
self.Settings.MinimumOrderMarginPortfolioPercentage = 0
self.universe_settings.resolution = Resolution.DAILY # Use daily data for all assets
# -----------------------------
# Add traded assets
# -----------------------------
asset_tickers = ["TLT", "GLD", "XLE", "QQQ"]
self.asset_symbols = [
self.add_equity(t, Resolution.DAILY).symbol
for t in asset_tickers
]
# -----------------------------
# Add factor proxies (not traded)
# -----------------------------
factor_tickers = ["SPY", "BND"]
self.factor_symbols = [
self.add_equity(t, Resolution.DAILY).symbol
for t in factor_tickers
]
# -----------------------------
# Parameters
# -----------------------------
self.lookback = 252 # Use 1 year of daily data
self.rebalance_interval = timedelta(days=100) # Rebalance roughly every 4 months
self.last_rebalance = datetime.min
self.epsilon = 1e-6 # Small ridge for numerical stability
self.factor_exposure_limit = 0.5 # Not enforced yet, placeholder for later
def on_data(self, data: Slice):
"""
Rebalance the portfolio if the rebalance interval has passed.
Steps:
1. Fetch historical prices and factor proxies.
2. Compute asset and factor returns.
3. Estimate factor betas (B) via OLS and residuals.
4. Compute total covariance matrix (factor + idiosyncratic).
5. Compute long-only minimum variance weights.
6. Execute trades according to computed weights.
7. Log key diagnostics.
"""
# -----------------------------
# Rebalance check
# -----------------------------
if self.time - self.last_rebalance < self.rebalance_interval:
return
self.last_rebalance = self.time
# -----------------------------
# Fetch historical data
# -----------------------------
symbols = self.asset_symbols + self.factor_symbols
history = self.history(symbols, self.lookback, Resolution.DAILY)
if history.empty:
return
prices = history.close.unstack(level=0)
returns = prices.pct_change().dropna()
if len(returns) < 100:
# Ensure enough data points
return
# -----------------------------
# Separate asset and factor returns
# -----------------------------
asset_returns = returns[self.asset_symbols].to_numpy()
factor_returns = returns[self.factor_symbols].to_numpy()
T, N = asset_returns.shape
K = factor_returns.shape[1]
# -----------------------------
# Estimate factor betas (B) via OLS and residuals
# Model: r_i = alpha + B_i * f + epsilon
# -----------------------------
X = np.column_stack([np.ones(T), factor_returns])
B = np.zeros((N, K))
residuals = np.zeros((N, T))
for i in range(N):
y = asset_returns[:, i]
beta = np.linalg.lstsq(X, y, rcond=None)[0]
B[i, :] = beta[1:] # Factor loadings
residuals[i, :] = y - X @ beta # Idiosyncratic component
# -----------------------------
# Compute factor covariance (Ledoit-Wolf shrinkage)
# -----------------------------
lw_f = LedoitWolf().fit(factor_returns)
Sigma_f = lw_f.covariance_
# -----------------------------
# Compute idiosyncratic covariance
# -----------------------------
idio_var = np.var(residuals, axis=1)
idio_var = np.maximum(idio_var, 1e-6) # Avoid zeros
Sigma_eps = np.diag(idio_var)
# -----------------------------
# Total covariance matrix: factor + idiosyncratic + small ridge
# -----------------------------
Sigma = B @ Sigma_f @ B.T + Sigma_eps
Sigma += self.epsilon * np.eye(N)
# -----------------------------
# Compute long-only minimum variance weights
# -----------------------------
w_closed = np.linalg.pinv(Sigma) @ np.ones(N)
w_closed = np.maximum(w_closed, 0) # Enforce long-only
w_closed /= w_closed.sum() # Normalize to sum to 1
self.Debug(f"Closed-form min-var weights (long-only): {w_closed}")
# -----------------------------
# Execute trades according to computed weights
# -----------------------------
for i, symbol in enumerate(self.asset_symbols):
self.set_holdings(symbol, float(w_closed[i]))
# -----------------------------
# Diagnostics: asset volatilities and factor exposures
# -----------------------------
asset_vols = np.sqrt(np.diag(Sigma))
self.Debug(f"Asset volatilities: {asset_vols}")
self.Debug(f"B matrix (factor exposures):\n{B}")
# -----------------------------
# Post-trade holdings weights
# -----------------------------
holdings_array = np.array([
self.portfolio[symbol].quantity * self.portfolio[symbol].price
for symbol in self.asset_symbols
])
holdings_weights = holdings_array / holdings_array.sum()
self.Debug(f"Post-trade holdings weights: {holdings_weights}")
Cloud terminal output example:
2025-09-15 16:00:00 Closed-form min-var weights (long-only): [0.26300731 0.37590958 0.1574632 0.2036199 ]
2025-09-15 16:00:00 Post-trade holdings weights: [0.29478935 0.36385326 0.20552039 0.135837 ]
If you have any thoughts on how to fix this, please let me know
Jaime Mizrahi
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!