Overall Statistics |
Total Orders 6872 Average Win 0.04% Average Loss -0.05% Compounding Annual Return 3.506% Drawdown 13.400% Expectancy 0.017 Start Equity 100000 End Equity 103809.60 Net Profit 3.810% Sharpe Ratio -0.202 Sortino Ratio -0.259 Probabilistic Sharpe Ratio 18.154% Loss Rate 42% Win Rate 58% Profit-Loss Ratio 0.76 Alpha -0.052 Beta 0.238 Annual Standard Deviation 0.109 Annual Variance 0.012 Information Ratio -1.107 Tracking Error 0.134 Treynor Ratio -0.092 Total Fees $6567.51 Estimated Strategy Capacity $0 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 40.42% |
from AlgorithmImports import * class EnhancedEMACrossoverRSIAlgorithm(QCAlgorithm): def Initialize(self): # Set backtest dates self.SetStartDate(2023, 1, 1) self.SetEndDate(2024, 1, 31) # Adjusted end date to a trading day # Set starting cash self.SetCash(100000) # Universe settings self.num_coarse = 100 # Reduced from 1000 to 100 self.symbol_data = {} # Risk management parameters self.stop_loss_pct = 0.025 # 2.5% stop-loss self.take_profit_pct = 0.10 # 10% take-profit self.risk_per_trade = 0.01 # Risk 1% of portfolio per trade # Add SPY for always invested rule self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol self.spy_entry_price = None # Initialize entry price for SPY # Schedule universe selection self.AddUniverse(self.CoarseSelectionFunction) # Rebalance only on trading days self.Schedule.On(self.DateRules.EveryDay(self.spy), self.TimeRules.AfterMarketOpen(self.spy, 30), self.Rebalance) # Set universe settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.Leverage = 2 self.UniverseSettings.FillForward = False # ATR for volatility filter self.volatility_threshold = 0.05 # 5% ATR threshold self.atr_period = 14 # ATR period def CoarseSelectionFunction(self, coarse): # Filter to top 100 liquid stocks sorted_coarse = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True) selected = [x.Symbol for x in sorted_coarse[:self.num_coarse]] return selected def OnSecuritiesChanged(self, changes): # Add indicators for new securities for security in changes.AddedSecurities: symbol = security.Symbol security.SetLeverage(2) # Set leverage to 2 if symbol not in self.symbol_data: ema5 = self.EMA(symbol, 5, Resolution.Daily) ema8 = self.EMA(symbol, 8, Resolution.Daily) ema13 = self.EMA(symbol, 13, Resolution.Daily) rsi = self.RSI(symbol, 14, MovingAverageType.Exponential, Resolution.Daily) atr = self.ATR(symbol, self.atr_period, MovingAverageType.Simple, Resolution.Daily) self.symbol_data[symbol] = { 'ema5': ema5, 'ema8': ema8, 'ema13': ema13, 'rsi': rsi, 'atr': atr, 'entry_price': None } # Remove data for removed securities for security in changes.RemovedSecurities: symbol = security.Symbol if symbol in self.symbol_data: if self.Portfolio[symbol].Invested: self.Liquidate(symbol) self.symbol_data.pop(symbol) def Rebalance(self): long_symbols = [] short_symbols = [] # Calculate signals for symbol, indicators in self.symbol_data.items(): if not all([indicators['ema5'].IsReady, indicators['ema8'].IsReady, indicators['ema13'].IsReady, indicators['rsi'].IsReady, indicators['atr'].IsReady]): continue ema5 = indicators['ema5'].Current.Value ema8 = indicators['ema8'].Current.Value ema13 = indicators['ema13'].Current.Value rsi = indicators['rsi'].Current.Value atr = indicators['atr'].Current.Value price = self.Securities[symbol].Price if price <= 0: continue # Volatility filter if atr / price > self.volatility_threshold: continue # Skip if volatility is too high # Long condition if ema5 > ema8 > ema13 and rsi > 60: long_symbols.append(symbol) # Short condition elif ema5 < ema8 < ema13 and rsi < 40: short_symbols.append(symbol) # Ensure we're always invested total_symbols = long_symbols + short_symbols if not total_symbols: # If no signals, invest in SPY if not self.Portfolio[self.spy].Invested: self.SetHoldings(self.spy, 1) # Set entry price for SPY self.spy_entry_price = self.Securities[self.spy].Price return else: if self.Portfolio[self.spy].Invested: self.Liquidate(self.spy) self.spy_entry_price = None # Calculate target allocation per position total_positions = len(long_symbols) + len(short_symbols) if total_positions == 0: return # Leverage limit of 2 target_percent = min(1.0, 2.0) / total_positions for symbol in long_symbols: self.EnterPosition(symbol, target_percent) for symbol in short_symbols: self.EnterPosition(symbol, -target_percent) # Liquidate positions no longer in signals invested_symbols = [x.Symbol for x in self.Portfolio.Values if x.Invested and x.Symbol != self.spy] for symbol in invested_symbols: if symbol not in total_symbols: self.Liquidate(symbol) if symbol in self.symbol_data: self.symbol_data[symbol]['entry_price'] = None def EnterPosition(self, symbol, target_percent): # Set holdings based on target percent self.SetHoldings(symbol, target_percent) if symbol in self.symbol_data: self.symbol_data[symbol]['entry_price'] = self.Securities[symbol].Price def OnData(self, data): # Check stop-loss and take-profit for SPY if self.Portfolio[self.spy].Invested and data.ContainsKey(self.spy) and data[self.spy] is not None: price = data[self.spy].Close entry_price = self.spy_entry_price if entry_price is not None: pnl = (price - entry_price) / entry_price if pnl <= -self.stop_loss_pct: # Stop-loss hit self.Liquidate(self.spy) self.spy_entry_price = None elif pnl >= self.take_profit_pct: # Take-profit hit self.Liquidate(self.spy) self.spy_entry_price = None # Check stop-loss and take-profit for other symbols for symbol in list(self.symbol_data.keys()): if symbol not in data or not data[symbol]: continue if not self.Portfolio[symbol].Invested: continue indicators = self.symbol_data[symbol] entry_price = indicators.get('entry_price') if entry_price is None: continue # Skip if entry_price is not set price = data[symbol].Close holdings = self.Portfolio[symbol] direction = 1 if holdings.IsLong else -1 pnl = (price - entry_price) * direction / entry_price if pnl <= -self.stop_loss_pct: # Stop-loss hit self.Liquidate(symbol) self.symbol_data[symbol]['entry_price'] = None elif pnl >= self.take_profit_pct: # Take-profit hit self.Liquidate(symbol) self.symbol_data[symbol]['entry_price'] = None