Overall Statistics
Total Orders
184
Average Win
0.74%
Average Loss
-0.08%
Compounding Annual Return
18.076%
Drawdown
12.700%
Expectancy
2.349
Start Equity
500000.00
End Equity
700923.49
Net Profit
40.185%
Sharpe Ratio
0.435
Sortino Ratio
0.469
Probabilistic Sharpe Ratio
27.926%
Loss Rate
66%
Win Rate
34%
Profit-Loss Ratio
8.74
Alpha
0.057
Beta
0.227
Annual Standard Deviation
0.206
Annual Variance
0.042
Information Ratio
-0.236
Tracking Error
0.228
Treynor Ratio
0.393
Total Fees
$182.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
2.85%
Drawdown Recovery
172
#region imports
from AlgorithmImports import *
import numpy as np
from hmmlearn.hmm import GaussianHMM
from datetime import timedelta
from collections import defaultdict
#endregion

class RegimeAwareMultiStrategyAlgorithm(QCAlgorithm):
    """
    Backtesting-Only Multi-Strategy Trading Algorithm
    
    Strategies Implemented:
    1. BTC Momentum (Long/Short based on RSI + Momentum)
    2. QQQ Wheel (Covered Calls + Cash-Secured Puts)
    3. VIX/SPY Gap Option Spreads (3-DTE Vertical Spreads)
    4. Forex Basket Momentum (EUR/USD/GBP/CHF Basket vs. SMA)
    
    REMOVED: All alpha models, insights, portfolio construction, and risk management
    frameworks that were required for alphaStream licensing.
    
    TRADING METHOD: Direct order tickets and position management.
    """

    def Initialize(self):
        """Initialize algorithm: symbols, indicators, and strategy parameters."""
        
        # ===== CONFIGURATION: Strategy Parameters =====
        
        # BTC Momentum Strategy
        self.btcMomentumPeriod = 30 # This is kept for the indicator definition
        self.btcRsiPeriod = 30 # This is kept for the indicator definition
        
        # BTC Trade Frequency Control
        self.btcLastTradeTime = None
        self.btcMinHoldingPeriod = timedelta(days=2)
        self.btcMinReentryPeriod = timedelta(hours=24)
        self.btcEntryTime = None
        self.btcPreviousSignal = None
        self.btcSignalConfirmationBars = 0
        self.btcRequiredConfirmations = 2  # Need 2 bars of same signal
        
        # Tighter thresholds
        self.btcMomentumBullEntry = 1
        self.btcMomentumBearEntry = -1
        self.btcOverbought = 51
        self.btcSold = 49
        
        # QQQ is used by the Gap strategy for gap calculation
        self.qqqSymbolTicker = "QQQ"
        
        # Gap Spread Strategy
        self.gapAllocation = 0.15
        self.gapVixThreshold = 0.009
        self.gapQqqThreshold = 0.001
        self.gapDteTarget = 3
        self.gapShortDelta = 0.16
        self.gapLongDelta = 0.10
        self.gapMinSpreadBid = 0.08
        self.gapMinOi = 100
        self.gapMaxSpreadPct = 0.10
        self.gapMonthlyDdLimit = 0.02
        
        # Gap Strategy State Tracking
        self.gapYesterdayVixClose = None
        self.gapYesterdayQqqClose = None
        self.gapTodayProcessed = False
        self.gapLastProcessedDate = None
        self.gapCurrentMonth = None
        self.gapMonthlyStartEquity = None
        self.gapStopTradingMonth = False
        self.openGapSpreadTickets = []

        # Forex Basket Strategy
        self.fxPairs = ["EURUSD", "USDCHF", "GBPUSD", "EURGBP"]
        self.fxSymbols = []
        self.fxSmaPeriods = 15 * 24  # 75 days in hourly bars
        self.fxAtrPeriods = 12 * 24  # 25 days in hourly bars
        self.fxSmaIndicators = {}
        self.fxAtrIndicators = {}
        self.fxVolatilityWindow = RollingWindow[float](14)
        self.fxPreviousSignal = None
        self.fxSignalThreshold = 0.05 # 1%
        self.fxRiskRewardRatio = 5.0
        self.fxStopLossAtrMultiplier = 5.0
        self.fxOpenTradeTickets = []
        self.fxOpenEntryTickets = []
        self.fxPrevSmaAvg = None
        self.fxVolatilityLowThreshold = 0.0003
        self.fxSmaVolatilityHighThreshold = 0.03
        
        # Regime Detection
        self.regimeVixBullThreshold = 23.50
        self.regimeVixBearThreshold = 24.50
        self.marketRegime = None  # -1 = Bear, 0 = Neutral, 1 = Bull
        
        # ===== ALGORITHM SETUP =====
        self.SetStartDate(2023, 11, 1)
        self.SetEndDate(2025, 11, 11)
        self.SetCash(500000)
        #self.SetBrokerageModel(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
        
        # ===== ADD SECURITIES & INDICATORS =====
        
        # BTC Crypto (Daily resolution for momentum/RSI)
        self.btcSymbol = self.AddCrypto("BTCUSD", Resolution.Daily, Market.Coinbase).Symbol
        self.btcMomentum = self.MOM(self.btcSymbol, self.btcMomentumPeriod, Resolution.Daily)
        self.btcRsi = self.RSI(self.btcSymbol, self.btcRsiPeriod, MovingAverageType.Simple, Resolution.Daily)
        self.btcAtr = self.ATR(self.btcSymbol, 14, MovingAverageType.Simple, Resolution.Daily)
        self.btcStoch = self.STO(self.btcSymbol, 14, 3, 3, Resolution.Daily)
        
        # QQQ Equity & Options (Wheel Strategy)
        self.qqqSymbol = self.AddEquity(self.qqqSymbolTicker, Resolution.Hour).Symbol
        
        # SPY Equity & Options (Gap Strategy)
        self.spySymbol = self.AddEquity("SPY", Resolution.Hour).Symbol
        self.spyOption = self.AddOption("SPY", Resolution.Hour)
        self.spyOption.SetFilter(lambda universe: universe.Strikes(-35, 35).Expiration(0, 3))
        
        # VIX for Regime Detection
        self.vixIndexSymbol = self.AddIndex("VIX", Resolution.Hour).Symbol
        self.spyDailySymbol = self.AddEquity("SPY", Resolution.Daily).Symbol
        self.vixDailySymbol = self.AddIndex("VIX", Resolution.Daily).Symbol

        # Forex Basket Assets & Indicators
        for pair in self.fxPairs:
            symbol = self.AddForex(pair, Resolution.Hour, Market.Oanda).Symbol
            self.fxSymbols.append(symbol)
            self.fxSmaIndicators[symbol] = self.SMA(symbol, self.fxSmaPeriods, Resolution.Hour)
            self.fxAtrIndicators[symbol] = self.ATR(symbol, self.fxAtrPeriods, MovingAverageType.Simple, Resolution.Hour)

        warmup_periods = max(25, self.fxSmaPeriods)
        self.SetWarmUp(warmup_periods)
        
        # ===== SCHEDULED FUNCTIONS =====
        self.Schedule.On(
            self.DateRules.MonthStart(),
            self.TimeRules.AfterMarketOpen(self.vixDailySymbol, 5),
            self.updateregime
        )
        
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.BeforeMarketClose(self.spySymbol, 15),
            self.gapliquidateandlog
        )
        
        # Schedule BTC once daily instead of hourly
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.At(0, 5),
            self.btc_scheduled_check
        )

        # Schedule for Forex Basket Strategy
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(6, 0), self.fx_signal_check)
        self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.At(18, 0), self.fx_signal_check)
        
       
    def OnData(self, slice):
        """Main trading logic - executed on data events."""
        if self.IsWarmingUp:
            return
        
        # Initialize regime if needed
        if self.marketRegime is None:
            self.updateregime()
        
        # Execute strategies
        # self.btc_momentum_trading(slice) # REMOVED - now runs on schedule
        self.gap_option_spread(slice)

    def btc_scheduled_check(self):
        """Scheduled daily BTC check."""
        self.btc_momentum_trading(None)

    def OnOrderEvent(self, orderEvent):
        """
        Handle order events for all strategies.
        Specifically used by the Forex strategy to place SL/TP orders after entry.
        """
        if orderEvent.Status != OrderStatus.Filled:
            return

        # Check if the filled order is one of our Forex entry orders
        is_entry_order = any(t.OrderId == orderEvent.OrderId for t in self.fxOpenEntryTickets)

        if is_entry_order:
            self.fx_place_bracket_orders(orderEvent.Symbol)

    def updateregime(self):
        """Update market regime based on VIX thresholds."""
        if self.IsWarmingUp:
            return
        
        if not self.Securities.ContainsKey(self.vixDailySymbol) or self.Securities[self.vixDailySymbol].Price == 0:
            self.Log("Regime update failed - VIX data unavailable.")
            return
        
        vixlevel = self.Securities[self.vixDailySymbol].Price
        
        if vixlevel >= self.regimeVixBearThreshold:
            self.marketRegime = -1  # BEAR
        elif vixlevel < self.regimeVixBullThreshold:
            self.marketRegime = 1   # BULL
        else:
            self.marketRegime = 0   # NEUTRAL
        
        regimename = {1: "BULL", 0: "NEUTRAL", -1: "BEAR"}[self.marketRegime]
        
        self.Log(f"Regime Updated: {regimename} | VIX Level: {vixlevel:.2f}")
        
    def btc_momentum_trading(self, data):
        """Execute BTC momentum strategy with RSI and momentum indicators.
        Long/Short based on regime, momentum, and RSI levels with reduced frequency."""
        
        if not all(indicator.IsReady for indicator in [self.btcRsi, self.btcMomentum, self.btcStoch]):
            return

        rsi = self.btcRsi.Current.Value
        momentum = self.btcMomentum.Current.Value
        holdings = self.Portfolio[self.btcSymbol]
        stoch_k = self.btcStoch.StochK.Current.Value

        # Cooldown checks
        if not holdings.Invested and self.btcLastTradeTime:
            if self.Time - self.btcLastTradeTime < self.btcMinReentryPeriod:
                return
        
        if holdings.Invested and self.btcEntryTime:
            if self.Time - self.btcEntryTime < self.btcMinHoldingPeriod:
                return
        
        # Signal generation
        current_signal = None
        
        if self.marketRegime == -1:
            if momentum < self.btcMomentumBearEntry and rsi < self.btcSold and stoch_k < 80:
                current_signal = "SHORT"
            elif rsi > self.btcSold or momentum > 1:
                current_signal = "COVER"
        else:
            if holdings.IsShort:
                current_signal = "COVER"
            elif momentum > self.btcMomentumBullEntry and rsi > self.btcOverbought and stoch_k > 20:
                current_signal = "BUY"
            elif rsi < self.btcOverbought or momentum < -1 :
                current_signal = "SELL"
        
        # Execution
        if not holdings.Invested and current_signal == "BUY":
            quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
            self.MarketOrder(self.btcSymbol, quantity)
            self.btcEntryTime = self.Time
            self.Log(f"BTC LONG | Mom: {momentum:.0f} | RSI: {rsi:.0f}")
        
        elif not holdings.Invested and current_signal == "SHORT":
            quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
            self.MarketOrder(self.btcSymbol, -quantity)
            self.btcEntryTime = self.Time
            self.Log(f"BTC SHORT | Mom: {momentum:.0f} | RSI: {rsi:.0f}")
        
        elif holdings.IsLong and current_signal == "SELL":
            self.Liquidate(self.btcSymbol)
            self.btcLastTradeTime = self.Time
            self.btcEntryTime = None
            self.Log(f"BTC EXIT LONG | Mom: {momentum:.0f} | RSI: {rsi:.0f}")
        
        elif holdings.IsShort and current_signal == "COVER":
            self.Liquidate(self.btcSymbol)
            self.btcLastTradeTime = self.Time
            self.btcEntryTime = None
            self.Log(f"BTC COVER SHORT | Mom: {momentum:.0f} | RSI: {rsi:.0f}")

    def gapliquidateandlog(self):
        """Liquidate expired gap spread positions and cancel open orders."""
        # Cancel any open limit orders from the gap strategy
        for ticket in self.openGapSpreadTickets:
            if ticket.Status != OrderStatus.Filled and ticket.Status != OrderStatus.Canceled:
                ticket.Cancel()
                self.Log(f"[GAP STRATEGY] Canceled open limit order for {ticket.Symbol}")
        self.openGapSpreadTickets.clear()
        
        # Liquidate any remaining option positions from the gap strategy
        openpositions = [pos for pos in self.Portfolio.Values 
                        if pos.Invested and pos.Symbol.SecurityType == SecurityType.Option
                        and pos.Symbol.ID.Symbol == self.spyOption.Symbol.Value] # Only liquidate SPY options
        
        for pos in openpositions:
            self.MarketOrder(pos.Symbol, -pos.Quantity)
            self.Log(f"GAP: Liquidated position {pos.Symbol} at market close.")

    def gap_option_spread(self, data):
        """Sell iron condors on VIX/QQQ gap days."""
        
        currentdate = self.Time.date()
        # Use current bar's Open/Close for VIX and QQQ
        currentvixopen = self.Securities[self.vixIndexSymbol].Open if self.Securities[self.vixIndexSymbol].HasData else None
        currentvixclose = self.Securities[self.vixIndexSymbol].Close if self.Securities[self.vixIndexSymbol].HasData else None
        currentqqqopen = self.Securities[self.qqqSymbol].Open if self.Securities[self.qqqSymbol].HasData else None
        currentqqqclose = self.Securities[self.qqqSymbol].Close if self.Securities[self.qqqSymbol].HasData else None
        
        # Ensure we have valid open/close prices
        if currentvixopen is None or currentvixclose is None or currentqqqopen is None or currentqqqclose is None:
            return

        # Initialize yesterday's closes if not set
        if self.gapYesterdayVixClose is None or self.gapYesterdayQqqClose is None:
            self.gapYesterdayVixClose = currentvixclose
            self.gapYesterdayQqqClose = currentqqqclose
            self.gapLastProcessedDate = currentdate
            return
        
        # Reset daily flag at new day
        if currentdate != self.gapLastProcessedDate:
            self.gapTodayProcessed = False
            self.gapLastProcessedDate = currentdate
        
        if self.gapTodayProcessed:
            # Update yesterday's close for the next day's calculation
            self.gapYesterdayVixClose = currentvixclose
            self.gapYesterdayQqqClose = currentqqqclose
            return
        
        # Calculate gap percentages
        vixgappct = (currentvixopen - self.gapYesterdayVixClose) / self.gapYesterdayVixClose
        qqqgappct = (currentqqqopen - self.gapYesterdayQqqClose) / self.gapYesterdayQqqClose
        
        
        # Only trade if NOT in bear regime
        if self.marketRegime == -1:
            self.gapYesterdayVixClose = currentvixclose
            self.gapYesterdayQqqClose = currentqqqclose
            return
        
        if not data.OptionChains or self.spyOption.Symbol not in data.OptionChains:
            self.gapYesterdayVixClose = currentvixclose
            self.gapYesterdayQqqClose = currentqqqclose
            return
        
        optionchain = data.OptionChains[self.spyOption.Symbol]
        if not optionchain or len(optionchain) == 0:
            self.gapYesterdayVixClose = currentvixclose
            self.gapYesterdayQqqClose = currentqqqclose
            return
        
        # Call spread: VIX gap up, QQQ gap up (risk-on)
        # FIX: Corrected logic for call spread.
        if vixgappct > self.gapVixThreshold and qqqgappct < -self.gapQqqThreshold:
            self.Log(f"GAP TRIGGERED - CALL SPREAD | VIX Gap: {vixgappct:.4f} | QQQ Gap: {qqqgappct:.4f}")
            if self.submitspreadlimitorder(optionchain, call=True):
                self.gapTodayProcessed = True
                self.Log("HYBRID GAPSPREAD: SUBMITTED CALL SPREAD")
        
        # Put spread: VIX gap up, QQQ gap down (fear/dip)
        # FIX: Corrected logic for put spread.
        elif vixgappct < -self.gapVixThreshold and qqqgappct > self.gapQqqThreshold:
            self.Log(f"GAP TRIGGERED - PUT SPREAD | VIX Gap: {vixgappct:.4f} | QQQ Gap: {qqqgappct:.4f}")
            if self.submitspreadlimitorder(optionchain, call=False):
                self.gapTodayProcessed = True
                self.Log("HYBRID GAPSPREAD: SUBMITTED PUT SPREAD")
        
        self.gapYesterdayVixClose = currentvixclose
        self.gapYesterdayQqqClose = currentqqqclose

    def submitspreadlimitorder(self, optionchain, call=True):
        """Submit combo limit order for gap spreads."""
        qty = 5 # Number of spreads to trade
        targetdeltashort = self.gapShortDelta if call else -self.gapShortDelta
        targetdeltalong = self.gapLongDelta if call else -self.gapLongDelta
        optiontype = OptionRight.Call if call else OptionRight.Put
        
        # Find short leg (selling this leg)
        shorts = [x for x in optionchain 
                 if x.Right == optiontype 
                 and hasattr(x, 'Greeks') and x.Greeks.Delta is not None
                 and ((call and x.Greeks.Delta >= targetdeltashort) or (not call and x.Greeks.Delta <= targetdeltashort)) # Delta logic for short leg
                 and self.isliquid(x) 
                 and 0 < (x.Expiry - self.Time).days <= self.gapDteTarget]
        
        if not shorts:
            self.Log(f"GAP: No liquid shorts found matching delta {targetdeltashort}")
            return False
        
        shortleg = min(shorts, key=lambda x: abs(x.Greeks.Delta - targetdeltashort))
        
        # Find long leg (buying this leg)
        longs = [x for x in optionchain 
                if x.Right == optiontype 
                and hasattr(x, 'Greeks') and x.Greeks.Delta is not None
                and ((call and x.Greeks.Delta <= targetdeltalong) or (not call and x.Greeks.Delta >= targetdeltalong)) # Delta logic for long leg
                and self.isliquid(x) 
                and 0 < (x.Expiry - self.Time).days <= self.gapDteTarget]
        
        if not longs:
            self.Log(f"GAP: No liquid longs found matching delta {targetdeltalong}")
            return False
        
        longleg = min(longs, key=lambda x: abs(x.Greeks.Delta - targetdeltalong))
        
        # Verify same expiration and different strikes
        if shortleg.Expiry != longleg.Expiry:
            self.Log("GAP: Different expiries for short and long legs")
            return False
        
        if shortleg.Strike == longleg.Strike:
            self.Log("GAP: Short and long leg strikes are the same")
            return False
        
        # Create combo order: sell 1 short leg, buy 1 long leg for each spread
        legs = [
            Leg.Create(shortleg.Symbol, -1),  # Sell 1 short leg contract
            Leg.Create(longleg.Symbol, 1)     # Buy 1 long leg contract
        ]
        
        # FIX: Simplified limit price calculation. It's always (bid of short - ask of long) for a credit spread.
        limitprice = shortleg.BidPrice - longleg.AskPrice
        
        # FIX: Pass 'qty' (number of spreads) to ComboLimitOrder
        tickets = self.ComboLimitOrder(legs, qty, limitprice)
        self.openGapSpreadTickets.extend(tickets)
        
        return True

    def isliquid(self, contract):
        """Check if option contract has sufficient liquidity."""
        try:
            bid = contract.BidPrice if hasattr(contract, 'BidPrice') and contract.BidPrice else 0
            ask = contract.AskPrice if hasattr(contract, 'AskPrice') and contract.AskPrice else 0
            
            if bid < self.gapMinSpreadBid:
                return False
            
            # FIX: Calculate spread as a percentage of the bid price
            if bid <= 0: return False # Avoid division by zero
            spread_pct = (ask - bid) / bid
            
            oi = contract.OpenInterest if hasattr(contract, 'OpenInterest') else 0
            
            if oi < self.gapMinOi:
                return False
            
            # FIX: Compare percentage spread to percentage threshold
            if spread_pct > self.gapMaxSpreadPct:
                return False
            
            return True
        except Exception as e:
            self.Log(f"isliquid ERROR: {str(e)}")
            return False

    # ==================================================================================
    # ===== FOREX BASKET STRATEGY ======================================================
    # ==================================================================================

    def fx_signal_check(self):
        """
        Runs on a schedule to check for Forex Basket trading signals.
        """
        if self.IsWarmingUp or not all(self.fxSmaIndicators[p].IsReady for p in self.fxSymbols):
            return

        basket_avg_price = np.mean([self.Securities[p].Price for p in self.fxSymbols if self.Securities[p].Price > 0])
        if basket_avg_price == 0: return

        sma_avg = np.mean([self.fxSmaIndicators[p].Current.Value for p in self.fxSymbols])

        # Add current basket price to volatility window
        self.fxVolatilityWindow.Add(basket_avg_price)

        # --- Volatility Filter ---
        if self.fxVolatilityWindow.IsReady:
            prices = list(self.fxVolatilityWindow)
            returns = [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices)) if prices[i-1] != 0]
            
            if len(returns) > 1:
                volatility = np.std(returns)
                if volatility < self.fxVolatilityLowThreshold:
                    self.Log(f"FX Basket: Volatility too low ({volatility:.4f}), skipping signal check.")
                    return

        # --- Market Extremes Filter ---
        if self.fxPrevSmaAvg is not None:
            sma_change = abs(sma_avg - self.fxPrevSmaAvg) / self.fxPrevSmaAvg if self.fxPrevSmaAvg != 0 else 0
            if sma_change > self.fxSmaVolatilityHighThreshold:
                self.Log(f"FX Basket: SMA too volatile (changed {sma_change:.2%}), skipping signal check.")
                self.fxPrevSmaAvg = sma_avg
                return
        self.fxPrevSmaAvg = sma_avg

        current_signal = None
        if basket_avg_price > sma_avg * (1 + self.fxSignalThreshold):
            current_signal = "BUY"
        elif basket_avg_price < sma_avg * (1 - self.fxSignalThreshold):
            current_signal = "SELL"

        self.fxPreviousSignal = current_signal

        is_invested = any(self.Portfolio[p].Invested for p in self.fxSymbols)
        if not is_invested:
            self.Log(f"FX Basket: Signal '{current_signal}' CONFIRMED. Placing trade. (Price: {basket_avg_price:.5f}, SMA: {sma_avg:.5f})")
            self.fx_place_basket_trade(current_signal)
    
    def fx_place_basket_trade(self, direction):
        """
        Liquidates existing positions and places new limit orders to enter a position.
        """
        # Cancel all open orders (entry, SL, TP) before placing new ones
        for ticket in self.fxOpenTradeTickets: ticket.Cancel(f"FX Basket: New signal '{direction}' received.")
        self.fxOpenTradeTickets.clear()
        for ticket in self.fxOpenEntryTickets: ticket.Cancel(f"FX Basket: New signal '{direction}' received.")
        self.fxOpenEntryTickets.clear()

        # Liquidate any existing positions
        for pair in self.fxSymbols:
            if self.Portfolio[pair].Invested:
                self.Liquidate(pair, f"FX Basket: New signal '{direction}' received.")

        # --- AGGRESSIVE DYNAMIC POSITION SIZING ---
        # WARNING: This sizing is very aggressive and likely to cause margin calls.
        portfolio_value_increment = 25000
        lots_per_basket_increment = 12500

        num_baskets_to_trade = int(self.Portfolio.TotalPortfolioValue / portfolio_value_increment)
        self.Log(f"FX Basket Sizing: Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}. Affords {num_baskets_to_trade} baskets.")

        if num_baskets_to_trade == 0:
            self.Log("FX Basket: Trade SKIPPED. Portfolio value is less than the $25,000 required for one basket.")
            return

        final_lots_per_pair = lots_per_basket_increment * num_baskets_to_trade
        self.Log(f"FX Basket: Placing BASKET {direction} limit orders. Lots per pair: {final_lots_per_pair:,} ({num_baskets_to_trade} baskets)")

        for pair in self.fxSymbols:
            price = self.Securities[pair].Price
            if price == 0:
                self.Log(f"FX Basket: Price for {pair} is zero. Skipping order.")
                continue

            order_quantity = final_lots_per_pair if direction == "BUY" else -final_lots_per_pair
            ticket = self.LimitOrder(pair, order_quantity, price)
            self.fxOpenEntryTickets.append(ticket)

    def fx_place_bracket_orders(self, symbol):
        """
        Places ATR-based Stop Loss and Take Profit orders for a given symbol.
        """
        position = self.Portfolio[symbol]
        if not position.Invested:
            return

        atr_value = self.fxAtrIndicators[symbol].Current.Value
        if atr_value == 0:
            self.Log(f"FX Basket: ATR for {symbol} is zero, cannot place bracket orders.")
            return

        entry_price = position.AveragePrice
        stop_distance = atr_value * self.fxStopLossAtrMultiplier
        profit_distance = stop_distance * self.fxRiskRewardRatio

        if position.IsLong:
            stop_price = entry_price - stop_distance
            profit_price = entry_price + profit_distance
        else: # IsShort
            stop_price = entry_price + stop_distance
            profit_price = entry_price - profit_distance

        # Place Stop Loss Order
        stop_loss_order = self.StopLimitOrder(symbol, -position.Quantity, stop_price, stop_price)
        self.fxOpenTradeTickets.append(stop_loss_order)

        # Place Take Profit Order
        take_profit_order = self.LimitOrder(symbol, -position.Quantity, profit_price)
        self.fxOpenTradeTickets.append(take_profit_order)

        self.Log(f"FX Basket: Placed Bracket for {symbol}: Stop @ {stop_price:.5f}, TP @ {profit_price:.5f}")