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