| 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.")