Overall Statistics |
Total Orders 10249 Average Win 1.49% Average Loss -1.02% Compounding Annual Return 34.085% Drawdown 65.600% Expectancy 0.289 Start Equity 100000 End Equity 581919.70 Net Profit 481.920% Sharpe Ratio 0.734 Sortino Ratio 0.787 Probabilistic Sharpe Ratio 17.574% Loss Rate 47% Win Rate 53% Profit-Loss Ratio 1.45 Alpha 0.211 Beta 1.017 Annual Standard Deviation 0.43 Annual Variance 0.185 Information Ratio 0.536 Tracking Error 0.396 Treynor Ratio 0.31 Total Fees $4566.16 Estimated Strategy Capacity $490000000.00 Lowest Capacity Asset EDA TGRALZT9E5ID Portfolio Turnover 8.55% |
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(252, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe self.selected_symbols: List[Symbol] = [] # List to track the top momentum stocks self.momentum_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Universe schedule (first trading day of each month) self.UniverseSettings.Schedule.On(self.DateRules.MonthStart()) # Add a custom universe self.AddUniverse(self.filter_universe, self.log_filtered_universe) # Schedule daily momentum calculations self.Schedule.On(self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 15), self.CalculateMomentumScores) def filter_universe(self, fundamental): """ Filters the universe based on custom rules """ return [ x.Symbol for x in sorted( [ f for f in fundamental if f.HasFundamentalData and f.Price > 10 and f.DollarVolume > 10_000_000 and f.MarketCap > 500_000_000 ], key=lambda f: f.DollarVolume, reverse=True )[:100] # Limit to top 50 symbols by DollarVolume ] def log_filtered_universe(self, selected): """ Logs the filtered universe and updates the selected symbols list """ selected_list = list(selected) # Convert selected to a list tickers = [s.Symbol.Value for s in selected_list] # Extract tickers from the Symbol property of Fundamental objects self.Debug(f"Selected {len(selected_list)} stocks in FundamentalSelection: {', '.join(tickers)}") self.selected_symbols = [s.Symbol for s in selected_list] # Update selectedSymbols with Symbol objects return [s.Symbol for s in selected_list] # Return only Symbol objects def CalculateMomentumScores(self): if self.IsWarmingUp: return # Skip trading until warm-up is complete """Calculate momentum scores for the selected universe using Smoothness Ratio and Maximum Drawdown.""" results = [] for symbol in self.selected_symbols: if symbol not in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol] = SymbolData(self, symbol) data = self.symbol_data_by_symbol[symbol] if data.IsReady: # Fetch historical data for 3 months history = self.History(symbol, 63, Resolution.Daily)['close'] if len(history) < 63: continue # Calculate Momentum 3-1 price_t_3 = history.iloc[-63] # Price 3 months ago price_t_1 = history.iloc[-21] # Price 1 month ago momentum_3_1 = ((price_t_1 - price_t_3) / price_t_3) * 100 # Convert to percentage if momentum_3_1 < 0: continue # Exclude stocks with negative momentum # Calculate Maximum Drawdown (MDD) running_max = history.cummax() drawdowns = (history - running_max) / running_max max_drawdown = drawdowns.min() if max_drawdown < -0.05: # Exclude stocks with drawdown > 20% continue # Calculate Smoothness Ratio daily_returns = history.pct_change().dropna() rolling_std = daily_returns.std() rolling_std_pct = rolling_std * 100 # Convert to percentage smoothness_ratio = momentum_3_1 / rolling_std_pct if rolling_std_pct > 0 else 0 # Append results results.append({ "Symbol": symbol, "Momentum3-1": momentum_3_1, "SmoothnessRatio": smoothness_ratio, "MaxDrawdown": max_drawdown }) # Sort results by Smoothness Ratio (descending) self.momentum_scores = sorted(results, key=lambda x: x["SmoothnessRatio"], reverse=True) # Log top 10 symbols and their metrics top_10 = self.momentum_scores[:10] log_message = "Top 10 Momentum Stocks (3-1): " for rank, result in enumerate(top_10): log_message += f"Rank {rank + 1}: {result['Symbol'].Value} (Momentum: {result['Momentum3-1']:.2f}%, " log_message += f"Smoothness: {result['SmoothnessRatio']:.2f}, Drawdown: {result['MaxDrawdown']:.2f}); " self.Debug(log_message) # Update momentum symbols self.momentum_symbols = [result["Symbol"] for result in top_10] def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) def OnData(self, data): if self.IsWarmingUp: return # Skip trading until warm-up is complete # Log momentum symbols ready for trading tomorrow if self.momentum_symbols: self.Debug(f"Momentum symbols calculated today: {[symbol.Value for symbol in self.momentum_symbols]}") else: self.Debug("No momentum symbols calculated today.") # List to track symbols meeting the trading criteria ready_to_trade = [] # ===== Manage Existing Portfolio Positions ===== for holding in self.Portfolio.Values: symbol = holding.Symbol if not holding.Invested: continue if symbol not in self.symbol_data_by_symbol or self.symbol_data_by_symbol[symbol] is None: continue symbolData = self.symbol_data_by_symbol[symbol] if not symbolData.IsReady: continue price = symbolData.Price sma50 = symbolData.SMA50.Current.Value if price < sma50: # Exit logic: Price drops below SMA50 self.MarketOnOpenOrder(symbol, -holding.Quantity) # Exit using MOO self.Log(f"Exiting position in {symbol} with MOO order - Price: {price:.2f}, 50 SMA: {sma50:.2f}") else: self.Log(f"Managing position in {symbol} - Holding valid: Price: {price:.2f} > SMA50: {sma50:.2f}") # ===== Check New Positions Based on Coarse and Fine Selection ===== for symbol, symbolData in self.symbol_data_by_symbol.items(): if symbolData is None or not symbolData.IsReady: continue symbolData.Update(data) price = symbolData.Price sma50 = symbolData.SMA50.Current.Value sma150 = symbolData.SMA150.Current.Value sma200 = symbolData.SMA200.Current.Value high_52 = symbolData.High52.Current.Value low_52 = symbolData.Low52.Current.Value threshold_30_above_low = low_52 * 1.3 threshold_25_within_high = high_52 * 0.75 if price > sma50 > sma150 > sma200: if price > threshold_30_above_low and price >= threshold_25_within_high: # Add to the list of symbols ready to trade if not self.Portfolio[symbol].Invested: ready_to_trade.append(symbol) quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) symbolData.SetLong() self.Log(f"TTP: True - Entered long position in {symbol} - Price: {price:.2f} > SMA50: {sma50:.2f} > SMA150 {sma150:.2f} > SMA200 {sma200:.2f}. Price > 52W Low: {low_52:.2f} by 30% and Within 52W High: {high_52:.2f} by 25%") # Log the symbols ready to trade tomorrow if ready_to_trade: self.Debug(f"Symbols meeting trading criteria today, ready for tomorrow: {[symbol.Value for symbol in ready_to_trade]}") else: self.Debug("No symbols meet the trading criteria today for new positions.") def is_bullish_market(self): """Check if the market is bullish based on SPY SMA100.""" if not self.spy_100MA.IsReady: return False spy_price = self.Securities[self.spy].Price return spy_price > self.spy_100MA.Current.Value def OnSecuritiesChanged(self, changes): # Handle added securities for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: symbolData = SymbolData(self, symbol) security.SetSlippageModel(ConstantSlippageModel(0.001)) # Example: Setting slippage model self.symbol_data_by_symbol[symbol] = symbolData # Handle removed securities for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: try: self.symbol_data_by_symbol[symbol].Dispose() except Exception as e: self.Debug(f"Error disposing SymbolData for {symbol.Value}: {e}") finally: del self.symbol_data_by_symbol[symbol] def on_warmup_finished(self): self.Debug("OnWarmUpFinished method triggered.") self.Debug("Warm-up finished. Running momentum score calculation.") self.CalculateMomentumScores() class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False # Initialize indicators self.ATR = algorithm.ATR(symbol, 20, MovingAverageType.Simple, Resolution.Daily) self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) self.Reset() @property def IsReady(self): return ( self.ATR.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52 is not None and self.High52.IsReady and self.Low52 is not None and self.Low52.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(7, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Warm up indicators for market regime filtering self.warm_up_indicator(self.spy, self.spy_100MA, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe and momentum stocks self.selected_symbols: List[Symbol] = [] self.momentum_symbols: List[Symbol] = [] self.exit_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.UniverseSettings.Asynchronous = True self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Universe schedule (first trading day of each month) self.UniverseSettings.Schedule.On(self.DateRules.WeekStart()) # Use QuantConnect's built-in DollarVolume Universe for liquidity filtering self.AddUniverse(self.Universe.DollarVolume.Top(20)) # Limiting to top 20 for testing # Schedule weekly momentum calculations self.Schedule.On(self.DateRules.WeekStart(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 15), self.CalculateMomentumScores) # Schedule placing MarketOnOpen orders self.Schedule.On( self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 2), self.PlaceMarketOnOpenOrders ) def on_warmup_finished(self): self.Debug("OnWarmUpFinished method triggered.") self.Debug("Warm-up finished. Running momentum score calculation.") self.CalculateMomentumScores() def is_bullish_market(self): """Check if the market is bullish based on SPY SMA100.""" if not self.spy_100MA.IsReady: return False spy_price = self.Securities[self.spy].Price return spy_price > self.spy_100MA.Current.Value def OnSecuritiesChanged(self, changes): """Handle securities added and removed from the universe.""" for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: # Initialize SymbolData symbol_data = SymbolData(self, symbol) self.symbol_data_by_symbol[symbol] = symbol_data for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol].Dispose() del self.symbol_data_by_symbol[symbol] self.Debug(f"Removed security: {symbol.Value}") def CalculateMomentumScores(self): if self.IsWarmingUp: return # Skip trading until warm-up is complete """Calculate momentum scores using ROC42 and ATR40.""" top_momentum = [] # Maintain a fixed-size list of the top 10 momentum symbols for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue # Calculate Momentum Score: ROC42 / ATR40 roc = symbol_data.ROC42.Current.Value atr = symbol_data.ATR40.Current.Value if atr > 0: # Avoid division by zero momentum_score = roc / atr # Insert into the sorted list of top momentum symbols top_momentum.append((symbol, momentum_score)) top_momentum.sort(key=lambda x: x[1], reverse=True) # Sort by momentum score descending if len(top_momentum) > 10: top_momentum.pop() # Keep only the top 10 # Log top 10 symbols log_message = "Top 10 Momentum Stocks: " for rank, (symbol, momentum_score) in enumerate(top_momentum): log_message += f"Rank {rank + 1}: {symbol.Value} (Momentum Score: {momentum_score:.2f}); " self.Debug(log_message) # Update momentum symbols self.momentum_symbols = [symbol for symbol, _ in top_momentum] def OnData(self, data): if self.IsWarmingUp: return # Market Regime Filter: Exit all positions if SPY is below its 100 SMA if not self.is_bullish_market(): if any(self.Portfolio[symbol].Invested for symbol in self.Portfolio.Keys): self.exit_symbols = [symbol for symbol in self.Portfolio.Keys if self.Portfolio[symbol].Invested] self.Debug("Market regime bearish. Liquidating positions.") self.momentum_symbols = [] # Reset momentum symbols return self.exit_symbols = [] # Reset symbols for exit current_holdings = {symbol.Key for symbol in self.Portfolio if self.Portfolio[symbol.Key].Invested} new_holdings = set(self.momentum_symbols) for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue symbol_data.Update(data) price = symbol_data.Price high_52 = symbol_data.High52.Current.Value low_52 = symbol_data.Low52.Current.Value sma150 = symbol_data.SMA150.Current.Value sma200 = symbol_data.SMA200.Current.Value threshold_30_above_low = low_52 * 1.3 threshold_25_within_high = high_52 * 0.75 # Exit condition: Price below SMA50 or symbol no longer in top 10 if symbol in current_holdings and (price < symbol_data.SMA50.Current.Value or symbol not in new_holdings): self.exit_symbols.append(symbol) # Entry criteria: Ensure price meets thresholds if symbol not in current_holdings and price > threshold_30_above_low and price <= threshold_25_within_high: if price > sma150 > sma200: self.momentum_symbols.append(symbol) def PlaceMarketOnOpenOrders(self): """ Place MarketOnOpen orders for exit symbols and new entries """ for symbol in self.exit_symbols: holding = self.Portfolio[symbol] if holding.Invested: self.MarketOnOpenOrder(symbol, -holding.Quantity) self.Log(f"Exiting {symbol.Value} - Price: {holding.AveragePrice:.2f}") for symbol in self.momentum_symbols: if symbol in self.symbol_data_by_symbol: symbol_data = self.symbol_data_by_symbol[symbol] price = symbol_data.Price if price > symbol_data.SMA50.Current.Value: # Entry condition quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) self.Log(f"Entering {symbol.Value} - Quantity: {quantity}, Price: {price:.2f}") def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ if price <= 0: # Prevent division by zero self.Debug("PositionSizing: Invalid price encountered. Skipping position sizing.") return 0 cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False # Add indicators self.ATR40 = algorithm.ATR(symbol, 40, MovingAverageType.Simple, Resolution.Daily) self.ROC42 = algorithm.ROC(symbol, 42, Resolution.Daily) # 42-day Rate of Change self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) # Warm up indicators using QuantConnect's warm_up_indicator method algorithm.warm_up_indicator(symbol, self.ATR40, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.ROC42, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.SMA50, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.SMA150, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.SMA200, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.High52, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.Low52, Resolution.Daily) # Register indicators for auto-update algorithm.RegisterIndicator(symbol, self.ATR40, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.ROC42, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.SMA50, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.SMA150, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.SMA200, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.High52, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.Low52, Resolution.Daily) self.Reset() @property def IsReady(self): return ( self.ATR40.IsReady and self.ROC42.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52.IsReady and self.Low52.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR40) self.algorithm.deregister_indicator(self.ROC42) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(7, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Warm up indicators for market regime filtering self.warm_up_indicator(self.spy, self.spy_100MA, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe and momentum stocks self.selected_symbols: List[Symbol] = [] self.momentum_symbols: List[Symbol] = [] self.exit_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.UniverseSettings.Asynchronous = True self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Universe schedule (first trading day of each month) self.UniverseSettings.Schedule.On(self.DateRules.WeekStart()) # Use QuantConnect's built-in DollarVolume Universe for liquidity filtering self.AddUniverse(self.Universe.DollarVolume.Top(20)) # Limiting to top 20 for testing # Schedule weekly momentum calculations self.Schedule.On(self.DateRules.WeekStart(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 15), self.CalculateMomentumScores) # Schedule placing MarketOnOpen orders self.Schedule.On( self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 2), self.PlaceMarketOnOpenOrders ) def on_warmup_finished(self): self.Debug("OnWarmUpFinished method triggered.") self.Debug("Warm-up finished. Running momentum score calculation.") self.CalculateMomentumScores() def is_bullish_market(self): """Check if the market is bullish based on SPY SMA100.""" if not self.spy_100MA.IsReady: return False spy_price = self.Securities[self.spy].Price return spy_price > self.spy_100MA.Current.Value def OnSecuritiesChanged(self, changes): """Handle securities added and removed from the universe.""" for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: # Initialize SymbolData symbol_data = SymbolData(self, symbol) self.symbol_data_by_symbol[symbol] = symbol_data for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol].Dispose() del self.symbol_data_by_symbol[symbol] self.Debug(f"Removed security: {symbol.Value}") def CalculateMomentumScores(self): if self.IsWarmingUp: return # Skip trading until warm-up is complete """Calculate momentum scores using ROC42 and ATR40.""" top_momentum = [] # Maintain a fixed-size list of the top 10 momentum symbols for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue # Calculate Momentum Score: ROC42 / ATR40 roc = symbol_data.ROC42.Current.Value atr = symbol_data.ATR40.Current.Value if atr > 0: # Avoid division by zero momentum_score = roc / atr # Insert into the sorted list of top momentum symbols top_momentum.append((symbol, momentum_score)) top_momentum.sort(key=lambda x: x[1], reverse=True) # Sort by momentum score descending if len(top_momentum) > 10: top_momentum.pop() # Keep only the top 10 # Log top 10 symbols log_message = "Top 10 Momentum Stocks: " for rank, (symbol, momentum_score) in enumerate(top_momentum): log_message += f"Rank {rank + 1}: {symbol.Value} (Momentum Score: {momentum_score:.2f}); " self.Debug(log_message) # Update momentum symbols self.momentum_symbols = [symbol for symbol, _ in top_momentum] def OnData(self, data): if self.IsWarmingUp: return # Market Regime Filter: Exit all positions if SPY is below its 100 SMA if not self.is_bullish_market(): if any(self.Portfolio[symbol].Invested for symbol in self.Portfolio.Keys): self.exit_symbols = [symbol for symbol in self.Portfolio.Keys if self.Portfolio[symbol].Invested] self.Debug("Market regime bearish. Liquidating positions.") self.momentum_symbols = [] # Reset momentum symbols return self.exit_symbols = [] # Reset symbols for exit current_holdings = {symbol.Key for symbol in self.Portfolio if self.Portfolio[symbol.Key].Invested} new_holdings = set(self.momentum_symbols) for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue symbol_data.Update(data) price = symbol_data.Price high_52 = symbol_data.High52.Current.Value low_52 = symbol_data.Low52.Current.Value sma150 = symbol_data.SMA150.Current.Value sma200 = symbol_data.SMA200.Current.Value threshold_30_above_low = low_52 * 1.3 threshold_25_within_high = high_52 * 0.75 # Exit condition: Price below SMA50 or symbol no longer in top 10 if symbol in current_holdings and (price < symbol_data.SMA50.Current.Value or symbol not in new_holdings): self.exit_symbols.append(symbol) # Entry criteria: Ensure price meets thresholds if symbol not in current_holdings and price > threshold_30_above_low and price <= threshold_25_within_high: if price > sma150 > sma200: self.momentum_symbols.append(symbol) def PlaceMarketOnOpenOrders(self): """ Place MarketOnOpen orders for exit symbols and new entries """ for symbol in self.exit_symbols: holding = self.Portfolio[symbol] if holding.Invested: self.MarketOnOpenOrder(symbol, -holding.Quantity) self.Log(f"Exiting {symbol.Value} - Price: {holding.AveragePrice:.2f}") for symbol in self.momentum_symbols: if symbol in self.symbol_data_by_symbol: symbol_data = self.symbol_data_by_symbol[symbol] price = symbol_data.Price if price > symbol_data.SMA50.Current.Value: # Entry condition quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) self.Log(f"Entering {symbol.Value} - Quantity: {quantity}, Price: {price:.2f}") def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ if price <= 0: # Prevent division by zero self.Debug("PositionSizing: Invalid price encountered. Skipping position sizing.") return 0 cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False # Add indicators self.ATR40 = algorithm.ATR(symbol, 40, MovingAverageType.Simple, Resolution.Daily) self.ROC42 = algorithm.ROC(symbol, 42, Resolution.Daily) # 42-day Rate of Change self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) # Warm up indicators using QuantConnect's warm_up_indicator method algorithm.warm_up_indicator(symbol, self.ATR40, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.ROC42, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.SMA50, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.SMA150, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.SMA200, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.High52, Resolution.Daily) algorithm.warm_up_indicator(symbol, self.Low52, Resolution.Daily) # Register indicators for auto-update algorithm.RegisterIndicator(symbol, self.ATR40, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.ROC42, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.SMA50, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.SMA150, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.SMA200, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.High52, Resolution.Daily) algorithm.RegisterIndicator(symbol, self.Low52, Resolution.Daily) self.Reset() @property def IsReady(self): return ( self.ATR40.IsReady and self.ROC42.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52.IsReady and self.Low52.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR40) self.algorithm.deregister_indicator(self.ROC42) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(252, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe and momentum stocks self.selected_symbols: List[Symbol] = [] self.momentum_symbols: List[Symbol] = [] self.exit_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.UniverseSettings.Asynchronous = True self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Use QuantConnect's built-in DollarVolume Universe for liquidity filtering self.AddUniverse(self.Universe.DollarVolume.Top(500)) # Schedule weekly momentum calculations self.Schedule.On(self.DateRules.WeekStart(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 15), self.CalculateMomentumScores) # Schedule placing MarketOnOpen orders self.Schedule.On( self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 2), self.PlaceMarketOnOpenOrders ) def OnSecuritiesChanged(self, changes): """Handle securities added and removed from the universe.""" for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol] = SymbolData(self, symbol) # self.Debug(f"Added security: {symbol.Value}") for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol].Dispose() del self.symbol_data_by_symbol[symbol] # self.Debug(f"Removed security: {symbol.Value}") def CalculateMomentumScores(self): if self.IsWarmingUp: return # Skip trading until warm-up is complete """Calculate momentum scores for the selected universe using Smoothness Ratio and Maximum Drawdown.""" # Fetch historical data for all selected symbols in one batch history = self.History(list(self.symbol_data_by_symbol.keys()), 63, Resolution.Daily) if history.empty: return # Exit if no data is returned results = [] for symbol in self.symbol_data_by_symbol.keys(): if symbol not in history.index.levels[0]: continue symbol_history = history.loc[symbol] if len(symbol_history) < 63: continue # Calculate Momentum 3-1 price_t_3 = symbol_history['close'].iloc[-63] # Price 3 months ago price_t_1 = symbol_history['close'].iloc[-21] # Price 1 month ago momentum_3_1 = ((price_t_1 - price_t_3) / price_t_3) * 100 # Convert to percentage if momentum_3_1 < 0: continue # Exclude stocks with negative momentum # Calculate Maximum Drawdown (MDD) running_max = symbol_history['close'].cummax() drawdowns = (symbol_history['close'] - running_max) / running_max max_drawdown = drawdowns.min() if max_drawdown < -0.05: # Exclude stocks with drawdown > 5% continue # Calculate Smoothness Ratio daily_returns = symbol_history['close'].pct_change().dropna() rolling_std = daily_returns.std() rolling_std_pct = rolling_std * 100 # Convert to percentage smoothness_ratio = momentum_3_1 / rolling_std_pct if rolling_std_pct > 0 else 0 # Append only necessary data for ranking results.append({ "Symbol": symbol, "SmoothnessRatio": smoothness_ratio }) # Sort results by Smoothness Ratio (descending) and pick top 10 self.momentum_scores = sorted(results, key=lambda x: x["SmoothnessRatio"], reverse=True)[:10] # Log top 10 symbols top_10 = self.momentum_scores log_message = "Top 10 Momentum Stocks (3-1): " for rank, result in enumerate(top_10): log_message += f"Rank {rank + 1}: {result['Symbol'].Value} (Smoothness: {result['SmoothnessRatio']:.2f}); " self.Debug(log_message) # Update momentum symbols self.momentum_symbols = [result["Symbol"] for result in top_10] def OnData(self, data): if self.IsWarmingUp: return self.exit_symbols = [] # Reset symbols for exit current_holdings = {symbol.Key for symbol in self.Portfolio if self.Portfolio[symbol.Key].Invested} new_holdings = set(self.momentum_symbols) for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue symbol_data.Update(data) price = symbol_data.Price high_52 = symbol_data.High52.Current.Value low_52 = symbol_data.Low52.Current.Value sma150 = symbol_data.SMA150.Current.Value sma200 = symbol_data.SMA200.Current.Value threshold_30_above_low = low_52 * 1.3 threshold_25_within_high = high_52 * 0.75 # Exit condition: Price below SMA50 or symbol no longer in top 10 if symbol in current_holdings and (price < symbol_data.SMA50.Current.Value or symbol not in new_holdings): self.exit_symbols.append(symbol) # Entry criteria: Ensure price meets thresholds if symbol not in current_holdings and price > threshold_30_above_low and price <= threshold_25_within_high: if price > sma150 > sma200: self.momentum_symbols.append(symbol) def PlaceMarketOnOpenOrders(self): """ Place MarketOnOpen orders for exit symbols and new entries """ for symbol in self.exit_symbols: holding = self.Portfolio[symbol] if holding.Invested: self.MarketOnOpenOrder(symbol, -holding.Quantity) self.Log(f"Exiting {symbol.Value} - Price: {holding.AveragePrice:.2f}") for symbol in self.momentum_symbols: if symbol in self.symbol_data_by_symbol: symbol_data = self.symbol_data_by_symbol[symbol] price = symbol_data.Price if price > symbol_data.SMA50.Current.Value: # Entry condition quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) self.Log(f"Entering {symbol.Value} - Quantity: {quantity}, Price: {price:.2f}") def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ if price <= 0: # Prevent division by zero self.Debug("PositionSizing: Invalid price encountered. Skipping position sizing.") return 0 cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False # Add indicators self.ATR = algorithm.ATR(symbol, 20, MovingAverageType.Simple, Resolution.Daily) self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) self.ROC63 = algorithm.ROC(symbol, 63, Resolution.Daily) # 63-day Rate of Change self.Reset() @property def IsReady(self): return ( self.ATR.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52.IsReady and self.Low52.IsReady and self.ROC63.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(252, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe and momentum stocks self.selected_symbols: List[Symbol] = [] self.momentum_symbols: List[Symbol] = [] self.exit_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.UniverseSettings.Asynchronous = True self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Use QuantConnect's built-in DollarVolume Universe for liquidity filtering self.AddUniverse(self.Universe.DollarVolume.Top(100)) # Schedule weekly momentum calculations self.Schedule.On(self.DateRules.WeekStart(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 15), self.CalculateMomentumScores) # Schedule placing MarketOnOpen orders self.Schedule.On( self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 2), self.PlaceMarketOnOpenOrders ) def OnSecuritiesChanged(self, changes): """Handle securities added and removed from the universe.""" for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol] = SymbolData(self, symbol) self.Debug(f"Added security: {symbol.Value}") for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol].Dispose() del self.symbol_data_by_symbol[symbol] self.Debug(f"Removed security: {symbol.Value}") def CalculateMomentumScores(self): if self.IsWarmingUp: return # Skip trading until warm-up is complete """Calculate momentum scores using ROC42 and ATR40.""" top_momentum = [] # Maintain a fixed-size list of the top 10 momentum symbols for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue # Calculate Momentum Score: ROC42 / ATR40 roc = symbol_data.ROC42.Current.Value atr = symbol_data.ATR40.Current.Value if atr > 0: # Avoid division by zero momentum_score = roc / atr # Insert into the sorted list of top momentum symbols top_momentum.append((symbol, momentum_score)) top_momentum.sort(key=lambda x: x[1], reverse=True) # Sort by momentum score descending if len(top_momentum) > 10: top_momentum.pop() # Keep only the top 10 # Log top 10 symbols log_message = "Top 10 Momentum Stocks: " for rank, (symbol, momentum_score) in enumerate(top_momentum): log_message += f"Rank {rank + 1}: {symbol.Value} (Momentum Score: {momentum_score:.2f}); " self.Debug(log_message) # Update momentum symbols self.momentum_symbols = [symbol for symbol, _ in top_momentum] def OnData(self, data): if self.IsWarmingUp: return self.exit_symbols = [] # Reset symbols for exit current_holdings = {symbol.Key for symbol in self.Portfolio if self.Portfolio[symbol.Key].Invested} new_holdings = set(self.momentum_symbols) for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue symbol_data.Update(data) price = symbol_data.Price high_52 = symbol_data.High52.Current.Value low_52 = symbol_data.Low52.Current.Value sma150 = symbol_data.SMA150.Current.Value sma200 = symbol_data.SMA200.Current.Value threshold_30_above_low = low_52 * 1.3 threshold_25_within_high = high_52 * 0.75 # Exit condition: Price below SMA50 or symbol no longer in top 10 if symbol in current_holdings and (price < symbol_data.SMA50.Current.Value or symbol not in new_holdings): self.exit_symbols.append(symbol) # Entry criteria: Ensure price meets thresholds if symbol not in current_holdings and price > threshold_30_above_low and price <= threshold_25_within_high: if price > sma150 > sma200: self.momentum_symbols.append(symbol) def PlaceMarketOnOpenOrders(self): """ Place MarketOnOpen orders for exit symbols and new entries """ for symbol in self.exit_symbols: holding = self.Portfolio[symbol] if holding.Invested: self.MarketOnOpenOrder(symbol, -holding.Quantity) self.Log(f"Exiting {symbol.Value} - Price: {holding.AveragePrice:.2f}") for symbol in self.momentum_symbols: if symbol in self.symbol_data_by_symbol: symbol_data = self.symbol_data_by_symbol[symbol] price = symbol_data.Price if price > symbol_data.SMA50.Current.Value: # Entry condition quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) self.Log(f"Entering {symbol.Value} - Quantity: {quantity}, Price: {price:.2f}") def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ if price <= 0: # Prevent division by zero self.Debug("PositionSizing: Invalid price encountered. Skipping position sizing.") return 0 cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False # Add indicators self.ATR40 = algorithm.ATR(symbol, 40, MovingAverageType.Simple, Resolution.Daily) self.ROC42 = algorithm.ROC(symbol, 42, Resolution.Daily) # 42-day Rate of Change self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) self.Reset() @property def IsReady(self): return ( self.ATR40.IsReady and self.ROC42.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52.IsReady and self.Low52.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR40) self.algorithm.deregister_indicator(self.ROC42) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(252, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe and momentum stocks self.selected_symbols: List[Symbol] = [] self.momentum_symbols: List[Symbol] = [] self.exit_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Universe schedule (first trading day of each month) self.UniverseSettings.Schedule.On(self.DateRules.MonthStart()) # Add a custom universe self.AddUniverse(self.filter_universe, self.log_filtered_universe) # Schedule placing MarketOnOpen orders self.Schedule.On( self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 2), self.PlaceMarketOnOpenOrders ) def filter_universe(self, fundamental): """ Filters the universe based on custom rules """ return [ x.Symbol for x in sorted( [ f for f in fundamental if f.HasFundamentalData and f.Price > 10 and f.DollarVolume > 10_000_000 and f.MarketCap > 500_000_000 ], key=lambda f: f.DollarVolume, reverse=True )[:100] ] def log_filtered_universe(self, selected): """ Logs the filtered universe and updates the selected symbols list """ selected_list = list(selected) # Convert selected to a list tickers = [s.Symbol.Value for s in selected_list] # Extract tickers from the Symbol property of Fundamental objects self.Debug(f"Selected {len(selected_list)} stocks in FundamentalSelection: {', '.join(tickers)}") self.selected_symbols = [s.Symbol for s in selected_list] # Update selectedSymbols with Symbol objects return [s.Symbol for s in selected_list] # Return only Symbol objects def OnData(self, data): if self.IsWarmingUp: return self.momentum_symbols = [] # Reset symbols for entry self.exit_symbols = [] # Reset symbols for exit for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue symbol_data.Update(data) price = symbol_data.Price # Entry condition: Momentum-based selection if symbol not in self.Portfolio or not self.Portfolio[symbol].Invested: sma50 = symbol_data.SMA50.Current.Value sma150 = symbol_data.SMA150.Current.Value sma200 = symbol_data.SMA200.Current.Value if price > sma50 > sma150 > sma200: self.momentum_symbols.append(symbol) # Exit condition: Price below SMA50 if self.Portfolio[symbol].Invested: sma50 = symbol_data.SMA50.Current.Value if price < sma50: self.exit_symbols.append(symbol) def PlaceMarketOnOpenOrders(self): """ Place MarketOnOpen orders for entry and exit symbols """ for symbol in self.exit_symbols: holding = self.Portfolio[symbol] if holding.Invested: self.MarketOnOpenOrder(symbol, -holding.Quantity) self.Log(f"Exiting {symbol.Value} - Price: {holding.AveragePrice:.2f}") for symbol in self.momentum_symbols: if symbol in self.symbol_data_by_symbol: symbol_data = self.symbol_data_by_symbol[symbol] price = symbol_data.Price quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) self.Log(f"Entering {symbol.Value} - Quantity: {quantity}, Price: {price:.2f}") def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ if price <= 0: # Prevent division by zero self.Debug("PositionSizing: Invalid price encountered. Skipping position sizing.") return 0 cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: symbol_data = SymbolData(self, symbol) self.symbol_data_by_symbol[symbol] = symbol_data for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol].Dispose() del self.symbol_data_by_symbol[symbol] class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False self.ATR = algorithm.ATR(symbol, 20, MovingAverageType.Simple, Resolution.Daily) self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) self.Reset() @property def IsReady(self): return ( self.ATR.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52 is not None and self.High52.IsReady and self.Low52 is not None and self.Low52.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(252, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe and momentum stocks self.selected_symbols: List[Symbol] = [] self.momentum_symbols: List[Symbol] = [] self.exit_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Universe schedule (first trading day of each month) self.UniverseSettings.Schedule.On(self.DateRules.MonthStart()) # Add a custom universe self.AddUniverse(self.filter_universe, self.log_filtered_universe) # Schedule placing MarketOnOpen orders self.Schedule.On( self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 2), self.PlaceMarketOnOpenOrders ) def filter_universe(self, fundamental): """ Filters the universe based on custom rules """ return [ x.Symbol for x in sorted( [ f for f in fundamental if f.HasFundamentalData and f.Price > 10 and f.DollarVolume > 10_000_000 and f.MarketCap > 500_000_000 ], key=lambda f: f.DollarVolume, reverse=True )[:100] ] def log_filtered_universe(self, selected): """ Logs the filtered universe and updates the selected symbols list """ selected_list = list(selected) # Convert selected to a list tickers = [s.Symbol.Value for s in selected_list] # Extract tickers from the Symbol property of Fundamental objects self.Debug(f"Selected {len(selected_list)} stocks in FundamentalSelection: {', '.join(tickers)}") self.selected_symbols = [s.Symbol for s in selected_list] # Update selectedSymbols with Symbol objects return [s.Symbol for s in selected_list] # Return only Symbol objects def OnData(self, data): if self.IsWarmingUp: return self.momentum_symbols = [] # Reset symbols for entry self.exit_symbols = [] # Reset symbols for exit for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue symbol_data.Update(data) price = symbol_data.Price # Entry condition: Momentum-based selection if symbol not in self.Portfolio or not self.Portfolio[symbol].Invested: sma50 = symbol_data.SMA50.Current.Value sma150 = symbol_data.SMA150.Current.Value sma200 = symbol_data.SMA200.Current.Value high_52 = symbol_data.High52.Current.Value low_52 = symbol_data.Low52.Current.Value threshold_30_above_low = low_52 * 1.3 threshold_25_within_high = high_52 * 0.75 if price > sma50 > sma150 > sma200: if price > threshold_30_above_low and price >= threshold_25_within_high: self.momentum_symbols.append(symbol) # Exit condition: Price below SMA50 if self.Portfolio[symbol].Invested: sma50 = symbol_data.SMA50.Current.Value if price < sma50: self.exit_symbols.append(symbol) def PlaceMarketOnOpenOrders(self): """ Place MarketOnOpen orders for entry and exit symbols """ for symbol in self.exit_symbols: holding = self.Portfolio[symbol] if holding.Invested: self.MarketOnOpenOrder(symbol, -holding.Quantity) self.Log(f"Exiting {symbol.Value} - Price: {holding.AveragePrice:.2f}") for symbol in self.momentum_symbols: if symbol in self.symbol_data_by_symbol: symbol_data = self.symbol_data_by_symbol[symbol] price = symbol_data.Price quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) self.Log(f"Entering {symbol.Value} - Quantity: {quantity}, Price: {price:.2f}") def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ if price <= 0: # Prevent division by zero self.Debug("PositionSizing: Invalid price encountered. Skipping position sizing.") return 0 cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: symbol_data = SymbolData(self, symbol) self.symbol_data_by_symbol[symbol] = symbol_data for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol].Dispose() del self.symbol_data_by_symbol[symbol] class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False self.ATR = algorithm.ATR(symbol, 20, MovingAverageType.Simple, Resolution.Daily) self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) self.Reset() @property def IsReady(self): return ( self.ATR.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52 is not None and self.High52.IsReady and self.Low52 is not None and self.Low52.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0
from AlgorithmImports import * class MomentumStockPortfolioAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2019, 1, 1) self.SetEndDate(2025, 1, 1) self.SetCash(100000) self.Debug("Algorithm initialized with start date: 2023-01-01 and cash: 100000") # Set warm-up period self.SetWarmUp(252, Resolution.Daily) # Add SPY for market regime filtering self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_100MA = self.SMA(self.spy, 100, Resolution.Daily) # Dictionary to store SymbolData for each symbol self.symbol_data_by_symbol: Dict[Symbol, SymbolData] = {} # List to track the selected universe and momentum stocks self.selected_symbols: List[Symbol] = [] self.momentum_symbols: List[Symbol] = [] self.exit_symbols: List[Symbol] = [] # Universe Settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.ExtendedMarketHours = False self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.SplitAdjusted self.Debug(f"Universe settings configured: Resolution = {self.UniverseSettings.Resolution}, " f"ExtendedMarketHours = {self.UniverseSettings.ExtendedMarketHours}, " f"DataNormalizationMode = {self.UniverseSettings.DataNormalizationMode}") # Add a custom universe self.AddUniverse(self.filter_universe, self.log_filtered_universe) # Schedule daily momentum calculations self.Schedule.On(self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 15), self.CalculateMomentumScores) # Schedule placing MarketOnOpen orders self.Schedule.On( self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketOpen(self.spy, 2), self.PlaceMarketOnOpenOrders ) def filter_universe(self, fundamental): """ Filters the universe based on custom rules """ return [ x.Symbol for x in sorted( [ f for f in fundamental if f.HasFundamentalData and f.Price > 10 and f.DollarVolume > 10_000_000 and f.MarketCap > 500_000_000 ], key=lambda f: f.DollarVolume, reverse=True )[:100] ] def log_filtered_universe(self, selected): """ Logs the filtered universe and updates the selected symbols list """ selected_list = list(selected) # Convert selected to a list tickers = [s.Symbol.Value for s in selected_list] # Extract tickers from the Symbol property of Fundamental objects self.Debug(f"Selected {len(selected_list)} stocks in FundamentalSelection: {', '.join(tickers)}") self.selected_symbols = [s.Symbol for s in selected_list] # Update selectedSymbols with Symbol objects return [s.Symbol for s in selected_list] # Return only Symbol objects def CalculateMomentumScores(self): if self.IsWarmingUp: return # Skip trading until warm-up is complete """Calculate momentum scores for the selected universe using Smoothness Ratio and Maximum Drawdown.""" # Fetch historical data for all selected symbols in one batch history = self.History(self.selected_symbols, 63, Resolution.Daily) if history.empty: return # Exit if no data is returned results = [] for symbol in self.selected_symbols: if symbol not in history.index.levels[0]: continue symbol_history = history.loc[symbol] if len(symbol_history) < 63: continue # Calculate Momentum 3-1 price_t_3 = symbol_history['close'].iloc[-63] # Price 3 months ago price_t_1 = symbol_history['close'].iloc[-21] # Price 1 month ago momentum_3_1 = ((price_t_1 - price_t_3) / price_t_3) * 100 # Convert to percentage if momentum_3_1 < 0: continue # Exclude stocks with negative momentum # Calculate Maximum Drawdown (MDD) running_max = symbol_history['close'].cummax() drawdowns = (symbol_history['close'] - running_max) / running_max max_drawdown = drawdowns.min() if max_drawdown < -0.05: # Exclude stocks with drawdown > 5% continue # Calculate Smoothness Ratio daily_returns = symbol_history['close'].pct_change().dropna() rolling_std = daily_returns.std() rolling_std_pct = rolling_std * 100 # Convert to percentage smoothness_ratio = momentum_3_1 / rolling_std_pct if rolling_std_pct > 0 else 0 # Append only necessary data for ranking results.append({ "Symbol": symbol, "SmoothnessRatio": smoothness_ratio }) # Sort results by Smoothness Ratio (descending) and pick top 10 self.momentum_scores = sorted(results, key=lambda x: x["SmoothnessRatio"], reverse=True)[:10] # Log top 10 symbols top_10 = self.momentum_scores log_message = "Top 10 Momentum Stocks (3-1): " for rank, result in enumerate(top_10): log_message += f"Rank {rank + 1}: {result['Symbol'].Value} (Smoothness: {result['SmoothnessRatio']:.2f}); " self.Debug(log_message) # Update momentum symbols self.momentum_symbols = [result["Symbol"] for result in top_10] def OnData(self, data): if self.IsWarmingUp: return self.exit_symbols = [] # Reset symbols for exit for symbol, symbol_data in self.symbol_data_by_symbol.items(): if not symbol_data.IsReady: continue symbol_data.Update(data) price = symbol_data.Price # Exit condition: Price below SMA50 if self.Portfolio[symbol].Invested: sma50 = symbol_data.SMA50.Current.Value if price < sma50: self.exit_symbols.append(symbol) def PlaceMarketOnOpenOrders(self): """ Place MarketOnOpen orders for exit symbols """ for symbol in self.exit_symbols: holding = self.Portfolio[symbol] if holding.Invested: self.MarketOnOpenOrder(symbol, -holding.Quantity) self.Log(f"Exiting {symbol.Value} - Price: {holding.AveragePrice:.2f}") for symbol in self.momentum_symbols: if symbol in self.symbol_data_by_symbol: symbol_data = self.symbol_data_by_symbol[symbol] price = symbol_data.Price quantity = self.PositionSizing(price) if quantity > 0: self.MarketOnOpenOrder(symbol, quantity) self.Log(f"Entering {symbol.Value} - Quantity: {quantity}, Price: {price:.2f}") def PositionSizing(self, price): """ Calculate position sizing based on portfolio value and price """ if price <= 0: # Prevent division by zero self.Debug("PositionSizing: Invalid price encountered. Skipping position sizing.") return 0 cash_to_invest = self.Portfolio.TotalPortfolioValue * 0.1 # 10% allocation return int(cash_to_invest / price) def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: symbol = security.Symbol if symbol not in self.symbol_data_by_symbol: symbol_data = SymbolData(self, symbol) self.symbol_data_by_symbol[symbol] = symbol_data for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data_by_symbol: self.symbol_data_by_symbol[symbol].Dispose() del self.symbol_data_by_symbol[symbol] class SymbolData: def __init__(self, algorithm, symbol): self.algorithm = algorithm self.symbol = symbol self.Price = 0 self.IsLong = False # Add indicators self.ATR = algorithm.ATR(symbol, 20, MovingAverageType.Simple, Resolution.Daily) self.SMA50 = algorithm.SMA(symbol, 50, Resolution.Daily) self.SMA150 = algorithm.SMA(symbol, 150, Resolution.Daily) self.SMA200 = algorithm.SMA(symbol, 200, Resolution.Daily) self.High52 = algorithm.MAX(symbol, 252, Resolution.Daily) self.Low52 = algorithm.MIN(symbol, 252, Resolution.Daily) self.ROC63 = algorithm.ROC(symbol, 63, Resolution.Daily) # 63-day Rate of Change self.Reset() @property def IsReady(self): return ( self.ATR.IsReady and self.SMA50.IsReady and self.SMA150.IsReady and self.SMA200.IsReady and self.High52.IsReady and self.Low52.IsReady and self.ROC63.IsReady ) def Update(self, data): if self.symbol in data.Bars: self.Price = data.Bars[self.symbol].Close def SetLong(self): self.IsLong = True def Reset(self): self.IsLong = False def Dispose(self): # Deregister indicators to stop automatic updates self.algorithm.deregister_indicator(self.ATR) self.algorithm.deregister_indicator(self.SMA50) self.algorithm.deregister_indicator(self.SMA150) self.algorithm.deregister_indicator(self.SMA200) if self.High52: self.algorithm.deregister_indicator(self.High52) if self.Low52: self.algorithm.deregister_indicator(self.Low52) # Reset attributes self.IsLong = False self.Price = 0