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