Overall Statistics |
Total Orders 334 Average Win 0.96% Average Loss -0.80% Compounding Annual Return 27.152% Drawdown 10.400% Expectancy 0.027 Start Equity 20000 End Equity 20407.74 Net Profit 2.039% Sharpe Ratio -0.388 Sortino Ratio -0.472 Probabilistic Sharpe Ratio 34.410% Loss Rate 53% Win Rate 47% Profit-Loss Ratio 1.20 Alpha -0.732 Beta 1.25 Annual Standard Deviation 0.259 Annual Variance 0.067 Information Ratio -2.53 Tracking Error 0.24 Treynor Ratio -0.081 Total Fees $678.89 Estimated Strategy Capacity $1900000.00 Lowest Capacity Asset SOXS UKTSIYPJHFMT Portfolio Turnover 754.10% |
# region imports from AlgorithmImports import * # endregion BACKTEST_START_YEAR = 2024 # Set start Year of the Backtest BACKTEST_START_MONTH = 5 # Set start Month of the Backtest BACKTEST_START_DAY = 1 # Set start Day of the Backtest BACKTEST_END_YEAR = 2024 # Set end Year of the Backtest BACKTEST_END_MONTH = 5 # Set end Month of the Backtest BACKTEST_END_DAY = 31 # Set end Day of the Backtest BACKTEST_ACCOUNT_CASH = 20000 # Set Backtest Strategy Cash CAPITAL_PER_TRADE = 20_000 SYMBOLS = ["SOXS", "SOXL"] ENABLE_MAX_DAILY_LOSS = False MAX_DAILY_LOSS_PERCENT = 3.5 # = True (with capital T) for enabling, = False (with capital F) for disabling a strategy ENABLE_STRATEGY_1 = True ENABLE_STRATEGY_2 = True ENABLE_STRATEGY_3 = True ENABLE_STRATEGY_4 = True ENABLE_STRATEGY_5 = True STRATEGY_1_CONFIG = [ # (start_time, end_time, percent_from_day_low, profit_target_percent, trailing_stop_percent, latest_exit_time, percent_increase_from_prev_bar_low) (time(9, 40), time(9, 45), 3.0, 1.5, 1.55, time(11, 5), 0.8), (time(9, 45), time(10, 00), 1.4, 1.5, 1.55, time(11, 5), 0.8) ] STRATEGY_2_CONFIG = [ # (start_time, end_time, profit_target, trailing_stop, latest_exit, low_threshold, close_threshold) (time(9, 45), time(10, 30), 1.5, 1.55, time(11, 5), 0.88, 0.8), (time(10, 30), time(10, 50), 1.0, 1.05, time(11, 5), 0.88, 0.8), (time(10, 50), time(11, 15), 0.8, 1.05, time(12, 5), 0.88, 0.8) ] STRATEGY_3_CONFIG = [ # (start_time, end_time, profit_target_percent, trailing_stop_percent, latest_exit_time) (time(11, 25), time(11, 45), 0.8, 1.05, time(12, 5)), ] STRATEGY_4_CONFIG = [ # (start_time, end_time, profit_target_percent, trailing_stop_percent, latest_exit_time) (time(13, 25), time(13, 55), 1.5, 1.55, time(15, 55)), ] STRATEGY_5_CONFIG = [ # (start_time, end_time, profit_target_percent, trailing_stop_percent, latest_exit_time) (time(15, 00), time(15, 30), 1.5, 1.05, time(15, 55)), ]
# region imports from AlgorithmImports import * # endregion from QuantConnect.Algorithm import QCAlgorithm from QuantConnect.Data.Consolidators import TradeBarConsolidator from QuantConnect.Data.Market import TradeBar from QuantConnect.Orders import OrderStatus from QuantConnect import Resolution, DataNormalizationMode from datetime import timedelta, time, datetime from collections import deque # Import deque import config as cfg class MultiStrategyAlgorithm(QCAlgorithm): def Initialize(self): # Set start and end date for backtesting self.SetStartDate(cfg.BACKTEST_START_YEAR, cfg.BACKTEST_START_MONTH, cfg.BACKTEST_START_DAY) self.SetEndDate(cfg.BACKTEST_END_YEAR, cfg.BACKTEST_END_MONTH, cfg.BACKTEST_END_DAY) # self.SetAccountCurrency("USDC") # Setting backtest account cash self.SetCash(cfg.BACKTEST_ACCOUNT_CASH) # Set time zone to US Eastern Time self.SetTimeZone("America/New_York") # Define the symbols self.symbols = cfg.SYMBOLS self.equities = {} self.profit_limit_orders = {} # Store limit order tickets for profit-taking self.enable_strat_1 = cfg.ENABLE_STRATEGY_1 self.enable_strat_2 = cfg.ENABLE_STRATEGY_2 self.enable_strat_3 = cfg.ENABLE_STRATEGY_3 self.enable_strat_4 = cfg.ENABLE_STRATEGY_4 self.enable_strat_5 = cfg.ENABLE_STRATEGY_5 # Add securities and set data resolution for symbol in self.symbols: equity = self.AddEquity(symbol, Resolution.Minute) equity.SetDataNormalizationMode(DataNormalizationMode.Raw) self.equities[symbol] = equity.Symbol # Initialize variables self.exit_parameters = {} # Dictionary to store exit parameters per symbol self.previous_bars = {} self.day_lows = {} self.opening_prices = {} self.strategy_blocked = {} # Tracks if a strategy is blocked for the day per symbol self.portfolio_positions = {} self.strategy_names = ['Strategy1', 'Strategy2', 'Strategy3', 'Strategy4', 'Strategy5'] self.deployed_capital_per_trade = cfg.CAPITAL_PER_TRADE for symbol in self.symbols: # Consolidate data into 5-minute bars consolidator = TradeBarConsolidator(timedelta(minutes=5)) consolidator.DataConsolidated += self.OnDataConsolidated self.SubscriptionManager.AddConsolidator(symbol, consolidator) self.previous_bars[symbol] = deque(maxlen=5) # Initialize deque with maxlen=5 self.day_lows[symbol] = None self.opening_prices[symbol] = None self.exit_parameters[symbol] = { "profit_target_percent": None, "trailing_stop_percent": None, "latest_exit_time": None, "entry_price": None, "highest_price": None, "trailing_stop_price": None, } self.strategy_blocked[symbol] = {name: False for name in self.strategy_names} # Schedule function to reset daily variables at market open self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen(self.symbols[0], 0), self.ResetDailyVariables) self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose(self.symbols[0], 0), self.RecordDailyStartingValue) # Initialize daily P&L tracking self.daily_pnl = 0 self.stop_trading = False self.enable_max_daily_loss = cfg.ENABLE_MAX_DAILY_LOSS self.max_daily_loss = -(cfg.MAX_DAILY_LOSS_PERCENT / 100) * self.deployed_capital_per_trade # 3% of deployed capital per trade self.daily_starting_value = self.Portfolio.TotalPortfolioValue def RecordDailyStartingValue(self): """Records the portfolio value at the start of each day.""" self.daily_starting_value = self.Portfolio.TotalPortfolioValue # self.Debug(f"Daily starting value recorded: {self.daily_starting_value}") def ResetDailyVariables(self): """Resets daily variables at the start of each trading day.""" self.day_lows = {symbol: None for symbol in self.symbols} self.daily_pnl = 0 self.stop_trading = False self.opening_prices = {symbol: None for symbol in self.symbols} self.strategy_blocked = {symbol: {name: False for name in self.strategy_names} for symbol in self.symbols} def OnDataConsolidated(self, sender, bar): """Updates the deque with consolidated 5-minute bars.""" symbol = bar.Symbol.Value # self.debug(f"{symbol} {self.time} {bar.close}") self.previous_bars[symbol].appendleft(bar) # Add new bar to the left def OnData(self, data): """Processes each 1-minute bar and checks entry and exit conditions.""" # Get the current time in hours and minutes current_time = self.Time.strftime("%H:%M") # Calculate daily P&L current_value = self.Portfolio.TotalPortfolioValue daily_pnl = current_value - self.daily_starting_value # Check for daily stop-loss limit if self.enable_max_daily_loss and daily_pnl <= self.max_daily_loss: # self.Debug(f"Stop Trading triggered: Daily P&L = {daily_pnl}, Max Daily Loss = {self.max_daily_loss}") self.StopTradingForTheDay(daily_pnl) # Check if the time is within the desired range if not self.stop_trading and "09:30" <= current_time <= "16:00": for symbol in data.Keys: bar = data[symbol] if bar is None: continue symbol_str = str(symbol) # Update day low if self.day_lows[symbol_str] is None or bar.Low < self.day_lows[symbol_str]: self.day_lows[symbol_str] = bar.Low # Call strategies if self.enable_strat_1: self.Strategy1(symbol_str, bar) if self.enable_strat_2: self.Strategy2(symbol_str, bar) if self.enable_strat_3: self.Strategy3(symbol_str, bar) if self.enable_strat_4: self.Strategy4(symbol_str, bar) if self.enable_strat_5: self.Strategy5(symbol_str, bar) # Check exit conditions self.CheckExitConditions(symbol_str, bar) def Strategy1(self, symbol, bar): """Implements Strategy 1: BUY LOW.""" strategy_name = 'Strategy1' # Check if strategy is blocked for the day for this symbol if self.strategy_blocked[symbol][strategy_name]: return time_now = self.Time.time() # Define time windows and parameters buy_low_windows = cfg.STRATEGY_1_CONFIG for (window_start, window_end, percent_from_day_low, profit_target_percent, trailing_stop_percent, latest_exit_time, percent_increase_from_prev_bar_low) in buy_low_windows: if window_start <= time_now <= window_end: day_low = self.day_lows[symbol] previous_bar_low = self.previous_bars[symbol][0].Low if day_low is not None: # Calculate percentage increases percent_increase_from_day_low_calc = (bar.Close - day_low) / day_low * 100 percent_increase_from_prev_bar_low_calc = (bar.Close - previous_bar_low) / previous_bar_low * 100 # Check entry conditions if (percent_increase_from_day_low_calc >= percent_from_day_low and percent_increase_from_prev_bar_low_calc >= percent_increase_from_prev_bar_low): # If a position is already open in this symbol if self.Portfolio[symbol].Invested: # Block the strategy for the day self.strategy_blocked[symbol][strategy_name] = True else: # Open position quantity = self.CalculateOrderQuantity(symbol, self.deployed_capital_per_trade) tag = f"Entry Order Strategy 1" self.EnterPosition(symbol, quantity, profit_target_percent, tag) # Store exit parameters self.exit_parameters[symbol] = { "profit_target_percent": profit_target_percent, "trailing_stop_percent": trailing_stop_percent, "latest_exit_time": latest_exit_time, "entry_price": bar.Close, "highest_price": bar.Close, "trailing_stop_price": bar.Close * (1 - trailing_stop_percent / 100), } # self.Debug(f"{self.Time} - {strategy_name} Entry on {symbol} at {bar.Close}") break # Only one window applies at a time def Strategy2(self, symbol, current_bar): """Implements Strategy 2: NEW LOW.""" strategy_name = 'Strategy2' # Check if strategy is blocked for the day for this symbol if self.strategy_blocked[symbol][strategy_name]: return current_time = self.Time.time() # Define time windows and parameters strategy_windows = cfg.STRATEGY_2_CONFIG for (window_start_time, window_end_time, profit_target_percent, trailing_stop_percent, latest_exit_time, low_percentage_threshold, close_percentage_threshold) in strategy_windows: low_percentage_threshold /= 100 # Convert to decimal close_percentage_threshold /= 100 # Convert to decimal if window_start_time <= current_time <= window_end_time: # Ensure we have enough previous bars if len(self.previous_bars[symbol]) < 3: continue latest_close = current_bar.Close latest_low = self.previous_bars[symbol][0].Low second_last_low = self.previous_bars[symbol][1].Low third_last_high = self.previous_bars[symbol][2].High # Check thresholds if (second_last_low < third_last_high * (1 - low_percentage_threshold) and # Low threshold condition latest_close > latest_low * (1 + close_percentage_threshold)): # Close threshold condition # If a position is already open in this symbol if self.Portfolio[symbol].Invested: # Block the strategy for the day self.strategy_blocked[symbol][strategy_name] = True else: # Calculate order quantity and place the order quantity = self.CalculateOrderQuantity(symbol, self.deployed_capital_per_trade) tag = f"Entry Order Strategy 2" self.EnterPosition(symbol, quantity, profit_target_percent, tag) # Store exit parameters self.exit_parameters[symbol] = { "profit_target_percent": profit_target_percent, "trailing_stop_percent": trailing_stop_percent, "latest_exit_time": latest_exit_time, "entry_price": current_bar.Close, "highest_price": current_bar.Close, "trailing_stop_price": current_bar.Close * (1 - trailing_stop_percent / 100), } # self.Debug(f"{self.Time} - {strategy_name} Entry on {symbol} at {current_bar.Close}") break # Only one window applies at a time else: # Conditions not met; exit loop return def Strategy3(self, symbol, bar): """Implements Strategy 3: TWO GREENS.""" strategy_name = 'Strategy3' # Check if strategy is blocked for the day for this symbol if self.strategy_blocked[symbol][strategy_name]: return time_now = self.Time.time() # Define time windows and parameters two_greens_windows = cfg.STRATEGY_3_CONFIG for (window_start, window_end, profit_target_percent, trailing_stop_percent, latest_exit_time) in two_greens_windows: if window_start <= time_now <= window_end: # Ensure we have enough previous bars if len(self.previous_bars[symbol]) < 3: continue # Check if last two bars closed higher if (self.previous_bars[symbol][0].Close > self.previous_bars[symbol][1].Close and self.previous_bars[symbol][1].Close > self.previous_bars[symbol][2].Close): # If a position is already open in this symbol if self.Portfolio[symbol].Invested: # Block the strategy for the day self.strategy_blocked[symbol][strategy_name] = True else: # Open position quantity = self.CalculateOrderQuantity(symbol, self.deployed_capital_per_trade) tag = f"Entry Order Strategy 3" self.EnterPosition(symbol, quantity, profit_target_percent, tag) # Store exit parameters self.exit_parameters[symbol] = { "profit_target_percent": profit_target_percent, "trailing_stop_percent": trailing_stop_percent, "latest_exit_time": latest_exit_time, "entry_price": bar.Close, "highest_price": bar.Close, "trailing_stop_price": bar.Close * (1 - trailing_stop_percent / 100), } # self.Debug(f"{self.Time} - {strategy_name} Entry on {symbol} at {bar.Close}") break # Only one window applies at a time def Strategy4(self, symbol, bar): """Implements Strategy 4: THREE GREENS.""" strategy_name = 'Strategy4' # Check if strategy is blocked for the day for this symbol if self.strategy_blocked[symbol][strategy_name]: return time_now = self.Time.time() # Define time windows and parameters three_greens_windows = cfg.STRATEGY_4_CONFIG for (window_start, window_end, profit_target_percent, trailing_stop_percent, latest_exit_time) in three_greens_windows: if window_start <= time_now <= window_end: # Ensure we have enough previous bars if len(self.previous_bars[symbol]) < 4: continue # Check if last three bars closed higher if (self.previous_bars[symbol][0].Close > self.previous_bars[symbol][1].Close and self.previous_bars[symbol][1].Close > self.previous_bars[symbol][2].Close and self.previous_bars[symbol][2].Close > self.previous_bars[symbol][3].Close): # If a position is already open in this symbol if self.Portfolio[symbol].Invested: # Block the strategy for the day self.strategy_blocked[symbol][strategy_name] = True else: # Open position quantity = self.CalculateOrderQuantity(symbol, self.deployed_capital_per_trade) tag = f"Entry Order Strategy 4" self.EnterPosition(symbol, quantity, profit_target_percent, tag) # Store exit parameters self.exit_parameters[symbol] = { "profit_target_percent": profit_target_percent, "trailing_stop_percent": trailing_stop_percent, "latest_exit_time": latest_exit_time, "entry_price": bar.Close, "highest_price": bar.Close, "trailing_stop_price": bar.Close * (1 - trailing_stop_percent / 100), } # self.Debug(f"{self.Time} - {strategy_name} Entry on {symbol} at {bar.Close}") break # Only one window applies at a time def Strategy5(self, symbol, bar): """Implements Strategy 5: FINAL.""" strategy_name = 'Strategy5' # Check if strategy is blocked for the day for this symbol if self.strategy_blocked[symbol][strategy_name]: return time_now = self.Time.time() # Define time windows and parameters final_windows = cfg.STRATEGY_5_CONFIG for (window_start, window_end, profit_target_percent, trailing_stop_percent, latest_exit_time) in final_windows: if window_start <= time_now <= window_end: # Ensure we have enough previous bars if len(self.previous_bars[symbol]) < 3: continue # Check if last two bars closed higher if (self.previous_bars[symbol][0].Close > self.previous_bars[symbol][1].Close and self.previous_bars[symbol][1].Close > self.previous_bars[symbol][2].Close): # If a position is already open in this symbol if self.Portfolio[symbol].Invested: # Block the strategy for the day self.strategy_blocked[symbol][strategy_name] = True else: # Open position quantity = self.CalculateOrderQuantity(symbol, self.deployed_capital_per_trade) tag = f"Entry Order Strategy 5" self.EnterPosition(symbol, quantity, profit_target_percent, tag) # Store exit parameters self.exit_parameters[symbol] = { "profit_target_percent": profit_target_percent, "trailing_stop_percent": trailing_stop_percent, "latest_exit_time": latest_exit_time, "entry_price": bar.Close, "highest_price": bar.Close, "trailing_stop_price": bar.Close * (1 - trailing_stop_percent / 100), } # self.Debug(f"{self.Time} - {strategy_name} Entry on {symbol} at {bar.Close}") break # Only one window applies at a time def PlaceProfitTakingLimitOrder(self, symbol, entry_price, profit_target_percent): """Places a limit order for profit-taking.""" quantity = self.Portfolio[symbol].Quantity profit_target_price = entry_price * (1 + profit_target_percent / 100) ticket = self.LimitOrder(symbol, -quantity, profit_target_price, tag=f"Profit-taking limit order at {profit_target_price}") self.profit_limit_orders[symbol] = ticket # self.Debug(f"{self.Time} - Placed profit-taking limit order for {symbol} at {profit_target_price}") def CancelProfitTakingLimitOrder(self, symbol): """Cancels the profit-taking limit order if it exists.""" if symbol in self.profit_limit_orders and self.profit_limit_orders[symbol] is not None: ticket = self.profit_limit_orders[symbol] if ticket.Status not in [OrderStatus.Filled, OrderStatus.Canceled]: ticket.Cancel() # self.Debug(f"{self.Time} - Canceled profit-taking limit order for {symbol}") self.profit_limit_orders[symbol] = None def EnterPosition(self, symbol, quantity, profit_target_percent, tag): """Handles entering a position and placing a profit-taking limit order.""" entry_price = self.Securities[symbol].Price self.MarketOrder(symbol, quantity, tag=tag) self.PlaceProfitTakingLimitOrder(symbol, entry_price, profit_target_percent) # self.Debug(f"{self.Time} - Entered position for {symbol} at {entry_price}") def CheckExitConditions(self, symbol, bar): """Checks exit conditions for the open position in the symbol.""" if not self.Portfolio[symbol].Invested: self.CancelProfitTakingLimitOrder(symbol) # Ensure no stale orders return params = self.exit_parameters[symbol] current_price = bar.Close # Update highest price and trailing stop price if current_price > params["highest_price"]: params["highest_price"] = current_price params["trailing_stop_price"] = params["highest_price"] * (1 - params["trailing_stop_percent"] / 100) trailing_stop_price = params.get("trailing_stop_price") # Check trailing stop if trailing_stop_price is not None and current_price <= trailing_stop_price: self.Liquidate(symbol, tag=f"{self.Time} - Exited {symbol} at trailing stop {trailing_stop_price}") self.CancelProfitTakingLimitOrder(symbol) # self.Debug(f"{self.Time} - Exited {symbol} at trailing stop") # Check latest exit time elif params["latest_exit_time"] is not None and self.Time.time() >= params["latest_exit_time"]: self.Liquidate(symbol, tag=f"{self.Time} - Exited {symbol} at latest exit time {params['latest_exit_time']}") self.CancelProfitTakingLimitOrder(symbol) # self.Debug(f"{self.Time} - Exited {symbol} at latest exit time") def StopTradingForTheDay(self, daily_pnl): """Stops trading for the day and liquidates all positions.""" self.stop_trading = True self.Liquidate(tag=f"Daily PNL {daily_pnl}") def CalculateOrderQuantity(self, symbol, position_size): """Calculates the quantity of shares to buy based on position size in USD.""" price = self.Securities[symbol].Price quantity = int(position_size / price) return quantity