Overall Statistics
Total Orders
255
Average Win
0.49%
Average Loss
-0.09%
Compounding Annual Return
12.782%
Drawdown
21.000%
Expectancy
1.552
Start Equity
500000.00
End Equity
915998.28
Net Profit
83.200%
Sharpe Ratio
0.447
Sortino Ratio
0.474
Probabilistic Sharpe Ratio
17.812%
Loss Rate
59%
Win Rate
41%
Profit-Loss Ratio
5.16
Alpha
0.005
Beta
0.622
Annual Standard Deviation
0.141
Annual Variance
0.02
Information Ratio
-0.253
Tracking Error
0.123
Treynor Ratio
0.102
Total Fees
$318.50
Estimated Strategy Capacity
$0
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
1.58%
Drawdown Recovery
448
#region imports
from AlgorithmImports import *
import numpy as np
import json
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."""
        
        # ===== LIVE TRADING CONFIG =====
        self.maxMarginUsage = 0.80 # Do not open new trades if margin usage exceeds 80%
        self.maxDrawdownAlertPercent = 0.04 # Alert if equity drops 4% from peak

        # ===== CONFIGURATION: Strategy Parameters =====
        
        # BTC Momentum Strategy
        self.btcMomentumPeriod = 15 # This is kept for the indicator definition
        self.btcRsiPeriod = 15 # 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 = 55
        self.btcSold = 45
        self.btc_trailing_high = 0
        self.btcTrailingStopPercent = 0.07 # 7% trailing stop
        
        # 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.12
        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.001 # 1%
        self.fxRiskRewardRatio = 5.0
        self.fxStopLossAtrMultiplier = 5.0
        self.fxOpenTradeTickets = []
        self.fxOpenEntryTickets = []
        self.fxPrevSmaAvg = None
        
        
        # Regime Detection
        self.regimeVixBullThreshold = 23.50
        self.regimeVixBearThreshold = 24.50
        self.marketRegime = None  # -1 = Bear, 0 = Neutral, 1 = Bull
        
        # Drawdown tracking
        self.peakEquity = self.Portfolio.TotalPortfolioValue
        self.drawdownAlertSent = False

        # ===== ALGORITHM SETUP =====
        self.SetStartDate(2020, 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(-15, 15).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)

        self.load_state() # Load saved state for restart resilience

        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)

        # Schedule daily summary log for live trading
        self.Schedule.On(
            self.DateRules.EveryDay(),
            self.TimeRules.At(21, 0), # End of day
            self.log_daily_summary
        )
        
       
    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.check_drawdown()
        self.btc_trailing_stop_check()
        self.gap_option_spread(slice)

        # Update runtime statistics for live dashboard
        margin_used_percent = self.Portfolio.TotalMarginUsed / self.Portfolio.TotalPortfolioValue if self.Portfolio.TotalPortfolioValue > 0 else 0
        self.SetRuntimeStatistic("Margin Usage", f"{margin_used_percent:.1%}")
        self.SetRuntimeStatistic("Peak Equity", f"${self.peakEquity:,.2f}")

    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
        
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        symbol = orderEvent.Symbol

        # Handle BTC order events
        if symbol == self.btcSymbol:
            position = self.Portfolio[self.btcSymbol]
            log_message = ""
            if position.Invested: # Entry
                self.btc_trailing_high = orderEvent.FillPrice # Reset trailing stop high
                log_message = f"BTC {order.Direction} ENTRY @ {orderEvent.FillPrice:,.2f}"
            else: # Exit
                log_message = f"BTC EXIT @ {orderEvent.FillPrice:,.2f}"

            self.Log(log_message)
            if self.LiveMode:
                self.Notify.Email(
                    "dca.llc.md@gmail.com",
                    log_message,
                    f"Time: {self.Time}. Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}"
                )
        
        # 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)
            if self.LiveMode:
                self.Notify.Email(
                    "dca.llc.md@gmail.com",
                    f"FX Basket Trade: {order.Direction} {symbol.Value}",
                    f"Filled {order.Quantity} of {symbol.Value} @ {orderEvent.FillPrice:.5f}. Portfolio Value: ${self.Portfolio.TotalPortfolioValue:,.2f}"
                )
    
    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}")
        self.SetRuntimeStatistic("Market Regime", regimename)

    def btc_trailing_stop_check(self):
        """Liquidate BTC if price falls below trailing high."""
        position = self.Portfolio[self.btcSymbol]
        if not position.Invested:
            return

        price = self.Securities[self.btcSymbol].Price
        
        # Update high-water mark
        if position.IsLong:
            self.btc_trailing_high = max(self.btc_trailing_high, price)
            stop_price = self.btc_trailing_high * (1 - self.btcTrailingStopPercent)
            if price < stop_price:
                self.Liquidate(self.btcSymbol, f"Trailing Stop Triggered at {price:.2f}")
                self.btcLastTradeTime = self.Time # Apply cooldown
        # Trailing stop for shorts can be added here if needed

    def check_drawdown(self):
        """Monitors portfolio equity for major drawdowns from its peak."""
        current_equity = self.Portfolio.TotalPortfolioValue
        
        # Update peak equity
        if current_equity > self.peakEquity:
            self.peakEquity = current_equity
            self.drawdownAlertSent = False # Reset alert flag when a new peak is made

        # Check for drawdown
        drawdown = (self.peakEquity - current_equity) / self.peakEquity
        if drawdown >= self.maxDrawdownAlertPercent and not self.drawdownAlertSent:
            self.drawdownAlertSent = True # Prevent spamming alerts
            alert_message = (f"LIFETIME DRAWDOWN ALERT: Portfolio has dropped {drawdown:.2%} from its peak.\n"
                             f"Peak Equity: ${self.peakEquity:,.2f}\n"
                             f"Current Equity: ${current_equity:,.2f}")
            self.Log(alert_message)
            if self.LiveMode:
                self.Notify.Email("dca.llc.md@gmail.com", "LIFETIME DRAWDOWN ALERT", alert_message)


    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 self.btcRsi.IsReady or not self.btcMomentum.IsReady:
            return

        rsi = self.btcRsi.Current.Value
        momentum = self.btcMomentum.Current.Value
        holdings = self.Portfolio[self.btcSymbol]
        
        # 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.btcOverbought:
                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.btcSold:
                current_signal = "BUY"
            elif rsi > self.btcOverbought or momentum < -1 :
                current_signal = "SELL"
        
        # Signal confirmation
        if current_signal == self.btcPreviousSignal:
            self.btcSignalConfirmationBars += 1
        else:
            self.btcSignalConfirmationBars = 1
            self.btcPreviousSignal = current_signal
        
        if self.btcSignalConfirmationBars < self.btcRequiredConfirmations:
            return
        
        # Execution
        if not holdings.Invested and current_signal == "BUY":
            if not self.has_sufficient_buying_power(): return
            quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
            self.MarketOrder(self.btcSymbol, quantity)
            self.btcEntryTime = self.Time
        
        elif not holdings.Invested and current_signal == "SHORT":
            if not self.has_sufficient_buying_power(): return
            quantity = int(self.Portfolio.TotalPortfolioValue * 0.25 / self.Securities[self.btcSymbol].Price)
            self.MarketOrder(self.btcSymbol, -quantity)
            self.btcEntryTime = self.Time
        
        elif holdings.IsLong and current_signal == "SELL":
            self.Liquidate(self.btcSymbol)
            self.btcLastTradeTime = self.Time
            self.btcEntryTime = None
        
        elif holdings.IsShort and current_signal == "COVER":
            self.Liquidate(self.btcSymbol)
            self.btcLastTradeTime = self.Time
            self.btcEntryTime = None

    def gapliquidateandlog(self):
        """Liquidate expired gap spread positions and cancel open orders."""
        if self.IsWarmingUp:
            return

        # 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.vixDailySymbol].Open if self.Securities[self.vixDailySymbol].HasData else None
        currentvixclose = self.Securities[self.vixDailySymbol].Close if self.Securities[self.vixDailySymbol].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."""
        if not self.has_sufficient_buying_power():
            return False

        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

    def has_sufficient_buying_power(self, new_trade_margin_estimate=0):
        """Check if margin usage is within the defined limit."""
        margin_used_percent = self.Portfolio.TotalMarginUsed / self.Portfolio.TotalPortfolioValue if self.Portfolio.TotalPortfolioValue > 0 else 0
        if margin_used_percent >= self.maxMarginUsage:
            self.Log(f"Trade SKIPPED: Margin usage at {margin_used_percent:.1%} exceeds limit of {self.maxMarginUsage:.1%}.")
            return False
        return True

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

    def fx_signal_check(self):
        """
        Runs on a schedule to check for Forex Basket trading signals.
        """
        self.Log(f"FX_SIGNAL_CHECK called at {self.Time}, "f"basket_avg={np.mean([self.Securities[p].Price for p in self.fxSymbols if self.Securities[p].Price > 0]):.5f}")

        if self.IsWarmingUp or not all(indicator.IsReady for indicator in self.fxSmaIndicators.values()):
            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])

        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 and current_signal:
            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.
        """
        if not self.has_sufficient_buying_power():
            return

        # 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}")

    # ==================================================================================
    # ===== LIVE TRADING & RESILIENCE HELPERS ==========================================
    # ==================================================================================

    def log_daily_summary(self):
        """Logs a daily summary of portfolio performance."""
        if self.LiveMode:
            open_positions = [pos.Symbol.Value for pos in self.Portfolio.Values if pos.Invested]
            self.Log(f"DAILY SUMMARY - {self.Time:%Y-%m-%d}: Equity: ${self.Portfolio.TotalPortfolioValue:,.2f}, "
                     f"Unrealized PnL: ${self.Portfolio.TotalUnrealizedProfit:,.2f}, "
                     f"Open Positions: {len(open_positions)}, "
                     f"Margin Used: {(self.Portfolio.TotalMarginUsed / self.Portfolio.TotalPortfolioValue):.1%}")

    def OnEndOfAlgorithm(self):
        """Save state at the end of the algorithm."""
        self.save_state()

    def save_state(self):
        """Saves the algorithm's state variables to ObjectStore for persistence."""
        state = {
            "btcLastTradeTime": self.btcLastTradeTime,
            "btcEntryTime": self.btcEntryTime,
            "gapYesterdayVixClose": self.gapYesterdayVixClose,
            "gapYesterdayQqqClose": self.gapYesterdayQqqClose,
            "gapLastProcessedDate": self.gapLastProcessedDate,
            "btc_trailing_high": self.btc_trailing_high,
            "peakEquity": self.peakEquity,
            "drawdownAlertSent": self.drawdownAlertSent
        }
        self.ObjectStore.Save("algorithm_state", json.dumps(state, default=str))
        self.Log("Algorithm state saved.")

    def load_state(self):
        """Loads the algorithm's state from ObjectStore on startup."""
        if self.ObjectStore.ContainsKey("algorithm_state"):
            try:
                state_json = self.ObjectStore.Read("algorithm_state")
                state = json.loads(state_json)
                
                parse_dt = lambda dt_str: datetime.strptime(dt_str.split('.')[0], '%Y-%m-%d %H:%M:%S') if dt_str and dt_str != 'None' else None

                self.btcLastTradeTime = parse_dt(state.get("btcLastTradeTime"))
                self.btcEntryTime = parse_dt(state.get("btcEntryTime"))
                self.gapYesterdayVixClose = state.get("gapYesterdayVixClose")
                self.gapYesterdayQqqClose = state.get("gapYesterdayQqqClose")
                self.gapLastProcessedDate = datetime.strptime(state.get("gapLastProcessedDate"), '%Y-%m-%d').date() if state.get("gapLastProcessedDate") else None
                self.btc_trailing_high = state.get("btc_trailing_high", 0)
                self.peakEquity = state.get("peakEquity", self.Portfolio.TotalPortfolioValue)
                self.drawdownAlertSent = state.get("drawdownAlertSent", False)

                self.Log("Successfully loaded saved algorithm state.")
            except Exception as e:
                self.Log(f"Error loading state: {e}. Starting with a fresh state.")