Overall Statistics |
Total Orders 26 Average Win 0.16% Average Loss -0.08% Compounding Annual Return 2.561% Drawdown 0.600% Expectancy 0.100 Start Equity 100000 End Equity 100108.61 Net Profit 0.109% Sharpe Ratio -3.638 Sortino Ratio -5.326 Probabilistic Sharpe Ratio 35.519% Loss Rate 62% Win Rate 38% Profit-Loss Ratio 1.86 Alpha -0.121 Beta 0.126 Annual Standard Deviation 0.018 Annual Variance 0 Information Ratio -7.83 Tracking Error 0.065 Treynor Ratio -0.521 Total Fees $88.12 Estimated Strategy Capacity $1400000.00 Lowest Capacity Asset S XPRDD03HELB9 Portfolio Turnover 32.45% |
# region imports from AlgorithmImports import * # endregion BACKTEST_START_YEAR = 2024 # Set start Year of the Backtest BACKTEST_START_MONTH = 1 # Set start Month of the Backtest BACKTEST_START_DAY = 10 # Set start Day of the Backtest BACKTEST_END_YEAR = 2024 # Set end Year of the Backtest BACKTEST_END_MONTH = 1 # Set end Month of the Backtest BACKTEST_END_DAY = 25 # Set end Day of the Backtest BACKTEST_ACCOUNT_CASH = 100000 # Set Backtest Strategy Cash CAPITAL_PER_TRADE = 20_000 SYMBOLS = ["X", "S"] # = 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 = False 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.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("X", 0), self.ResetDailyVariables) # Initialize daily P&L tracking self.daily_pnl = 0 self.stop_trading = False self.max_daily_loss = -0.035 * self.deployed_capital_per_trade # 3% of deployed capital per trade 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.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") # 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: self.debug(f"bar symbol {symbol} {self.time}") symbol = str(symbol) # Update day low if self.day_lows[symbol] is None or bar.Low < self.day_lows[symbol]: self.day_lows[symbol] = bar.Low # Ensure we have enough data if len(self.previous_bars[symbol]) < 5: continue if not self.portfolio.invested: # Call strategies if self.enable_strat_1: self.Strategy1(symbol, bar) if self.enable_strat_2: self.Strategy2(symbol, bar) if self.enable_strat_3: self.Strategy3(symbol, bar) if self.enable_strat_4: self.Strategy4(symbol, bar) if self.enable_strat_5: self.Strategy5(symbol, bar) # Check exit conditions self.CheckExitConditions(symbol, 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 = (bar.Close - day_low) / day_low * 100 percent_increase_from_prev_bar_low = (bar.Close - previous_bar_low) / previous_bar_low * 100 # Check entry conditions if (percent_increase_from_day_low >= percent_from_day_low and percent_increase_from_prev_bar_low >= 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) self.MarketOrder( symbol, quantity, tag=f"Strategy 1, Window {window_start} {window_end} " f"{self.previous_bars[symbol][2].Close} {self.previous_bars[symbol][1].Close} {self.previous_bars[symbol][0].Close}" ) # 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, 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 time_now = self.Time.time() # Define time windows and parameters new_low_windows = cfg.STRATEGY_2_CONFIG for (window_start, window_end, profit_target, trailing_stop, latest_exit, low_threshold, close_threshold) in new_low_windows: low_threshold /= 100 close_threshold /= 100 if window_start <= time_now <= window_end: # Ensure we have enough previous bars if len(self.previous_bars[symbol]) >= 6: previous_bar_low = self.previous_bars[symbol][0].Low # Calculate the highest high of the prior 1-5 bars with an offset of 1 bar prior_bars = self.previous_bars[symbol][1:6] # Bars with offset of 1 bar prior_highs = [bar.High for bar in prior_bars] highest_prior_high = max(prior_highs) # Check conditions if (previous_bar_low < highest_prior_high * (1 - low_threshold) and bar.Close > previous_bar_low * (1 + close_threshold)): # 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) self.MarketOrder(symbol, quantity, tag=f"Strategy 2, Window {window_start} {window_end}") # Store exit parameters self.exit_parameters[symbol] = { "profit_target_percent": profit_target, "trailing_stop_percent": trailing_stop, "latest_exit_time": latest_exit, "entry_price": bar.Close, "highest_price": bar.Close, "trailing_stop_price": bar.Close * (1 - trailing_stop), } self.Debug(f"{self.Time} - {strategy_name} Entry on {symbol} at {bar.Close}") break # Only one window applies at a time else: # Not enough data to calculate prior highs 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: # 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) self.MarketOrder( symbol, quantity, tag=f"Strategy 3, Window {window_start} {window_end} " f"{self.previous_bars[symbol][2].Close} {self.previous_bars[symbol][1].Close} {self.previous_bars[symbol][0].Close}" ) # 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: # 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) self.MarketOrder( symbol, quantity, tag=f"Strategy 4, Window {window_start} {window_end}" ) # 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: # 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) self.MarketOrder( symbol, quantity, tag=f"Strategy 5, Window {window_start} {window_end}" ) # 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 CheckExitConditions(self, symbol, bar): """Checks exit conditions for the open position in the symbol.""" if not self.Portfolio[symbol].Invested: return params = self.exit_parameters[symbol] entry_price = params["entry_price"] current_price = bar.Close percent_change = (current_price - entry_price) / entry_price * 100 # Update highest price and trailing stop price if current_price > params["highest_price"]: params["highest_price"] = current_price # Check profit target if params["profit_target_percent"] is not None and percent_change >= params["profit_target_percent"]: self.Liquidate(symbol) self.Debug(f"{self.Time} - Exited {symbol} at profit target") # Check trailing stop elif params["trailing_stop_price"] is not None and current_price <= params["trailing_stop_price"]: self.Liquidate(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) self.Debug(f"{self.Time} - Exited {symbol} at latest exit time") def OnOrderEvent(self, orderEvent): """Updates daily P&L and checks for daily stop loss.""" if orderEvent.Status == OrderStatus.Filled: symbol = orderEvent.Symbol fill_price = orderEvent.FillPrice fill_quantity = orderEvent.FillQuantity direction = 1 if orderEvent.Direction == OrderDirection.Sell else -1 # Ensure a portfolio object exists for tracking the symbol's position if symbol not in self.portfolio_positions: self.portfolio_positions[symbol] = {'quantity': 0, 'avg_cost': 0} # Update portfolio position position = self.portfolio_positions[symbol] prev_quantity = position['quantity'] prev_avg_cost = position['avg_cost'] new_quantity = prev_quantity + direction * fill_quantity if new_quantity == 0: # Position fully closed: Realized P&L based on the difference from the average cost profit_loss = direction * fill_quantity * (fill_price - prev_avg_cost) elif (prev_quantity > 0 and direction == -1) or (prev_quantity < 0 and direction == 1): # Reducing a position: Calculate realized P&L for the reduced portion realized_quantity = min(abs(prev_quantity), fill_quantity) profit_loss = realized_quantity * (fill_price - prev_avg_cost) * direction else: # Increasing position or changing direction: No realized P&L profit_loss = 0 # Update the cost basis for the new position if new_quantity != 0: position['avg_cost'] = (prev_quantity * prev_avg_cost + fill_quantity * fill_price * direction) / new_quantity # Update the position quantity position['quantity'] = new_quantity # Accumulate realized P&L self.daily_pnl += profit_loss # Check for daily stop loss if self.daily_pnl <= self.max_daily_loss: self.StopTradingForTheDay() def StopTradingForTheDay(self): """Stops trading for the day and liquidates all positions.""" self.stop_trading = True self.Liquidate() 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