Overall Statistics
Total Orders
463
Average Win
1.26%
Average Loss
-1.14%
Compounding Annual Return
14.119%
Drawdown
30.700%
Expectancy
0.519
Start Equity
100000
End Equity
416582.41
Net Profit
316.582%
Sharpe Ratio
0.554
Sortino Ratio
0.577
Probabilistic Sharpe Ratio
8.054%
Loss Rate
28%
Win Rate
72%
Profit-Loss Ratio
1.11
Alpha
0.015
Beta
0.926
Annual Standard Deviation
0.162
Annual Variance
0.026
Information Ratio
0.097
Tracking Error
0.095
Treynor Ratio
0.097
Total Fees
$1320.71
Estimated Strategy Capacity
$6600000.00
Lowest Capacity Asset
XLK RGRPZX100F39
Portfolio Turnover
2.01%
from AlgorithmImports import *

class MomentumAssetAllocationStrategy(QCAlgorithm):

    def Initialize(self):
        # Set the start date and initial capital for the backtest
        self.SetStartDate(2014, 1, 1)  # Start the backtest from January 1, 2014
        self.SetCash(100000)  # Initial capital of $100,000

        # Dictionary to store Rate of Change (ROC) indicators and SMAs (50-day and 200-day) for each ETF
        self.data = {}  # To store ROC (momentum) indicators
        self.sma50 = {}  # To store 50-day Simple Moving Average for regime filter
        self.sma200 = {}  # To store 200-day Simple Moving Average for regime filter
        period = 12 * 21  # Lookback period for ROC (12 months * 21 trading days per month = 252 days)

        # Warm-up period to gather enough historical data before making trading decisions
        self.SetWarmUp(period, Resolution.Daily)  # Ensure enough data is collected for the 12-month ROC calculation

        # Number of top ETFs to select based on momentum (we select the top 3)
        self.traded_count = 3
        
        # List of 10 sector ETFs (representing different sectors of the market)
        self.symbols = ["XLB", "XLE", "XLF", "XLI", "XLK", "XLP", "XLU", "XLV", "XLY", "VNQ"]

        # Initialize the indicators (ROC, SMA50, and SMA200) for each ETF
        for symbol in self.symbols:
            self.AddEquity(symbol, Resolution.Minute)  # Add equity data for each ETF at minute resolution
            self.data[symbol] = self.ROC(symbol, period, Resolution.Daily)  # 12-month ROC for momentum
            self.sma50[symbol] = self.SMA(symbol, 50, Resolution.Daily)  # 50-day SMA for regime filter
            self.sma200[symbol] = self.SMA(symbol, 200, Resolution.Daily)  # 200-day SMA for regime filter
        
        # Variable to track the last rebalance month to ensure the strategy rebalances only once per month
        self.recent_month = -1  # Initialized to an invalid month to force the first rebalance

    def OnData(self, data):
        # Exit if the algorithm is still warming up (collecting historical data for the indicators)
        if self.IsWarmingUp:
            return

        # Ensure that trading decisions are made after the market opens (at 9:31 AM)
        if not (self.Time.hour == 9 and self.Time.minute == 31):
            return  # Avoid trading at the market open to reduce the effect of early-day volatility

        self.Log(f"Market Open Time: {self.Time}")

        # Rebalance the portfolio once per month (check if the month has changed)
        if self.Time.month == self.recent_month:
            return  # Skip rebalancing if it's the same month as the last rebalance
        self.recent_month = self.Time.month  # Update the last rebalance month to the current month
        
        self.Log(f"New monthly rebalance...")

        # Prepare to select the top 3 ETFs based on momentum and the regime filter (SMA 50 > SMA 200)
        selected = {}

        # Loop through each ETF and check if the indicators (ROC, SMA50, and SMA200) are ready
        for symbol in self.symbols:
            if not data.ContainsKey(symbol):
                continue  # Skip the ETF if data for the symbol is not available
            
            roc = self.data[symbol]  # Retrieve the ROC (momentum) indicator for the ETF
            sma50 = self.sma50[symbol]  # Retrieve the 50-day SMA for the ETF
            sma200 = self.sma200[symbol]  # Retrieve the 200-day SMA for the ETF
            
            # Check if all indicators (ROC, SMA50, SMA200) have enough data and are ready
            if roc.IsReady and sma50.IsReady and sma200.IsReady:
                # Apply the regime filter: Only include ETFs where the 50-day SMA > 200-day SMA (bullish trend)
                if sma50.Current.Value > sma200.Current.Value:
                    selected[symbol] = roc  # Select the ETF if it passes the regime filter
                    self.Log(f"{symbol} passes the regime filter: 50-SMA > 200-SMA")
                else:
                    self.Log(f"{symbol} fails the regime filter: 50-SMA <= 200-SMA")

        # Sort the selected ETFs by their momentum (ROC) in descending order (highest momentum first)
        sorted_by_momentum = sorted(selected.items(), key=lambda x: x[1].Current.Value, reverse=True)
        self.Log(f"Number of assets passing the regime filter: {len(sorted_by_momentum)}")

        # List to store the selected top 3 ETFs based on momentum
        long = []
        
        # Only proceed if there are enough ETFs that passed the regime filter
        if len(sorted_by_momentum) >= self.traded_count:
            long = [x[0] for x in sorted_by_momentum][:self.traded_count]  # Select the top 3 momentum ETFs

        # Get the list of currently invested ETFs in the portfolio
        invested = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
        
        # Liquidate any ETFs that are no longer part of the top 3 momentum performers
        for symbol in invested:
            if symbol not in long:
                self.Liquidate(symbol)  # Exit the position for ETFs that are no longer in the top 3

        self.Log(f"Selected long leg for next month: {long}")
        
        # **Dynamic Weighting**: Allocate capital proportionally to the strength of each ETF's momentum
        total_momentum = sum([x[1].Current.Value for x in sorted_by_momentum[:self.traded_count]])  # Sum of top 3 momentum values
        if total_momentum == 0:
            total_momentum = 1  # Avoid division by zero if momentum values are zero for all selected assets
        
        # Allocate portfolio weights based on each ETF's relative momentum strength
        for symbol, roc in sorted_by_momentum[:self.traded_count]:
            weight = roc.Current.Value / total_momentum  # Calculate weight based on momentum
            self.SetHoldings(symbol, weight)  # Set holdings for the ETF based on the calculated weight
            self.Log(f"Allocated {weight * 100:.2f}% to {symbol} based on momentum strength.")