Overall Statistics |
Total Orders 169 Average Win 1.55% Average Loss -1.52% Compounding Annual Return 41.498% Drawdown 27.700% Expectancy 0.596 Start Equity 1000000 End Equity 3091955.41 Net Profit 209.196% Sharpe Ratio 1.25 Sortino Ratio 1.496 Probabilistic Sharpe Ratio 66.528% Loss Rate 21% Win Rate 79% Profit-Loss Ratio 1.02 Alpha 0.288 Beta -0.177 Annual Standard Deviation 0.221 Annual Variance 0.049 Information Ratio 0.75 Tracking Error 0.276 Treynor Ratio -1.561 Total Fees $1287.86 Estimated Strategy Capacity $680000000.00 Lowest Capacity Asset AMZN R735QTJ8XC9X Portfolio Turnover 2.02% |
# region imports from AlgorithmImports import * from scipy.stats import linregress import numpy as np # endregion class SimpleDynamicMomentumAlgorithm(QCAlgorithm): def Initialize(self): self.SetStartDate(2021, 1, 1) # Set start date self.SetEndDate(datetime.now()) # Set end date self.SetCash(1000000) # Set initial capital # Define the universe of assets self.symbols = [self.AddEquity(ticker, Resolution.Daily).Symbol for ticker in ["AAPL", "MSFT", "GOOGL", "AMZN", "META"]] self.lookback = 90 # Lookback period for momentum calculation (e.g., 3 months) self.rebalance_period = 30 # Rebalance period (e.g., monthly) self.next_rebalance = self.Time + timedelta(days=self.rebalance_period) self.stop_loss_percentage = 0.175 # 17.5% stop-loss self.entry_prices = {} # Store the entry prices for positions self.highest_prices = {} # Store the highest price reached by a stock for trailing stop loss # Market index to gauge overall market conditions self.market = self.AddEquity("SPY", Resolution.Daily).Symbol # Moving averages for market condition self.short_sma = self.SMA(self.market, 50, Resolution.Daily) self.long_sma = self.SMA(self.market, 200, Resolution.Daily) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(10, 0), self.Rebalance) def OnData(self, data): self.UpdateTrailingStopLoss(data) def UpdateTrailingStopLoss(self, data): for symbol in list(self.entry_prices.keys()): if symbol in data and data[symbol] is not None: current_price = data[symbol].Price # Update the highest price reached if symbol not in self.highest_prices: self.highest_prices[symbol] = current_price else: self.highest_prices[symbol] = max(self.highest_prices[symbol], current_price) # Calculate trailing stop price trailing_stop_price = self.highest_prices[symbol] * (1 - self.stop_loss_percentage) # Check if the current price is below the stop price if current_price < trailing_stop_price: self.Liquidate(symbol) self.Debug(f"Trailing stop-loss triggered for {symbol.Value} at {current_price}") del self.entry_prices[symbol] del self.highest_prices[symbol] # Calculate momentum using annualized exponential regression slope def calculate_momentum(self, history): log_prices = np.log(history['close']) days = np.arange(len(log_prices)) slope, _, _, _, _ = linregress(days, log_prices) annualized_slope = slope * 252 # Assuming 252 trading days in a year return annualized_slope def Rebalance(self): if self.Time < self.next_rebalance: return if self.short_sma.Current.Value > self.long_sma.Current.Value: long_weight = 0.99 short_weight = 0.01 else: long_weight = 0.01 short_weight = 0.99 momentum = {} for symbol in self.symbols: history = self.History(symbol, self.lookback, Resolution.Daily) if not history.empty: momentum[symbol] = self.calculate_momentum(history) sorted_symbols = sorted(momentum.items(), key=lambda x: x[1], reverse=True) num_long = int(len(sorted_symbols) * long_weight) num_short = int(len(sorted_symbols) * short_weight) long_symbols = [symbol for symbol, mom in sorted_symbols[:num_long]] short_symbols = [symbol for symbol, mom in sorted_symbols[-num_short:]] long_weight_per_position = long_weight / num_long if num_long > 0 else 0 short_weight_per_position = short_weight / num_short if num_short > 0 else 0 for symbol in self.symbols: if symbol in long_symbols: self.SetHoldings(symbol, long_weight_per_position) self.entry_prices[symbol] = self.Securities[symbol].Price * (1 - self.stop_loss_percentage) elif symbol in short_symbols: self.SetHoldings(symbol, -short_weight_per_position) self.entry_prices[symbol] = self.Securities[symbol].Price * (1 + self.stop_loss_percentage) else: self.Liquidate(symbol) if symbol in self.entry_prices: del self.entry_prices[symbol] self.next_rebalance = self.Time + timedelta(days=self.rebalance_period) def OnEndOfAlgorithm(self): self.Debug("Algorithm finished running.")