Overall Statistics |
Total Orders 590 Average Win 1.24% Average Loss -0.50% Compounding Annual Return 24.580% Drawdown 33.400% Expectancy 0.535 Start Equity 20000 End Equity 57123.53 Net Profit 185.618% Sharpe Ratio 0.774 Sortino Ratio 0.835 Probabilistic Sharpe Ratio 31.929% Loss Rate 56% Win Rate 44% Profit-Loss Ratio 2.49 Alpha 0.065 Beta 1.107 Annual Standard Deviation 0.214 Annual Variance 0.046 Information Ratio 0.86 Tracking Error 0.087 Treynor Ratio 0.149 Total Fees $590.00 Estimated Strategy Capacity $0 Lowest Capacity Asset PG R735QTJ8XC9X Portfolio Turnover 1.53% |
from datetime import timedelta from AlgorithmImports import * class MomentumMeanReversionAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2020, 1, 1) # Set Start Date self.SetCash(20000) # Set Strategy Cash # Setting resolution to daily for the universe selection self.UniverseSettings.Resolution = Resolution.Daily self.AddEquity("SPY") self.AddUniverse(self.CoarseSelectionFunction) self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.AfterMarketOpen("SPY", 5), self.Rebalance) self.momentum_period = 90 self.mean_reversion_period = 20 self.number_of_stocks = 30 self.stop_loss_percent = 0.95 # Stop loss at 5% below purchase price self.symbols = [] self.next_rebalance_time = self.Time self.stop_loss_orders = {} self.std_dev_indicators = {} #self.SetWarmUp(30, Resolution.Daily) def CoarseSelectionFunction(self, coarse): if self.Time < self.next_rebalance_time: return Universe.Unchanged filtered = filter(lambda x: x.HasFundamentalData and x.Price > 5, coarse) sorted_by_dollar_volume = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True) selected_symbols = [x.Symbol for x in sorted_by_dollar_volume[:200]] self.symbols = selected_symbols[:self.number_of_stocks] return self.symbols def Rebalance(self): if self.Time < self.next_rebalance_time: return # Calculate momentum and mean reversion scores momentum_scores = self.CalculateMomentumScores(self.symbols) mean_reversion_scores = self.CalculateMeanReversionScores(self.symbols) # Select symbols based on scores selected_symbols = self.SelectSymbols(momentum_scores, mean_reversion_scores) # Liquidate and invest self.LiquidateUnselectedSymbols(selected_symbols) for symbol in selected_symbols: if not self.Portfolio[symbol].Invested: self.SetHoldings(symbol, 1 / len(selected_symbols)) self.next_rebalance_time = self.Time + timedelta(30) def CalculateMomentumScores(self, symbols): scores = {} for symbol in symbols: history = self.History(symbol, self.momentum_period, Resolution.Daily) if not history.empty: momentum = (history["close"][-1] - history["close"][0]) / history["close"][0] scores[symbol] = momentum return scores def CalculateMeanReversionScores(self, symbols): scores = {} for symbol in symbols: history = self.History(symbol, self.mean_reversion_period, Resolution.Daily) if not history.empty and len(history["close"]) > 1: rolling_mean = history["close"].mean() rolling_std = history["close"].std() current_price = history["close"][-1] z_score = (current_price - rolling_mean) / rolling_std scores[symbol] = -z_score # Negative Z-score for mean reversion return scores def SelectSymbols(self, momentum_scores, mean_reversion_scores): sorted_momentum = sorted(momentum_scores.items(), key=lambda x: x[1], reverse=True) sorted_mean_reversion = sorted(mean_reversion_scores.items(), key=lambda x: x[1]) # Example selection: top N/2 from each strategy top_momentum = {x[0] for x in sorted_momentum[:self.number_of_stocks // 2]} top_mean_reversion = {x[0] for x in sorted_mean_reversion[:self.number_of_stocks // 2]} return top_momentum.union(top_mean_reversion) def LiquidateUnselectedSymbols(self, selected_symbols): for symbol in self.Portfolio.Keys: if symbol not in selected_symbols and self.Portfolio[symbol].Invested: self.Liquidate(symbol) def OnOrderEvent(self, orderEvent): if orderEvent.Status == OrderStatus.Filled and orderEvent.OrderId in self.stop_loss_orders.values(): symbol = orderEvent.Symbol self.Debug(f"Stop loss triggered for {symbol}") keys_to_remove = [key for key, value in self.stop_loss_orders.items() if value == orderEvent.OrderId] for key in keys_to_remove: del self.stop_loss_orders[key] def SelectSymbols(self, momentum_scores, mean_reversion_scores): sorted_momentum = sorted(momentum_scores.items(), key=lambda x: x[1], reverse=True) sorted_mean_reversion = sorted(mean_reversion_scores.items(), key=lambda x: x[1]) # Example selection: top N/2 from each strategy top_momentum = {x[0] for x in sorted_momentum[:self.number_of_stocks // 2]} top_mean_reversion = {x[0] for x in sorted_mean_reversion[:self.number_of_stocks // 2]} return top_momentum.union(top_mean_reversion) def LiquidateUnselectedSymbols(self, selected_symbols): for symbol in self.Portfolio.Keys: if symbol not in selected_symbols and self.Portfolio[symbol].Invested: self.Liquidate(symbol) def OnOrderEvent(self, orderEvent): if orderEvent.Status == OrderStatus.Filled and orderEvent.OrderId in self.stop_loss_orders.values(): symbol = orderEvent.Symbol self.Debug(f"Stop loss triggered for {symbol}") keys_to_remove = [key for key, value in self.stop_loss_orders.items() if value == orderEvent.OrderId] for key in keys_to_remove: del self.stop_loss_orders[key]