Created with Highcharts 12.1.2EquityJun 10Jun 24Jul 8Jul 22Aug 5Aug 19Sep 2Sep 16Sep 30Oct 14Oct 28Nov 11Nov 25Dec 9Dec 23Jan 6750k1,000k1,250k-100012-1012010M20M020M40M750k1,000k1,250k050100
Overall Statistics
Total Orders
355
Average Win
0.37%
Average Loss
-0.60%
Compounding Annual Return
42.176%
Drawdown
9.400%
Expectancy
0.256
Start Equity
1000000
End Equity
1222889.53
Net Profit
22.289%
Sharpe Ratio
1.462
Sortino Ratio
1.683
Probabilistic Sharpe Ratio
70.054%
Loss Rate
22%
Win Rate
78%
Profit-Loss Ratio
0.61
Alpha
0.121
Beta
0.841
Annual Standard Deviation
0.163
Annual Variance
0.026
Information Ratio
0.741
Tracking Error
0.134
Treynor Ratio
0.283
Total Fees
$2932.91
Estimated Strategy Capacity
$3000000.00
Lowest Capacity Asset
ERX U7FBH5GTZQZP
Portfolio Turnover
20.11%
from AlgorithmImports import *
import numpy as np
from collections import deque
from datetime import timedelta

class MultiAssetPortfolioOptimization(QCAlgorithm):

    def Initialize(self):
        # 1) Backtest window & capital
        self.SetStartDate(2024, 6, 1)
        self.SetEndDate(2025, 1, 1)
        self.INIT_CASH = 1000000
        self.SetCash(self.INIT_CASH)  # Fixed: changed set_cash to SetCash

        # Initialization for the Buy and Hold Benchmark
        self.buy_and_hold_initialized = False
        self.buy_and_hold_shares = 0
        
        # 2) Set brokerage/benchmark
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
        self.SetBenchmark("SPY")
        
        # 3) Primary leveraged ETFs (2× leveraged, from different sectors)
        # SSO is now included as a sector rather than a rotation ETF.
        self.longAssets = {
            "TECH": self.AddEquity("QLD", Resolution.Daily).Symbol,    # Technology (Nasdaq-100)
            "ENERGY": self.AddEquity("ERX", Resolution.Daily).Symbol,    # Energy
            "IND": self.AddEquity("DNI", Resolution.Daily).Symbol,       # Industrials
            "SSO": self.AddEquity("SSO", Resolution.Daily).Symbol         # S&P 500 as a sector
        }
        
        # 4) Rotation hedge asset: TBF.
        self.tbfSymbol = self.AddEquity("TBF", Resolution.Daily).Symbol
        
        # 5) Define a candidate universe for the bond portfolio.
        self.bondCandidates = {
            "BIL": self.AddEquity("BIL", Resolution.Daily).Symbol,
            "SHY": self.AddEquity("SHY", Resolution.Daily).Symbol,
            "HYG": self.AddEquity("HYG", Resolution.Daily).Symbol
        }
        # For the TBF hedge, we assume a fixed duration.
        self.durationTBF = -17
        
        # 6) Additional assets for the valuation indicator:
        # SPY to extract forward earnings yield.
        self.spySymbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        # TNX as a proxy for the 10-year Treasury yield.
        self.tenYearSymbol = self.AddEquity("TNX", Resolution.Daily).Symbol
        # When the valuation indicator triggers, invest in 2× leveraged gold.
        self.goldSymbol = self.AddEquity("UGLD", Resolution.Daily).Symbol
        
        # 7) Indicators for individual signals:
        # 20-day moving average for each leveraged ETF.
        self.momentumMA = {}
        for symbol in self.longAssets.values():
            self.momentumMA[symbol] = self.SMA(symbol, 20, Resolution.Daily)
        # 20-day volume SMA for leveraged ETFs.
        self.volumeSMA = {}
        self.volumeMultiplierThreshold = 1.5
        for symbol in self.longAssets.values():
            self.volumeSMA[symbol] = self.SMA(symbol, 20, Resolution.Daily, Field.Volume)
        
        # SPY indicator for valuation:
        self.spyMA = self.SMA(self.spySymbol, 20, Resolution.Daily)
        
        # 8) Warm up so that all indicators are ready.
        self.SetWarmUp(200)
        
        # 9) Stop loss and take profit parameters.
        self.StopLossPctEquity = 0.04
        self.TakeProfitPctEquity = 0.2
        
        # 10) Data structures for tracking orders.
        self.ordersDict = {}
        self.entryPrices = {}
        
        # -------------------------------------------------------------------
        # Setup for inverse-vol weighting among leveraged ETFs.
        # -------------------------------------------------------------------
        self.volPeriod = 20  # rolling window for daily returns
        self.priceHistory = {}
        self.previousCloses = {}
        for symbol in self.longAssets.values():
            self.InitializeDailyReturnTracking(symbol)
        
        self.Debug("Initialization Complete")
    
    
    # --------------------- DAILY RETURN TRACKING ----------------------------
    def InitializeDailyReturnTracking(self, symbol):
        self.priceHistory[symbol] = deque(maxlen=self.volPeriod)
        self.previousCloses[symbol] = 0
        consolidator = TradeBarConsolidator(timedelta(days=1))
        consolidator.DataConsolidated += lambda sender, bar: self.OnDailyBarConsolidated(bar, symbol)
        self.SubscriptionManager.AddConsolidator(symbol, consolidator)
    
    def OnDailyBarConsolidated(self, bar, symbol):
        prev_close = self.previousCloses[symbol]
        if prev_close != 0:
            daily_return = (bar.Close - prev_close) / prev_close
            self.priceHistory[symbol].append(daily_return)
        self.previousCloses[symbol] = bar.Close
    
    
    # --------------------- FUNDAMENTAL INDICATORS ----------------------------
    def GetForwardEarningsYield(self):
        """
        Retrieve the S&P 500 forward earnings yield using SPY's fundamental data.
        This example attempts to use CompanyReference.ForwardEarningsYield.
        """
        fundamentals = self.Securities[self.spySymbol].Fundamentals
        if fundamentals is not None and fundamentals.CompanyReference is not None:
            if hasattr(fundamentals.CompanyReference, "ForwardEarningsYield"):
                return fundamentals.CompanyReference.ForwardEarningsYield
        return None
    
    
    def Get10YearYield(self):
        """
        Retrieve the 10-year Treasury yield from the TNX asset.
        Assumes TNX price is in percentage points (e.g., 3.5 for 3.5%),
        converting it to a decimal.
        """
        if self.tenYearSymbol in self.Securities:
            price = self.Securities[self.tenYearSymbol].Price
            if price is not None:
                return price / 100.0
        return None
    
    
    # --------------------- MAIN TRADING LOGIC -------------------------------
    def OnData(self, data):
        if self.IsWarmingUp:
            return

        if not self.buy_and_hold_initialized:
            # Fixed: access Securities[spySymbol] instead of securities[self.symbol]
            self.buy_and_hold_shares = self.INIT_CASH / self.Securities[self.spySymbol].Price
            self.Log("Bought " + str(self.buy_and_hold_shares) + " shares")
            self.buy_and_hold_initialized = True

        self.UpdatePlot()
        
        # --- Valuation Indicator: Compare S&P 500 forward earnings yield and 10-year yield.
        forwardEY = self.GetForwardEarningsYield()
        tenYearYield = self.Get10YearYield()
        if forwardEY is not None and tenYearYield is not None:
            # Theory: if the forward earnings yield is lower than the 10-year yield,
            # stocks are overvalued. In that case, liquidate positions and invest in 2× gold.
            if forwardEY < tenYearYield:
                assetsToClear = list(self.longAssets.values()) + [self.tbfSymbol] + list(self.bondCandidates.values())
                for sym in assetsToClear:
                    if self.Portfolio[sym].Invested:
                        self.Liquidate(sym)
                if not self.Portfolio[self.goldSymbol].Invested:
                    self.SetHoldings(self.goldSymbol, 1.0)
                    self.Debug("Valuation indicator triggered: Forward earnings yield < 10-year yield. Investing in 2× Gold (UGLD).")
                return  # Exit OnData after switching to gold.
            else:
                # If not triggered, ensure any gold position is liquidated.
                if self.Portfolio[self.goldSymbol].Invested:
                    self.Liquidate(self.goldSymbol)
        
        # --- Next, evaluate individual leveraged ETF signals (long signals).
        bullishSymbols = []
        for symbol in self.longAssets.values():
            if symbol not in data or data[symbol] is None:
                continue
            price = data[symbol].Price
            if self.momentumMA[symbol].IsReady and self.volumeSMA[symbol].IsReady:
                if price > self.momentumMA[symbol].Current.Value:
                    bullishSymbols.append(symbol)
                else:
                    if self.Portfolio[symbol].Invested:
                        self.Liquidate(symbol)
                        self.CancelOrdersForSymbol(symbol)
            else:
                continue
        
        if bullishSymbols:
            # Liquidate rotation and bond positions.
            for sym in [self.tbfSymbol] + list(self.bondCandidates.values()):
                if self.Portfolio[sym].Invested:
                    self.Liquidate(sym)
            
            # Allocate 100% among the bullish signals using inverse volatility weighting.
            inv_vol_bullish = {}
            total_inv_vol_bullish = 0.0
            for symbol in bullishSymbols:
                returns = list(self.priceHistory[symbol])
                vol = np.std(returns, ddof=1) if len(returns) > 1 else 0.02
                if vol <= 0:
                    vol = 1e-6
                inv_vol = 1.0 / vol
                inv_vol_bullish[symbol] = inv_vol
                total_inv_vol_bullish += inv_vol
            
            for symbol in bullishSymbols:
                weight = inv_vol_bullish[symbol] / total_inv_vol_bullish
                self.SetHoldings(symbol, weight)
                if self.Portfolio[symbol].Invested and symbol not in self.ordersDict:
                    entryPrice = data[symbol].Price
                    self.entryPrices[symbol] = entryPrice
                    self.PlaceStopAndTakeProfit(symbol, entryPrice, True,
                                                self.StopLossPctEquity,
                                                self.TakeProfitPctEquity)
        else:
            # No bullish signals: use bond rotation strategy.
            self.RotateRotation(data)
    
    
    # --------------------- ROTATION LOGIC (BOND Rotation Only) -------------------------------
    def RotateRotation(self, data):
        """
        When no individual signals are present, use bond rotation strategy.
        """
        # Liquidate any positions in long sector assets.
        for symbol in self.longAssets.values():
            if self.Portfolio[symbol].Invested:
                self.Liquidate(symbol)
                self.CancelOrdersForSymbol(symbol)
        
        # Liquidate TBF if invested.
        if self.Portfolio[self.tbfSymbol].Invested:
            self.Liquidate(self.tbfSymbol)
        
        bondSymbol = self.SelectBond()
        if bondSymbol is None:
            return
        
        # Retrieve real bond duration from fundamentals.
        bondDuration = self.GetBondDuration(bondSymbol)
        if bondDuration is None or bondDuration <= self.durationTBF:
            self.Debug(f"Unable to get valid duration for {bondSymbol.Value}")
            return
        
        # Compute immunization weights:
        # w_bond * bondDuration + w_TBF * durationTBF = 0, with w_bond + w_TBF = 1.
        w_bond = -self.durationTBF / (bondDuration - self.durationTBF)
        w_tbf = bondDuration / (bondDuration - self.durationTBF)
        
        # Liquidate any bond positions not selected.
        for bond in self.bondCandidates.values():
            if bond != bondSymbol and self.Portfolio[bond].Invested:
                self.Liquidate(bond)
        
        self.SetHoldings(bondSymbol, w_bond)
        self.SetHoldings(self.tbfSymbol, w_tbf)
        self.Debug(f"Bond rotation: Selected {bondSymbol.Value} with duration {bondDuration:0.2f} years - Weights: Bond={w_bond:0.2%}, TBF={w_tbf:0.2%}")
    
    
    def SelectBond(self):
        """Select the bond ETF with the maximum yield using real fundamental data."""
        bestYield = -999
        bestBond = None
        for key, symbol in self.bondCandidates.items():
            bondYield = self.GetBondYield(symbol)
            if bondYield is not None and bondYield > bestYield:
                bestYield = bondYield
                bestBond = symbol
        return bestBond
    
    
    def GetBondYield(self, symbol):
        """Return the bond yield using real fundamental data from CompanyReference.
           Here we check for a DividendYield attribute as a proxy for yield."""
        fundamentals = self.Securities[symbol].Fundamentals
        if fundamentals is not None and fundamentals.CompanyReference is not None:
            if hasattr(fundamentals.CompanyReference, "DividendYield"):
                return fundamentals.CompanyReference.DividendYield
        return None
    
    
    def GetBondDuration(self, symbol):
        """Return the bond duration using real fundamental data from CompanyReference."""
        fundamentals = self.Securities[symbol].Fundamentals
        if fundamentals is not None and fundamentals.CompanyReference is not None:
            if hasattr(fundamentals.CompanyReference, "Duration"):
                return fundamentals.CompanyReference.Duration
        return None
    
    
    # ----------------- ORDER MANAGEMENT (STOP LOSS & TAKE PROFIT) -----------------------
    def PlaceStopAndTakeProfit(self, symbol, entryPrice, long, stopPct, tpPct):
        self.CancelOrdersForSymbol(symbol)
        if long:
            stopPrice = entryPrice * (1 - stopPct)
            tpPrice   = entryPrice * (1 + tpPct)
        else:
            stopPrice = entryPrice * (1 + stopPct)
            tpPrice   = entryPrice * (1 - tpPct)
        quantity = self.Portfolio[symbol].Quantity
        closeQty = -quantity
        stopTicket = self.StopMarketOrder(symbol, closeQty, stopPrice)
        tpTicket   = self.LimitOrder(symbol, closeQty, tpPrice)
        self.ordersDict[symbol] = {"stop": stopTicket, "tp": tpTicket}
        self.Debug(f"{symbol.Value}: Entry={entryPrice:.2f}, Stop={stopPrice:.2f}, TP={tpPrice:.2f}")
    
    def CancelOrdersForSymbol(self, symbol):
        if symbol in self.ordersDict:
            orders = self.ordersDict[symbol]
            if orders["stop"] is not None:
                self.Transactions.CancelOrder(orders["stop"].OrderId)
            if orders["tp"] is not None:
                self.Transactions.CancelOrder(orders["tp"].OrderId)
            del self.ordersDict[symbol]
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            for sym, orders in list(self.ordersDict.items()):
                if orderEvent.OrderId in [orders["stop"].OrderId, orders["tp"].OrderId]:
                    self.CancelOrdersForSymbol(sym)
                    self.Debug(f"Exit order filled for {sym.Value}, cancelled the other order.")


    def UpdatePlot(self):
        # Updating the Performance chart
        
        # Plot the total portfolio value
        self.Plot("Performance", "Total Value", self.Portfolio.TotalPortfolioValue)
        # Plot the benchmark - Fixed: access spySymbol instead of self.symbol
        if self.buy_and_hold_initialized:
            benchmark = self.Securities[self.spySymbol].Price
            self.Plot("Performance", "Buy and Hold", benchmark * self.buy_and_hold_shares)