Overall Statistics
Total Orders
1064
Average Win
0.51%
Average Loss
-0.25%
Compounding Annual Return
8.354%
Drawdown
10.600%
Expectancy
1.088
Start Equity
1000000
End Equity
4530587.97
Net Profit
353.059%
Sharpe Ratio
0.722
Sortino Ratio
0.87
Probabilistic Sharpe Ratio
67.124%
Loss Rate
31%
Win Rate
69%
Profit-Loss Ratio
2.01
Alpha
0.034
Beta
0.063
Annual Standard Deviation
0.052
Annual Variance
0.003
Information Ratio
-0.185
Tracking Error
0.161
Treynor Ratio
0.603
Total Fees
$15529.45
Estimated Strategy Capacity
$0
Lowest Capacity Asset
IEF SGNKIKYGE9NP
Portfolio Turnover
0.84%
Drawdown Recovery
494
from AlgorithmImports import *

class MinVar(QCAlgorithm):

    def Initialize(self):
        # Backtest period
        self.set_start_date(2007, 3, 1)

        self.set_cash(1_000_000)

        # Set the brokerage model and account type
        self.set_brokerage_model(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)

        # Equities
        self.spy = self.add_equity("SPY", Resolution.DAILY).symbol
        self.ief = self.add_equity("IEF", Resolution.DAILY).symbol
        self.gld = self.add_equity("GLD", Resolution.DAILY).symbol
        self.uup = self.add_equity("UUP", Resolution.DAILY).symbol
        self.dbc = self.add_equity("DBC", Resolution.DAILY).symbol

        self.set_benchmark(self.spy)
        
        # Volatility target
        self.target_vol = 0.06 # 6% annualized
        
        # Maximum allowed leerage
        self.max_leverage = 2.0
        
        # Lookback window (days)
        self.lookback = 90

        # Warm-up period so that prices are available
        self.set_warm_up(self.lookback)

        # Monthly rebalance
        self.schedule.on(
            self.date_rules.month_start(self.spy),
            self.time_rules.after_market_open(self.spy, 30),
            self.Rebalance)

    def Rebalance(self):

        if self.is_warming_up: return

        # Get historical prices
        prices = self.history([self.spy, self.ief, self.gld, self.uup, self.dbc],
            self.lookback, Resolution.DAILY)
        if prices.empty: return

        # Pivot to price dataframe
        prices = prices.close.unstack(level=0)

        # Compute daily returns
        returns = prices.pct_change().dropna()

        # Volatility and the covariance matrix
        mu = np.ones(len(returns.columns))
        vol = returns.std() * np.sqrt(252.)
        cov = returns.cov() * 252.

        # Min-Var Weights
        try:
            weights = np.linalg.solve(cov, mu)
        except:
            return
        weights = np.nan_to_num(weights)
        weights = pd.Series(weights, index = returns.columns)
        weights[weights < 0] = 0.
        s = weights.abs().sum()
        if s > 0:
            weights /= s

        # Portfolio annualized volatility
        port_vol = np.sqrt(weights.T @ cov @ weights)

        # Apply vol and leverage control
        leverage = 1.
        if port_vol > 0:
            leverage = min(self.target_vol / port_vol, self.max_leverage)
        weights *= leverage

        # Set holdings
        targets = [
            PortfolioTarget(symbol, float(weight))
            for symbol, weight in weights.items()
            if self.securities[symbol].is_tradable
            and self.securities[symbol].has_data
            and self.securities[symbol].price > 0
        ]

        self.set_holdings(targets)