Overall Statistics
Total Orders
84
Average Win
1.40%
Average Loss
-1.02%
Compounding Annual Return
-16.059%
Drawdown
7.300%
Expectancy
-0.036
Start Equity
20000
End Equity
19717.46
Net Profit
-1.413%
Sharpe Ratio
-0.601
Sortino Ratio
-1.576
Probabilistic Sharpe Ratio
32.390%
Loss Rate
59%
Win Rate
41%
Profit-Loss Ratio
1.37
Alpha
-0.634
Beta
1.214
Annual Standard Deviation
0.229
Annual Variance
0.052
Information Ratio
-2.625
Tracking Error
0.208
Treynor Ratio
-0.113
Total Fees
$179.47
Estimated Strategy Capacity
$11000000.00
Lowest Capacity Asset
SOXS UKTSIYPJHFMT
Portfolio Turnover
211.99%
# 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 = 30                                 # 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 = True
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 = False
ENABLE_STRATEGY_4 = False
ENABLE_STRATEGY_5 = False


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.SECOND)
            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.Invested and not self.strategy_blocked[symbol][strategy_name]:
                            # Block the strategy for the day
                            self.debug(f"blocking strategy 1 at {self.time} for {symbol}")
                            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.Invested and not self.strategy_blocked[symbol][strategy_name]:
                        # Block the strategy for the day
                        self.debug(f"blocking strategy 2 at {self.time} for {symbol}")
                        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.Invested and not self.strategy_blocked[symbol][strategy_name]:
                        # Block the strategy for the day
                        self.debug(f"blocking strategy 3 at {self.time} for {symbol}")
                        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.Invested and not self.strategy_blocked[symbol][strategy_name]:
                        # Block the strategy for the day
                        self.debug(f"blocking strategy 4 at {self.time} for {symbol}")
                        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.Invested and not self.strategy_blocked[symbol][strategy_name]:
                        # Block the strategy for the day
                        self.debug(f"blocking strategy 5 at {self.time} for {symbol}")
                        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