Overall Statistics
Total Orders
1678
Average Win
0.29%
Average Loss
-0.18%
Compounding Annual Return
10.111%
Drawdown
34.400%
Expectancy
1.061
Start Equity
100000
End Equity
555930.28
Net Profit
455.930%
Sharpe Ratio
0.5
Sortino Ratio
0.535
Probabilistic Sharpe Ratio
3.363%
Loss Rate
23%
Win Rate
77%
Profit-Loss Ratio
1.66
Alpha
0.015
Beta
0.597
Annual Standard Deviation
0.115
Annual Variance
0.013
Information Ratio
-0.145
Tracking Error
0.089
Treynor Ratio
0.096
Total Fees
$0.00
Estimated Strategy Capacity
$1300000.00
Lowest Capacity Asset
BND TRO5ZARLX6JP
Portfolio Turnover
0.64%
Drawdown Recovery
866
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn.mixture import GaussianMixture # Use standard GMM for VAR calculation

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
# Note: This is not used in the Inverse VAR logic but is included for code consistency.
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

# Function to fit GMM (standard)
def make_gmm(n_components, random_state):
    """Factory function for GaussianMixture (standard GMM)."""
    return GaussianMixture(n_components=n_components, random_state=random_state)
    
def calc_quantile_var(data, alpha=0.05):
    """Compute VaR by quantile."""
    return data.quantile(alpha)

def calc_gmm_var(data_frame, n_components, random_state=7, n_samples=1000, alpha=0.05):
    """
    Replaces the external calc_gmm_var:
    Fits a GMM and calculates VaR based on the simulated distribution.
    """
    if data_frame.empty or data_frame.shape[0] < n_components + 1:
        return 0.0 # Return 0 if insufficient data

    # 1. Prepare data (ensure it's an array of samples)
    X = data_frame.values.reshape(-1, 1)

    # 2. Fit GMM
    gmm = make_gmm(n_components=n_components, random_state=random_state)
    gmm.fit(X)

    # 3. Sample from GMM
    # NOTE: The original code used risky=False, implying sampling from the full fitted distribution.
    rvs = gmm.sample(n_samples)[0].ravel()
    
    # 4. Calculate VaR (Alpha=0.05 for 95% VaR)
    var = calc_quantile_var(pd.Series(rvs), alpha=alpha)
    return var


# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------


class GMMInverseVAR(QCAlgorithm):
    """
    GMM Inverse VAR Strategy, matched to standardized structure.
    """

    def Initialize(self):
        """Initial algorithm settings"""

        # Set initial cash
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)

        self.SetStartDate(2007, 12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)  # Set Strategy Cash

        #self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
        #                       AccountType.Margin)

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)

        # -----------------------------------------------------------------------------
        # init custom universe (FIXED to use Symbol objects)
        # -----------------------------------------------------------------------------

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', "SPY")
        self.ticker_strings = [
            self.BASE_SYMBOL_TICKER, "QQQ", "DIA", "TLT", "GLD", 
            "EFA", "EEM", "BND", "VNQ",
        ]
        
        # Store Symbol objects for use in History and SetHoldings
        self.symbols = [] 
        for ticker in self.ticker_strings:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol)

        # Base symbol must be the Symbol object for scheduling
        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol

        # Algo Exchange Settings
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        # -----------------------------------------------------------------------------
        # other algo parameter settings (Standardized and using register_param)
        # -----------------------------------------------------------------------------

        self._init_prices = False

        self.LOOKBACK = register_param('historical lookback (days)', 60)  # 60 days lookback
        self.LEVERAGE = register_param('leverage', 1.5)
        self.TOLERANCE = register_param('rebalance tolerance', 0.025)
        self.N_COMPONENTS = register_param('gmm n components', 2) # n=2 components
        self.ALPHA = register_param('var confidence alpha', 0.05) # VaR at 95% confidence

        self.RANDOM_STATE = register_param('random_state', 7) 
        self.N_SAMPLES = register_param('gmm var n samples', 1000)

        ## set resolution for historical data calls
        self.HISTORY_RESOLUTION = Resolution.Daily
        register_param('history api resolution', str(self.HISTORY_RESOLUTION))

        # -----------------------------------------------------------------------------
        # track RAM and computation time for main func, also leverage and cash
        # -----------------------------------------------------------------------------

        self.splotName = "Strategy Info"
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series("RAM", SeriesType.Line, 0))
        sPlot.AddSeries(Series("Time", SeriesType.Line, 1))
        sPlot.AddSeries(Series("Cash", SeriesType.Line, 2))
        sPlot.AddSeries(Series("Leverage", SeriesType.Line, 3))
        self.AddChart(sPlot)

        self.time_to_run_main_algo = 0

        # -----------------------------------------------------------------------------
        # track portfolio weights by symbol
        # -----------------------------------------------------------------------------

        self.splotName3 = "Security Weights Info"
        sPlot3 = Chart(self.splotName3)

        for i, sec in enumerate(self.ticker_strings):
            sPlot3.AddSeries(Series(sec, SeriesType.Line, i))

        self.AddChart(sPlot3)

        # -----------------------------------------------------------------------------
        # scheduled functions (Using BASE_SYMBOL object)
        # -----------------------------------------------------------------------------

        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5),
            Action(self.init_prices),
        )

        # make buy list
        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.rebalance),
        )

        # plot RAM
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM),
        )

        # plot weights by asset symbol
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.BeforeMarketClose(self.BASE_SYMBOL, 70),
            Action(self.CHART_SECURITY_WEIGHTS),
        )
        
        # Log parameters
        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))


    def init_prices(self):
        """
        Initialize historical prices.
        """
        if not self.symbols:
            self.Log("no symbols")
            return

        if self._init_prices:
            return
            
        # Pass list of Symbol objects for History API
        self.prices = (
            self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"]
            .unstack(level=0)
            .astype(np.float32)
        )
        self._init_prices = True
        return

    def update_prices(self):
        """
        Update prices efficiently using integrated utility functions.
        """

        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time

        # how many periods do we need (using integrated logic)
        days_to_request = how_many_days(current_date, most_recent_date)

        # if prices up to date return (using integrated logic)
        if zero_days_to_request(days_to_request):
            return

        # get new data (using Symbol objects)
        new_prices = self.History(
            self.symbols, days_to_request, self.HISTORY_RESOLUTION
        )
        
        if "close" in new_prices.columns:
            new_prices = new_prices["close"].unstack(level=0).astype(np.float32)
        else:
            return
            
        # combine datasets using integrated utility
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return

    def check_current_weight(self, symbol):
        """
        Check symbol's current weight.
        Accepts Symbol object.
        """
        P = self.Portfolio
        
        if P.TotalPortfolioValue == 0:
            return 0.0

        # Get the security object using the Symbol object
        security = self.Securities[symbol]
        
        # Access HoldingsValue via the Security object
        current_weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight

    def rebalance(self):
        """fn: run main algorithm (Inverse VAR Rebalancing)"""
        self.Log(
            "\n"
            + "-" * 77
            + "\n[{}] Begin main algorithm computation...".format(self.UtcTime)
        )

        start_time = time.time()  # timer
        self.update_prices()  # update prices

        returns = np.log(self.prices / self.prices.shift(1)).dropna()

        # compute var
        var_dict = {}
        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # Use calc_gmm_var helper function
            var = calc_gmm_var(
                returns[ticker].to_frame(), 
                self.N_COMPONENTS, 
                random_state=self.RANDOM_STATE,
                n_samples=self.N_SAMPLES,
                alpha=self.ALPHA
            )
            var_dict[ticker] = var

        self.Log("var dict:\n{}".format(var_dict))

        # compute target weights
        var_ser = pd.DataFrame.from_dict(var_dict, orient="index").squeeze()
        
        # Inverse VAR weights are proportional to 1/|VaR|
        # Add epsilon to prevent division by zero, and take absolute value.
        invert = 1 / (var_ser.abs() + 1e-9) 
        
        target_weights_ser = invert / invert.sum()

        self.Log("inverse var weights: {}".format(target_weights_ser))

        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # get current weights
            current_weight = self.check_current_weight(sym_obj)
            target_weight = target_weights_ser[ticker] * self.LEVERAGE

            # if current weights outside of tolerance send new orders
            # Note: Target weight calculation uses the proportional allocation * Leverage
            tol = self.TOLERANCE * target_weight
            lower_bound = target_weight - tol
            upper_bound = target_weight + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                self.SetHoldings(sym_obj, target_weight)

        ## end timer
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, "Time", self.time_to_run_main_algo)
        return

    def OnData(self, data):
        """OnData event is the primary entry point for your algorithm.
        Each new data point will be pumped in here."""
        pass

    def CHART_RAM(self):
        # Once a day or something reasonable to prevent spam
        self.Plot(self.splotName, "RAM", OS.ApplicationMemoryUsed / 1024.0)
        P = self.Portfolio
        
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = (
                P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            )
            self.Plot(self.splotName, "Leverage", float(self.track_account_leverage))
        
        self.Plot(self.splotName, "Cash", float(P.Cash))
        return

    def CHART_SECURITY_WEIGHTS(self):
        """Plots the current weight of each security."""
        P = self.Portfolio
        
        for sym_obj in self.symbols:
            # FIX: Access the Security object first
            security = self.Securities[sym_obj] 
            
            weight = 0.0
            if P.TotalPortfolioValue != 0:
                 weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue) * 100
            
            self.Plot(self.splotName3, sym_obj.Value, weight)
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import json 
from sklearn.mixture import GaussianMixture # Explicit import for make_gmm

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# setup parameter registry 
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def make_returns(df):
    """Calculate log returns for a price DataFrame."""
    return np.log(df/df.shift(1)).dropna()

def get_current_date():
    """Helper to get current date for historical updates (if needed outside algorithm)."""
    return datetime.now().date()
    
# ------------------------------------------------------------------------------
# algorithm 
# ------------------------------------------------------------------------------

class TradingWithGMM(QCAlgorithm):
    """Algorithm which implements GMM framework"""
        
    def Initialize(self):
        '''All algorithms must initialized.'''
        
        # -----------------------------------------------------------------------------
        # GLOBAL PARAMETERS (Moved to Initialize for proper tracking)
        # -----------------------------------------------------------------------------
        self.IS_LONG_ONLY = register_param('is_long_only', True)
        self.N_SAMPLES = register_param('n samples (bootstrapping distr.)', 1000)
        self.SAMPLE_DISTR = register_param('sampling distr', 'normal distribution')
        
        # GMM variables
        self.RANDOM_STATE = register_param('random state', 777)
        self.ALPHA = register_param('alpha', 0.95) # for sampling confidence intervals
        self.N_COMPONENTS = register_param('n components (GMM)', 4)
        self.MAX_ITER = register_param('max iterations (GMM)', 100)
        self.N_INIT = register_param('n inits (GMM)', 25)

        self.INIT_PORTFOLIO_CASH = 100000
        self.SetStartDate(2007,1,1)  #Set Start Date
        self.SetEndDate(2025,10,17)    #Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)           #Set Strategy Cash
        
        # -----------------------------------------------------------------------------
        # init brokerage model
        # -----------------------------------------------------------------------------
        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)

                               
        # -----------------------------------------------------------------------------
        # init custom universe (FIXED to use Symbol objects)
        # -----------------------------------------------------------------------------
        ticker_list = ["SPY", "QQQ", "DIA", "EFA", "EEM",  "TLT", 'AGG', 'LQD', "GLD"]
        self.ticker_strings = register_param('ticker_strings', ticker_list)
        
        self.symbols = [] # This will hold Symbol objects
        for ticker in self.ticker_strings: 
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol)
        
        self.BASE_SYMBOL = self.Securities[self.ticker_strings[0]].Symbol # SPY Symbol object
        
        # -----------------------------------------------------------------------------
        # init placeholders
        # -----------------------------------------------------------------------------
        
        self.openMarketOnOpenOrders = []
        self._longs = False
        self._shorts = False 
        
        # -----------------------------------------------------------------------------
        # other algo parameter settings
        # -----------------------------------------------------------------------------
        
        self.HOLDING_PERIOD = register_param('holding period (days)', 63)
        self.LOOKBACK = register_param('lookback (days)', 252)
        self.BET_SIZE = register_param('bet size', 0.05)
        self.LEVERAGE = register_param('leverage', 1.)

        # -----------------------------------------------------------------------------        
        # track RAM and computation time for main func, also leverage and cash 
        # ----------------------------------------------------------------------------- 
        self.splotName = 'Strategy Info'
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series('RAM',  SeriesType.Line, 0))
        sPlot.AddSeries(Series('Time',  SeriesType.Line, 1))
        sPlot.AddSeries(Series('Leverage',  SeriesType.Line, 2))
        sPlot.AddSeries(Series('Cash',  SeriesType.Line, 3))        
        self.AddChart(sPlot)
        
        self.time_to_run_main_algo = 0    
        
        # -----------------------------------------------------------------------------
        # scheduled functions (Using BASE_SYMBOL object)
        # -----------------------------------------------------------------------------
        
        self.Schedule.On(
            self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.run_main_algo))        
        
        # send orders (FIXED: Uses BASE_SYMBOL object)
        self.Schedule.On(
            self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), 
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 30), 
            Action(self.send_orders))
        
        # check trade dates and liquidate if date condition (FIXED: Uses BASE_SYMBOL object)
        self.Schedule.On(
            self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 35),
            Action(self.check_liquidate))   
        
        # plot RAM (FIXED: Uses BASE_SYMBOL object)
        self.Schedule.On(
            self.DateRules.EveryDay(), 
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM))
        
        # -----------------------------------------------------------------------------
        # initialize historical prices 
        # -----------------------------------------------------------------------------
        self.prices = (self.History(self.symbols, self.LOOKBACK, Resolution.Daily)
                       ["close"]
                       .unstack(level=0)
                       .astype(np.float32)) 
            
        # -----------------------------------------------------------------------------    
        # LOG PARAMETER REGISTRY
        # ----------------------------------------------------------------------------- 
        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(
            json.dumps(PARAMETER_REGISTRY, indent=2)
        ))
    
    # -----------------------------------------------------------------------------
    # GMM HELPER FUNCTION (Defined inside class scope for best practice)
    # -----------------------------------------------------------------------------
    def make_gmm_model(self):
        """fn: create gmm object using instance parameters"""
        model_kwds = dict(n_components=self.N_COMPONENTS, 
                          max_iter=self.MAX_ITER,
                          n_init=self.N_INIT,
                          init_params='random',
                          random_state=self.RANDOM_STATE)

        gmm = GaussianMixture(**model_kwds)
        return gmm
    
    def update_prices(self):
        """fn: to update prices in an efficient manner"""
        
        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time
        
        # request only days that are missing from our dataset
        days_to_request = (current_date - most_recent_date).days
        
        # if prices up to date return 
        if days_to_request == 0:
            return
        
        # get prices (Using Symbol objects)
        new_prices = (self.History(self.symbols, days_to_request, Resolution.Daily)
                      ["close"]
                      .unstack(level=0)
                      .astype(np.float32))
                      
        # FIX: Ensure proper updating and slicing based on LOOKBACK
        self.prices = pd.concat([self.prices, new_prices])
        self.prices = self.prices.drop_duplicates().sort_index().iloc[-self.LOOKBACK:]
        return
    
    def check_liquidate(self):
        """
        fn: to check if todays date matches exit date and liquidate
        FIXED: Uses robust GetOrderTickets() and ORDER_STATUS_FILLED (3).
        """
        self.Log('\n'+'-'*77+'\n[{}] checking liquidation status...'.format(self.UtcTime))
                
        all_tickets = self.Transactions.GetOrderTickets()
        
        for holding in self.Portfolio.Values:
            if not holding.Invested:
                continue 
            
            symbol_tickets = [t for t in all_tickets if t.Symbol == holding.Symbol]
            latest_fill_event = None
            
            for ticket in symbol_tickets:
                # Use Status == ORDER_STATUS_FILLED (3)
                fill_events = [e for e in ticket.OrderEvents if e.Status == ORDER_STATUS_FILLED and e.FillQuantity > 0]

                if fill_events:
                    current_latest_fill = max(fill_events, key=lambda x: x.UtcTime)
                    
                    if latest_fill_event is None or current_latest_fill.UtcTime > latest_fill_event.UtcTime:
                        latest_fill_event = current_latest_fill
            
            if latest_fill_event:
                entry_time = latest_fill_event.UtcTime
                
                if self.UtcTime >= (entry_time + timedelta(self.HOLDING_PERIOD)):
                    self.Liquidate(holding.Symbol)
                    fmt_args = (self.UtcTime, holding.Symbol.Value, entry_time, self.UtcTime - entry_time)
                    self.Log('[{}] liquidating {}, order date: {}, time delta: {}'.format(*fmt_args))
        return
    
    def compute(self, sym_obj):
        """fn: computation for bootstrapped confidence intervals for individual symbol"""
        
        sym = sym_obj.Value # Ticker string for pandas lookup
        
        # Ensure prices for the symbol exist
        if sym not in self.prices.columns:
            self.Debug(f"Price data missing for {sym}.")
            return None
            
        train_px = self.prices[sym]
        train_df = make_returns(train_px).to_frame() # Ensure DataFrame for returns
        tmp_x = train_df.values.reshape(-1, 1)
        
        if tmp_x.shape[0] < self.N_COMPONENTS + 1:
            self.Debug(f"Insufficient data for GMM fit on {sym}.")
            return None
        
        ### fit GMM ###
        gmm = self.make_gmm_model().fit(tmp_x)
        hidden_states = gmm.predict(tmp_x)
    
        ### get last state estimate ###
        last_state = hidden_states[-1]
        # Ensure we access the mean/var as a scalar if it's 1D
        last_mean = gmm.means_[last_state][0] 
        last_var = np.diag(gmm.covariances_[last_state])[0]

        ### sample from distribution using last state parameters ###
        
        # Since the original code selected 'normal distribution'
        rvs = stats.norm.rvs(loc=last_mean, scale=np.sqrt(last_var), 
                             size=self.N_SAMPLES, random_state=self.RANDOM_STATE)
        
        # FIX: Use 'confidence' keyword for SciPy interval
        low_ci, high_ci = stats.norm.interval(confidence=self.ALPHA,
                                              loc=np.mean(rvs), scale=np.std(rvs))
        
        ## get current return (approximate open return) ##
        tmp_ret = np.log(float(self.Securities[sym_obj].Price) / train_px.iloc[-1])
        
        r_gt = (tmp_ret > high_ci)
        r_lt = (tmp_ret < low_ci)
        if r_gt: result_tag = 'too_high'
        elif r_lt: result_tag = 'too_low'
        else: result_tag = 'hit'
        
        ### row order: (symbol, low ci, high ci, current return, result_tag) ###
        sym_row = (sym_obj, low_ci, high_ci, tmp_ret, result_tag)
        return sym_row
        
    def run_main_algo(self):
        """fn: run main algorithm computation"""
        
        start_time = time.time()
    
        self.Log('\n'+'-'*77+'\n[{}] Begin main algo computation...'.format(self.UtcTime))
        
        ### set buy/sell lists to False to confirm no carryover ###
        self._longs = False
        self._shorts = False
        
        ### update prices ###
        self.update_prices()
        
        ### compute data ###
        # Iterate over Symbol objects and filter out invested positions
        tmp_data_list = [self.compute(asset_obj) 
                         for asset_obj in self.symbols
                         if not self.Portfolio[asset_obj].Invested and self.compute(asset_obj) is not None]
        
        ### construct long/short arrays ###
        if tmp_data_list:
            cols = ['symbol', 'low_ci', 'high_ci', 'current_return', 'result_tag']
            df = (pd.DataFrame(tmp_data_list, columns=cols))
          
            self.Log('[{}] algo data:\n\t{}'.format(self.UtcTime, df)) 
            
            # Mean Reversion Strategy (Original logic: Buy low)
            # Use symbol objects in the final list
            self._longs = np.asarray(df.query('result_tag=="too_low"')['symbol'].unique())
            
            # Breakout Strategy (Original logic was commented out, leaving short strategy out 
            # based on IS_LONG_ONLY = True, but keeping structure).
            if not self.IS_LONG_ONLY:
                 self._shorts = np.asarray(df.query('result_tag=="too_high"')['symbol'].unique())
            else:
                 self._shorts = np.array([]) # Ensure it's an empty numpy array
                
            log_str = (self.UtcTime, [s.Value for s in self._longs], [s.Value for s in self._shorts])
            self.Log('\n'+'-'*77+'\n[{0}] longs: {1}\n[{0}] shorts: {2}'.format(*log_str))
        else:
            self.Log('[{}] already fully invested or insufficient data, exiting...'.format(self.UtcTime))
            
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, 'Time', self.time_to_run_main_algo)
        return
    
    def send_orders(self):
        """fn: send orders"""
        
        self.Log('\n'+'-'*77+'\n[{}] checking L/S arrays to send orders...'.format(self.UtcTime))
        
        # SHORT ORDERS
        if isinstance(self._shorts, np.ndarray) and self._shorts.size:
            for sym_obj in self._shorts:
                if not self.Portfolio[sym_obj].Invested:   
                    self.Log('[{}] sending short order for {}...'.format(self.UtcTime, sym_obj.Value))
                    # FIX: Use MarketOrder for T+30min scheduling
                    short_shares = self.CalculateOrderQuantity(sym_obj, -self.LEVERAGE*self.BET_SIZE)
                    self.MarketOrder(sym_obj, short_shares)
        else:
            self.Log('[{}] no shorts listed, no orders sent...'.format(self.UtcTime))
        
        # LONG ORDERS    
        if isinstance(self._longs, np.ndarray) and self._longs.size:
            for sym_obj in self._longs:
                if not self.Portfolio[sym_obj].Invested: 
                    self.Log('[{}] sending long order for {}...'.format(self.UtcTime, sym_obj.Value))
                    # FIX: Use MarketOrder for T+30min scheduling
                    long_shares = self.CalculateOrderQuantity(sym_obj, self.LEVERAGE*self.BET_SIZE)
                    self.MarketOrder(sym_obj, long_shares)
        else:
            self.Log('[{}] no longs listed, no orders sent...'.format(self.UtcTime))
                        
        return 

    def CHART_RAM(self):
        """fn: to track Ram, Computation Time, Leverage, Cash"""
        self.Plot(self.splotName,'RAM', OS.ApplicationMemoryUsed/1024.)
        self.Plot(self.splotName,'Time', self.time_to_run_main_algo)

        P = self.Portfolio
        if P.TotalPortfolioValue != 0:
            self.track_leverage = P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            self.Plot(self.splotName, 'Leverage', float(self.track_leverage))
        
        self.Plot(self.splotName, 'Cash', float(self.Portfolio.Cash))   
        
    def OnData(self, data):
        '''OnData event is the primary entry point for your algorithm.'''
        pass
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

from AlgorithmImports import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn import mixture as mix

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

def make_returns(df):
    """Calculate log returns for a price DataFrame."""
    return np.log(df / df.shift(1))

def make_gmm(n_components, random_state):
    """Factory function for GaussianMixtureModel."""
    return mix.GaussianMixture(n_components=n_components, random_state=random_state)
    
def make_final_pred_df(pred_rows, cols, thres, sym):
    """Process prediction rows into a DataFrame and apply trading logic."""
    df = pd.DataFrame(pred_rows, columns=cols)
    df['symbol'] = sym
    
    # Determine which class is the 'high-return' state by comparing means
    high_mean_class = 1 if df['last_mean_class_1'].iloc[0] > df['last_mean_class_0'].iloc[0] else 0
    
    if high_mean_class == 1:
        # Check if the probability of the high-return state (Class 1) > threshold
        df['buys'] = np.where(df['last_prob_class_1'] > thres, 1, 0)
    else:
        # Check if the probability of the high-return state (Class 0) > threshold
        df['buys'] = np.where(df['last_prob_class_0'] > thres, 1, 0)
        
    return df.tail(1)

# --- Other Utilities (Remaining functions from algo_utils, unused by main logic) ---

def calc_quantile_var(data, alpha=0.05): return data.quantile(alpha)
def calc_historical_var(data, alpha=0.05): 
    if isinstance(data, pd.DataFrame): data = data.squeeze()
    return calc_quantile_var(data, alpha=alpha)
def get_open_order_secs(open_orders): 
    if open_orders: return [order.Symbol for order in open_orders]
    return []

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------

class TradingWithGMM(QCAlgorithm):
    def Initialize(self):
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)        

        self.SetStartDate(2007,4,10)
        self.SetEndDate(2025, 10, 13)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)
        
        #self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
        #                       AccountType.Margin)

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)

        # Correct Symbol Handling
        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', 'SPY')   
        self.tickers = [self.BASE_SYMBOL_TICKER, "QQQ", "DIA", "TLT", "GLD", "EFA", "EEM", "BND", "VNQ"]
        
        self.symbols = []
        for ticker in self.tickers:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol) 

        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        self.openMarketOnOpenOrders = []
        self._init_prices = False
        self._longs = list() 
        self._shorts = list()
        register_param('symbols: ', [s.Value for s in self.symbols])
        self._holding_period = register_param('holding period (days)', 30)
        self.LOOKBACK = register_param('historical lookback (days)', 252*3)
        self.BET_SIZE = register_param('bet size (%)', 1/len(self.symbols))
    
        self.RANDOM_STATE = register_param('random_state', 777)
        self.ALPHA = register_param('gmm alpha', 0.95) 
        self.N_COMPONENTS = register_param('gmm n components', 2)        
        self.THRES = register_param('threshold probability for buy signal', 0.9) # Reverted to 0.9
        self.SAMPLES = register_param('number of samples for bootstrap', 1000)
        self.HISTORY_RESOLUTION = Resolution.Daily 
        register_param('history api resolution', str(self.HISTORY_RESOLUTION)) 

        # Charting setup remains the same
        self.splotName = 'Strategy Info'
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series('RAM',  SeriesType.Line, 0))
        sPlot.AddSeries(Series('Time',  SeriesType.Line, 1))
        sPlot.AddSeries(Series('Cash',  SeriesType.Line, 2))
        sPlot.AddSeries(Series('Leverage',  SeriesType.Line, 3))
        self.AddChart(sPlot)
        self.time_to_run_main_algo = 0

    
        # Scheduled functions
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5), Action(self.init_prices))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10), Action(self.run_main_algo))
        
        # Changed to MarketOrder for execution at T+30 min (see send_orders)
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 30), Action(self.send_orders)) 
        
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 35), Action(self.check_liquidate))
        self.Schedule.On(self.DateRules.EveryDay(self.BASE_SYMBOL), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40), Action(self.CHART_RAM))


        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))

    def init_prices(self):
        if not self.symbols: self.Log('no symbols'); return
        if self._init_prices: return 
        
        self.prices = (self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self._init_prices=True
       
    def update_prices(self):
        most_recent_date = self.prices.index.max()
        current_date = self.Time
        days_to_request = how_many_days(current_date, most_recent_date)
        if zero_days_to_request(days_to_request): return

        new_prices = (self.History(self.symbols, days_to_request, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return
    
    def check_liquidate(self):
        """
        Robust liquidation check using GetOrderTickets() and manual filtering.
        FIX: Uses the integer value of OrderEventStatus.Filled (which is 3).
        """
        self.Log('\n'+'-'*77+'\n[{}] checking liquidation status...'.format(self.UtcTime))
                
        # Retrieve ALL order tickets once
        all_tickets = self.Transactions.GetOrderTickets()
        
        for holding in self.Portfolio.Values:
            if not holding.Invested:
                continue 
            
            # Filter all tickets to find those matching the current holding's Symbol
            symbol_tickets = [t for t in all_tickets if t.Symbol == holding.Symbol]
            
            latest_fill_event = None
            
            # Find the latest FILL event that was a BUY order
            for ticket in symbol_tickets:
                # FIX: Use the raw integer value of the status (OrderEventStatus.Filled == 3)
                fill_events = [e for e in ticket.OrderEvents if e.Status == ORDER_STATUS_FILLED and e.FillQuantity > 0]

                if fill_events:
                    # Find the latest fill event across all tickets
                    current_latest_fill = max(fill_events, key=lambda x: x.UtcTime)
                    
                    if latest_fill_event is None or current_latest_fill.UtcTime > latest_fill_event.UtcTime:
                        latest_fill_event = current_latest_fill
            
            if latest_fill_event:
                entry_time = latest_fill_event.UtcTime
                
                # Check if the current time is past the entry time + holding period
                if self.UtcTime >= (entry_time + timedelta(self._holding_period)):
                    self.Liquidate(holding.Symbol)
                    fmt_args = (self.UtcTime, holding.Symbol.Value, entry_time, self.UtcTime - entry_time)
                    self.Log('[{}] liquidating... {}, order date: {}, time delta: {}'.format(*fmt_args))
        return

    def run_main_algo(self):
        self.Log('\n'+'-'*77+'\n[{}] Begin main algorithm computation...'.format(self.UtcTime))
        
        start_time = time.time()
        self.update_prices()
        self._algo_data = False
        self._longs = list() 
        self._shorts = list()

        for sym_obj in self.symbols: 
            sym = sym_obj.Value
            
            try:
                self.Log('checking symbol: {}'.format(str(sym)))
                pred_rows = list()
                
                # Check if we should perform the GMM calculation
                if (not self.Portfolio[sym_obj].Invested):
                    
                    if sym not in self.prices.columns: continue
                        
                    train_px = self.prices.copy()
                    train_ts = make_returns(train_px)[sym].dropna()
                    train_ts = train_ts[np.isfinite(train_ts)]
                    
                    if train_ts.shape[0] < 50: continue

                    tmp_X_train = train_ts.values.reshape(-1, 1)
            
                    ### fit GMM ###
                    gmm = make_gmm(n_components=self.N_COMPONENTS, random_state=self.RANDOM_STATE).fit(tmp_X_train)
                    hidden_states = gmm.predict(tmp_X_train)
                    hidden_state_prob = pd.DataFrame(gmm.predict_proba(tmp_X_train), columns=['s1','s2'], index=train_ts.index)
                    
                    state_df = train_ts.to_frame()
                    hs_prob_df = (pd.concat([state_df, hidden_state_prob],axis=1))
                    
                    # Variables are defined here:
                    s1_mu = hs_prob_df.query('abs(s1)>0.5')[sym].mean() 
                    s2_mu = hs_prob_df.query('abs(s2)>0.5')[sym].mean() 
                    s1_std = hs_prob_df.query('abs(s1)>0.5')[sym].std() 
                    s2_std = hs_prob_df.query('abs(s2)>0.5')[sym].std()           
                    
                    # GMM state and interval calculation
                    last_state = hidden_states[-1]
                    last_mean = gmm.means_[last_state][0]
                    last_var = np.diag(gmm.covariances_[last_state])[0]
                    
                    rvs = gmm.sample(self.SAMPLES)[0] 
                    # Corrected argument name for scipy.stats compatibility
                    low_ci, high_ci = stats.norm.interval(confidence=self.ALPHA, loc=np.mean(rvs), scale=np.std(rvs))
            
                    tmp_ret = np.log(float(self.Securities[sym_obj].Price) / train_px[sym].iloc[-1])
                            
                    ### Row creation is now safely inside the calculation block
                    row = (train_ts.index[-1], last_state, last_mean, np.sqrt(last_var), 
                            low_ci, high_ci, tmp_ret,
                            gmm.means_.ravel()[0], gmm.means_.ravel()[1],
                            np.sqrt(np.diag(gmm.covariances_[0]))[0], np.sqrt(np.diag(gmm.covariances_[1]))[0],
                            hidden_state_prob.iloc[-1][0], hidden_state_prob.iloc[-1][1],
                            s1_mu,s2_mu,s1_std,s2_std)
                    pred_rows.append(row)
                    self.Debug('{} rowzz:\n{}'.format(str(sym), row))
                    
                if pred_rows:
                    cols = ['Dates', 'ith_state', 'ith_ret','ith_std', 'low_ci', 'high_ci', 'current_return',
                            'last_mean_class_0', 'last_mean_class_1', 'last_std_class_0', 'last_std_class_1',
                            'last_prob_class_0', 'last_prob_class_1', 'avg_class_0_mean', 'avg_class_1_mean',
                            'avg_class_0_std', 'avg_class_1_std']             
                    
                    pred_df = make_final_pred_df(pred_rows, cols, self.THRES, sym)
                    
                    if pred_df.iloc[-1].loc['buys']==1: 
                        self._longs.append(sym_obj)
                        self.Debug('>>> BUY SIGNAL GENERATED for {} (Prob > {}) <<<'.format(sym, self.THRES))

                else:
                    self.Debug('missing or invested in {}'.format(sym))
                    
            except Exception as e:
                self.Debug('{} error: {}'.format(sym, e))
                continue

        self.Debug('Final Longs List for next order run: {}'.format([s.Value for s in self._longs])) 

        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, 'Time', self.time_to_run_main_algo)                
        return 
    
    def send_orders(self):
        self.Log('\n'+'-'*77+'\n[{}] checking buy sell arrays to send orders...'.format(self.UtcTime))

        if self._longs:
            for sym_obj in self._longs:
                if not self.Portfolio[sym_obj].Invested:
                    # Use MarketOrder for execution 30 minutes after open
                    self.Log('[{}] SENDING MARKET ORDER for {}...'.format(self.UtcTime, sym_obj.Value))                        
                    self.MarketOrder(sym_obj, self.CalculateOrderQuantity(sym_obj, self.BET_SIZE))
                else:
                    self.Debug('Skipping {} - Already invested.'.format(sym_obj.Value))
        else:
            self.Log('send_orders >> no longs listed, no orders sent...')            
        return
    
    
    def OnData(self, data):
        pass

    def CHART_RAM(self):
        self.Plot(self.splotName,'RAM', OS.ApplicationMemoryUsed/1024.)
        P = self.Portfolio
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = P.TotalAbsoluteHoldingsCost/P.TotalPortfolioValue
            self.Plot(self.splotName, 'Leverage', float(self.track_account_leverage))
        
        self.Plot(self.splotName, 'Cash', float(self.Portfolio.Cash))
        return

# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn import mixture as mix

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# NOTE: Only necessary functions for this benchmark are retained.
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    # Ensures compatibility with unstacked History output
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------


class Benchmark6040(QCAlgorithm):
    """
    60/40 Equity/Bond Benchmark Strategy, matched to standard QC structure.
    """

    def Initialize(self):
        """Initial algorithm settings"""

        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)

        self.SetStartDate(2007,12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)  # Set Strategy Cash

        # -----------------------------------------------------------------------------
        # init brokerage model
        # -----------------------------------------------------------------------------

        #self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
        #                       AccountType.Margin)

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)

        # -----------------------------------------------------------------------------
        # init custom universe (FIXED to use Symbol objects)
        # -----------------------------------------------------------------------------

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', "SPY")
        self.ticker_strings = [self.BASE_SYMBOL_TICKER, "BND"]
        
        # Store Symbol objects for use in History and SetHoldings
        self.symbols = [] 
        for ticker in self.ticker_strings:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol)

        # Base symbol must be the Symbol object for scheduling
        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol

        # -----------------------------------------------------------------------------
        # Algo Exchange Settings
        # -----------------------------------------------------------------------------

        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        # -----------------------------------------------------------------------------
        # other algo parameter settings
        # -----------------------------------------------------------------------------

        self._init_prices = False

        self.LOOKBACK = register_param('historical lookback (days)', 252) # trading days
        self.LEVERAGE = register_param('leverage', 1.0)
        
        self.TARGET_WEIGHTS_TICKERS = {"SPY": 0.6, "BND": 0.4}
        register_param('target weights', self.TARGET_WEIGHTS_TICKERS)
        
        self.TOLERANCE = register_param('rebalance tolerance', 0.025)

        self.RANDOM_STATE = register_param('random_state', 7)

        ## set resolution for historical data calls
        self.HISTORY_RESOLUTION = Resolution.Daily
        register_param('history api resolution', str(self.HISTORY_RESOLUTION))


        # -----------------------------------------------------------------------------
        # track RAM and computation time for main func, also leverage and cash
        # -----------------------------------------------------------------------------

        self.splotName = "Strategy Info"
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series("RAM", SeriesType.Line, 0))
        sPlot.AddSeries(Series("Time", SeriesType.Line, 1))
        sPlot.AddSeries(Series("Cash", SeriesType.Line, 2))
        sPlot.AddSeries(Series("Leverage", SeriesType.Line, 3))
        self.AddChart(sPlot)

        self.time_to_run_main_algo = 0

        # -----------------------------------------------------------------------------
        # track portfolio weights by symbol
        # -----------------------------------------------------------------------------

        self.splotName3 = "Security Weights Info"
        sPlot3 = Chart(self.splotName3)

        # Use ticker strings for chart series names
        for i, sec in enumerate(self.ticker_strings):
            sPlot3.AddSeries(Series(sec, SeriesType.Line, i))

        self.AddChart(sPlot3)

        # -----------------------------------------------------------------------------
        # scheduled functions (Using BASE_SYMBOL object)
        # -----------------------------------------------------------------------------

        # make buy list
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5),
            Action(self.init_prices),
        )

        # Rebalance monthly
        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.rebalance),
        )

        # plot RAM
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM),
        )

        # plot weights by asset symbol
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.BeforeMarketClose(self.BASE_SYMBOL, 70),
            Action(self.CHART_SECURITY_WEIGHTS),
        )
        
        # Log parameters
        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))


    def init_prices(self):
        """
        Initialize historical prices.
        """
        if not self.symbols:
            self.Log("no symbols")
            return

        if self._init_prices:
            return
        
        # Pass list of Symbol objects for History API
        self.prices = (
            self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"]
            .unstack(level=0)
            .astype(np.float32)
        )
        self._init_prices = True
        return

    def update_prices(self):
        """
        Update prices efficiently using integrated utility functions.
        """

        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time

        # how many periods do we need (using integrated logic)
        days_to_request = how_many_days(current_date, most_recent_date)

        # if prices up to date return (using integrated logic)
        if zero_days_to_request(days_to_request):
            return

        # get new data (using Symbol objects)
        new_prices = self.History(
            self.symbols, days_to_request, self.HISTORY_RESOLUTION
        )
        
        if "close" in new_prices.columns:
            # new_prices is unstacked inside make_update_df for clean code
            new_prices = new_prices["close"].unstack(level=0).astype(np.float32)
        else:
            return
            
        # combine datasets using integrated utility
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return

    def check_current_weight(self, symbol):
        """
        Check symbol's current weight.
        Accepts Symbol object.
        """
        P = self.Portfolio
        
        if P.TotalPortfolioValue == 0:
            return 0.0

        # FIX: Get the security object using the Symbol object
        security = self.Securities[symbol]
        
        # FIX: Access HoldingsValue via the Security object
        current_weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight

    def rebalance(self):
        """fn: run main algorithm (Rebalancing)"""
        self.Log(
            "\n"
            + "-" * 77
            + "\n[{}] Begin main algorithm computation...".format(self.UtcTime)
        )

        start_time = time.time()  # timer
        self.update_prices()  # update prices

        # Iterate over Symbol objects
        for sym_obj in self.symbols:
            # Use ticker string to look up target weight
            ticker = sym_obj.Value
            
            # get current weights
            current_weight = self.check_current_weight(sym_obj)
            target_weight = self.TARGET_WEIGHTS_TICKERS.get(ticker, 0.0)

            # if current weights outside of tolerance send new orders
            tol = self.TOLERANCE * target_weight
            lower_bound = target_weight - tol
            upper_bound = target_weight + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                # Use Symbol object for SetHoldings
                self.SetHoldings(sym_obj, target_weight)

        ## end timer
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, "Time", self.time_to_run_main_algo)
        return

    def OnData(self, data):
        """OnData event is the primary entry point for your algorithm.
        Each new data point will be pumped in here."""
        pass

    def CHART_RAM(self):
        # Once a day or something reasonable to prevent spam
        self.Plot(self.splotName, "RAM", OS.ApplicationMemoryUsed / 1024.0)
        P = self.Portfolio
        
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = (
                P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            )
            self.Plot(self.splotName, "Leverage", float(self.track_account_leverage))
        
        self.Plot(self.splotName, "Cash", float(self.Portfolio.Cash))
        return

    def CHART_SECURITY_WEIGHTS(self):
        """Plots the current weight of each security."""
        P = self.Portfolio
        
        # Iterate over Symbol objects
        for sym_obj in self.symbols:
            
            # FIX: Access the Security object first
            security = self.Securities[sym_obj] 
            
            # Ensure TotalPortfolioValue is not zero before division
            weight = 0.0
            if P.TotalPortfolioValue != 0:
                 # Access HoldingsValue via the Security object
                 weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue) * 100
            
            # Plot using the ticker string (Symbol.Value) as the series name
            self.Plot(self.splotName3, sym_obj.Value, weight)
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn.mixture import BayesianGaussianMixture 

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

def make_returns(df):
    """Calculate log returns for a price DataFrame."""
    return np.log(df / df.shift(1))

# Update make_gmm to use BayesianGaussianMixture
def make_gmm(n_components, weight_concentration_prior=None):
    """Factory function for BayesianGaussianMixture."""
    if weight_concentration_prior is not None:
        return BayesianGaussianMixture(n_components=n_components, 
                                       weight_concentration_prior=weight_concentration_prior)
    else:
        return BayesianGaussianMixture(n_components=n_components)
    
def make_final_pred_df(pred_rows, cols, thres, sym):
    """Process prediction rows into a DataFrame and apply trading logic."""
    df = pd.DataFrame(pred_rows, columns=cols)
    df['symbol'] = sym
    
    # Determine which class is the 'high-return' state by comparing means
    high_mean_class = 1 if df['last_mean_class_1'].iloc[0] > df['last_mean_class_0'].iloc[0] else 0
    
    if high_mean_class == 1:
        # Check if the probability of the high-return state (Class 1) > threshold
        df['buys'] = np.where(df['last_prob_class_1'] > thres, 1, 0)
    else:
        # Check if the probability of the high-return state (Class 0) > threshold
        df['buys'] = np.where(df['last_prob_class_0'] > thres, 1, 0)
        
    return df.tail(1)

# --- Other Utilities (Remaining functions from algo_utils, unused by main logic) ---

def calc_quantile_var(data, alpha=0.05): return data.quantile(alpha)
def calc_historical_var(data, alpha=0.05): 
    if isinstance(data, pd.DataFrame): data = data.squeeze()
    return calc_quantile_var(data, alpha=alpha)
def get_open_order_secs(open_orders): 
    if open_orders: return [order.Symbol for order in open_orders]
    return []

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------

class TradingWithBayesianGMM(QCAlgorithm):
    def Initialize(self):
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)        

        self.SetStartDate(2007,12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)
        
        self.SetBrokerageModel(BrokerageName.ALPACA, AccountType.Margin)

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', 'SPY')   
        self.tickers = ["SPY", "QQQ", "DIA", "TLT", "GLD", "EFA", "EEM", "BND", "VNQ"]
        
        self.symbols = []
        for ticker in self.tickers:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol) 

        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        self.openMarketOnOpenOrders = []
        self._init_prices = False
        self._longs = list() 
        self._shorts = list()
        register_param('symbols: ', [s.Value for s in self.symbols])
        self._holding_period = register_param('holding period (days)', 30)
        
        # MODIFICATION: Set lookback to 252 days
        self.LOOKBACK = register_param('historical lookback (days)', 252)
        
        self.BET_SIZE = register_param('bet size (%)', 1/len(self.symbols))
    
        self.ALPHA = register_param('gmm alpha (for CI)', 0.95) 
        
        self.N_COMPONENTS = register_param('bgmm n components', 2) 
        self.WEIGHT_CONCENTRATION_PRIOR = register_param('bgmm weight concentration prior', 0.1) 

        self.THRES = register_param('threshold probability for buy signal', 0.9) 
        self.SAMPLES = register_param('number of samples for bootstrap', 1000)
        self.HISTORY_RESOLUTION = Resolution.Daily 
        register_param('history api resolution', str(self.HISTORY_RESOLUTION)) 

        # Charting setup remains the same
        self.splotName = 'Strategy Info'
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series('RAM',  SeriesType.Line, 0))
        sPlot.AddSeries(Series('Time',  SeriesType.Line, 1))
        sPlot.AddSeries(Series('Cash',  SeriesType.Line, 2))
        sPlot.AddSeries(Series('Leverage',  SeriesType.Line, 3))
        self.AddChart(sPlot)
        self.time_to_run_main_algo = 0

        # Scheduled functions
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5), Action(self.init_prices))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10), Action(self.run_main_algo))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 30), Action(self.send_orders)) 
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 35), Action(self.check_liquidate))
        self.Schedule.On(self.DateRules.EveryDay(self.BASE_SYMBOL), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40), Action(self.CHART_RAM))

        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))

    def init_prices(self):
        if not self.symbols: self.Log('no symbols'); return
        if self._init_prices: return 
        
        self.prices = (self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self._init_prices=True
       
    def update_prices(self):
        most_recent_date = self.prices.index.max()
        current_date = self.Time
        days_to_request = how_many_days(current_date, most_recent_date)
        if zero_days_to_request(days_to_request): return

        new_prices = (self.History(self.symbols, days_to_request, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return
    
    def check_liquidate(self):
        """
        Robust liquidation check using GetOrderTickets() and manual filtering.
        Uses the integer value of OrderEventStatus.Filled (which is 3).
        """
        self.Log('\n'+'-'*77+'\n[{}] checking liquidation status...'.format(self.UtcTime))
                
        all_tickets = self.Transactions.GetOrderTickets()
        
        for holding in self.Portfolio.Values:
            if not holding.Invested:
                continue 
            
            symbol_tickets = [t for t in all_tickets if t.Symbol == holding.Symbol]
            
            latest_fill_event = None
            
            for ticket in symbol_tickets:
                # Use the raw integer value of the status (OrderEventStatus.Filled == 3)
                fill_events = [e for e in ticket.OrderEvents if e.Status == ORDER_STATUS_FILLED and e.FillQuantity > 0]

                if fill_events:
                    current_latest_fill = max(fill_events, key=lambda x: x.UtcTime)
                    
                    if latest_fill_event is None or current_latest_fill.UtcTime > latest_fill_event.UtcTime:
                        latest_fill_event = current_latest_fill
            
            if latest_fill_event:
                entry_time = latest_fill_event.UtcTime
                
                if self.UtcTime >= (entry_time + timedelta(self._holding_period)):
                    self.Liquidate(holding.Symbol)
                    fmt_args = (self.UtcTime, holding.Symbol.Value, entry_time, self.UtcTime - entry_time)
                    self.Log('[{}] liquidating... {}, order date: {}, time delta: {}'.format(*fmt_args))
        return

    def run_main_algo(self):
        self.Log('\n'+'-'*77+'\n[{}] Begin main algorithm computation...'.format(self.UtcTime))
        
        start_time = time.time()
        self.update_prices()
        self._algo_data = False
        self._longs = list() 
        self._shorts = list()

        for sym_obj in self.symbols: 
            sym = sym_obj.Value
            
            try:
                self.Log('checking symbol: {}'.format(str(sym)))
                pred_rows = list()
                
                if (not self.Portfolio[sym_obj].Invested):
                    
                    if sym not in self.prices.columns: continue
                        
                    train_px = self.prices.copy()
                    train_ts = make_returns(train_px)[sym].dropna()
                    train_ts = train_ts[np.isfinite(train_ts)]
                    
                    if train_ts.shape[0] < self.N_COMPONENTS + 1:
                         self.Debug('{} train data has too few samples (<{})'.format(str(sym), self.N_COMPONENTS + 1))
                         continue

                    tmp_X_train = train_ts.values.reshape(-1, 1)
            
                    ### fit Bayesian GMM ###
                    bgmm = make_gmm(n_components=self.N_COMPONENTS, 
                                    weight_concentration_prior=self.WEIGHT_CONCENTRATION_PRIOR).fit(tmp_X_train)
                    hidden_states = bgmm.predict(tmp_X_train)
                    hidden_state_prob = pd.DataFrame(bgmm.predict_proba(tmp_X_train), columns=['s1','s2'], index=train_ts.index)
                    
                    state_df = train_ts.to_frame()
                    hs_prob_df = (pd.concat([state_df, hidden_state_prob],axis=1))
                    
                    s1_mu = hs_prob_df.query('abs(s1)>0.5')[sym].mean() 
                    s2_mu = hs_prob_df.query('abs(s2)>0.5')[sym].mean() 
                    s1_std = hs_prob_df.query('abs(s1)>0.5')[sym].std() 
                    s2_std = hs_prob_df.query('abs(s2)>0.5')[sym].std()           
                    
                    last_state = hidden_states[-1]
                    last_mean = bgmm.means_[last_state][0]
                    last_var = np.diag(bgmm.covariances_[last_state])[0]
                    
                    rvs = bgmm.sample(self.SAMPLES)[0] 
                    low_ci, high_ci = stats.norm.interval(confidence=self.ALPHA, loc=np.mean(rvs), scale=np.std(rvs))
            
                    tmp_ret = np.log(float(self.Securities[sym_obj].Price) / train_px[sym].iloc[-1])
                            
                    row = (train_ts.index[-1], last_state, last_mean, np.sqrt(last_var), 
                            low_ci, high_ci, tmp_ret,
                            bgmm.means_.ravel()[0], bgmm.means_.ravel()[1],
                            np.sqrt(np.diag(bgmm.covariances_[0]))[0], np.sqrt(np.diag(bgmm.covariances_[1]))[0],
                            hidden_state_prob.iloc[-1][0], hidden_state_prob.iloc[-1][1],
                            s1_mu,s2_mu,s1_std,s2_std)
                    pred_rows.append(row)
                    self.Debug('{} rowzz:\n{}'.format(str(sym), row))
                    
                if pred_rows:
                    cols = ['Dates', 'ith_state', 'ith_ret','ith_std', 'low_ci', 'high_ci', 'current_return',
                            'last_mean_class_0', 'last_mean_class_1', 'last_std_class_0', 'last_std_class_1',
                            'last_prob_class_0', 'last_prob_class_1', 'avg_class_0_mean', 'avg_class_1_mean',
                            'avg_class_0_std', 'avg_class_1_std']             
                    
                    pred_df = make_final_pred_df(pred_rows, cols, self.THRES, sym)
                    
                    if pred_df.iloc[-1].loc['buys']==1: 
                        self._longs.append(sym_obj)
                        self.Debug('>>> BUY SIGNAL GENERATED for {} (Prob > {}) <<<'.format(sym, self.THRES))

                else:
                    self.Debug('missing or invested in {}'.format(sym))
                    
            except Exception as e:
                self.Debug('{} error: {}'.format(sym, e))
                continue

        self.Debug('Final Longs List for next order run: {}'.format([s.Value for s in self._longs])) 

        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, 'Time', self.time_to_run_main_algo)                
        return 
    
    def send_orders(self):
        self.Log('\n'+'-'*77+'\n[{}] checking buy sell arrays to send orders...'.format(self.UtcTime))

        if self._longs:
            for sym_obj in self._longs:
                if not self.Portfolio[sym_obj].Invested:
                    self.Log('[{}] SENDING MARKET ORDER for {}...'.format(self.UtcTime, sym_obj.Value))                        
                    self.MarketOrder(sym_obj, self.CalculateOrderQuantity(sym_obj, self.BET_SIZE))
                else:
                    self.Debug('Skipping {} - Already invested.'.format(sym_obj.Value))
        else:
            self.Log('send_orders >> no longs listed, no orders sent...')            
        return
    
    
    def OnData(self, data):
        pass

    def CHART_RAM(self):
        self.Plot(self.splotName,'RAM', OS.ApplicationMemoryUsed/1024.)
        P = self.Portfolio
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = P.TotalAbsoluteHoldingsCost/P.TotalPortfolioValue
            self.Plot(self.splotName, 'Leverage', float(self.track_account_leverage))
        
        self.Plot(self.splotName, 'Cash', float(self.Portfolio.Cash))
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix # Still used for the base GaussianMixture class type hint or if needed for other functionality
from datetime import datetime, timedelta
import time
import decimal as d
import json 
# MODIFICATION 1: Import BayesianGaussianMixture
from sklearn.mixture import BayesianGaussianMixture 

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

def make_returns(df):
    """Calculate log returns for a price DataFrame."""
    return np.log(df / df.shift(1))

# MODIFICATION 2: Update make_gmm to use BayesianGaussianMixture
# Removed random_state from constructor as it's not directly supported for BGM
def make_gmm(n_components, weight_concentration_prior=None):
    """Factory function for BayesianGaussianMixture."""
    if weight_concentration_prior is not None:
        return BayesianGaussianMixture(n_components=n_components, 
                                       weight_concentration_prior=weight_concentration_prior)
    else:
        return BayesianGaussianMixture(n_components=n_components)
    
def make_final_pred_df(pred_rows, cols, thres, sym):
    """Process prediction rows into a DataFrame and apply trading logic."""
    df = pd.DataFrame(pred_rows, columns=cols)
    df['symbol'] = sym
    
    # Determine which class is the 'high-return' state by comparing means
    high_mean_class = 1 if df['last_mean_class_1'].iloc[0] > df['last_mean_class_0'].iloc[0] else 0
    
    if high_mean_class == 1:
        # Check if the probability of the high-return state (Class 1) > threshold
        df['buys'] = np.where(df['last_prob_class_1'] > thres, 1, 0)
    else:
        # Check if the probability of the high-return state (Class 0) > threshold
        df['buys'] = np.where(df['last_prob_class_0'] > thres, 1, 0)
        
    return df.tail(1)

# --- Other Utilities (Remaining functions from algo_utils, unused by main logic) ---

def calc_quantile_var(data, alpha=0.05): return data.quantile(alpha)
def calc_historical_var(data, alpha=0.05): 
    if isinstance(data, pd.DataFrame): data = data.squeeze()
    return calc_quantile_var(data, alpha=alpha)
def get_open_order_secs(open_orders): 
    if open_orders: return [order.Symbol for order in open_orders]
    return []

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------

class TradingWithBayesianGMM(QCAlgorithm): # MODIFICATION 3: Renamed class for clarity
    def Initialize(self):
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)        

        self.SetStartDate(2007,12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)
        
        self.SetBrokerageModel(BrokerageName.ALPACA, AccountType.Margin)

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', 'SPY')   
        self.tickers = ["SPY", "QQQ", "DIA", "TLT", "GLD", "EFA", "EEM", "BND", "VNQ"]
        
        self.symbols = []
        for ticker in self.tickers:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol) 

        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        self.openMarketOnOpenOrders = []
        self._init_prices = False
        self._longs = list() 
        self._shorts = list()
        register_param('symbols: ', [s.Value for s in self.symbols])
        self._holding_period = register_param('holding period (days)', 30)
        
        self.LOOKBACK = register_param('historical lookback (days)', 60) # Keeping 60 days
        
        self.BET_SIZE = register_param('bet size (%)', 1/len(self.symbols))
    
        # MODIFICATION 4: Removed RANDOM_STATE from class scope as it's not passed to BGM directly
        # self.RANDOM_STATE = register_param('random_state', 777) 
        self.ALPHA = register_param('gmm alpha (for CI)', 0.95) 
        
        self.N_COMPONENTS = register_param('bgm n components', 2) # Keeping 2 components        
        
        # MODIFICATION 5: Added weight_concentration_prior for Bayesian GMM
        self.WEIGHT_CONCENTRATION_PRIOR = register_param('bgm weight concentration prior', 0.1) 

        self.THRES = register_param('threshold probability for buy signal', 0.9) 
        self.SAMPLES = register_param('number of samples for bootstrap', 1000)
        self.HISTORY_RESOLUTION = Resolution.Daily 
        register_param('history api resolution', str(self.HISTORY_RESOLUTION)) 

        # Charting setup remains the same
        self.splotName = 'Strategy Info'
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series('RAM',  SeriesType.Line, 0))
        sPlot.AddSeries(Series('Time',  SeriesType.Line, 1))
        sPlot.AddSeries(Series('Cash',  SeriesType.Line, 2))
        sPlot.AddSeries(Series('Leverage',  SeriesType.Line, 3))
        self.AddChart(sPlot)
        self.time_to_run_main_algo = 0

        # Scheduled functions
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5), Action(self.init_prices))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10), Action(self.run_main_algo))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 30), Action(self.send_orders)) 
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 35), Action(self.check_liquidate))
        self.Schedule.On(self.DateRules.EveryDay(self.BASE_SYMBOL), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40), Action(self.CHART_RAM))

        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))

    def init_prices(self):
        if not self.symbols: self.Log('no symbols'); return
        if self._init_prices: return 
        
        self.prices = (self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self._init_prices=True
       
    def update_prices(self):
        most_recent_date = self.prices.index.max()
        current_date = self.Time
        days_to_request = how_many_days(current_date, most_recent_date)
        if zero_days_to_request(days_to_request): return

        new_prices = (self.History(self.symbols, days_to_request, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return
    
    def check_liquidate(self):
        """
        Robust liquidation check using GetOrderTickets() and manual filtering.
        Uses the integer value of OrderEventStatus.Filled (which is 3).
        """
        self.Log('\n'+'-'*77+'\n[{}] checking liquidation status...'.format(self.UtcTime))
                
        all_tickets = self.Transactions.GetOrderTickets()
        
        for holding in self.Portfolio.Values:
            if not holding.Invested:
                continue 
            
            symbol_tickets = [t for t in all_tickets if t.Symbol == holding.Symbol]
            
            latest_fill_event = None
            
            for ticket in symbol_tickets:
                fill_events = [e for e in ticket.OrderEvents if e.Status == ORDER_STATUS_FILLED and e.FillQuantity > 0]

                if fill_events:
                    current_latest_fill = max(fill_events, key=lambda x: x.UtcTime)
                    
                    if latest_fill_event is None or current_latest_fill.UtcTime > latest_fill_event.UtcTime:
                        latest_fill_event = current_latest_fill
            
            if latest_fill_event:
                entry_time = latest_fill_event.UtcTime
                
                if self.UtcTime >= (entry_time + timedelta(self._holding_period)):
                    self.Liquidate(holding.Symbol)
                    fmt_args = (self.UtcTime, holding.Symbol.Value, entry_time, self.UtcTime - entry_time)
                    self.Log('[{}] liquidating... {}, order date: {}, time delta: {}'.format(*fmt_args))
        return

    def run_main_algo(self):
        self.Log('\n'+'-'*77+'\n[{}] Begin main algorithm computation...'.format(self.UtcTime))
        
        start_time = time.time()
        self.update_prices()
        self._algo_data = False
        self._longs = list() 
        self._shorts = list()

        for sym_obj in self.symbols: 
            sym = sym_obj.Value
            
            try:
                self.Log('checking symbol: {}'.format(str(sym)))
                pred_rows = list()
                
                if (not self.Portfolio[sym_obj].Invested):
                    
                    if sym not in self.prices.columns: continue
                        
                    train_px = self.prices.copy()
                    train_ts = make_returns(train_px)[sym].dropna()
                    train_ts = train_ts[np.isfinite(train_ts)]
                    
                    if train_ts.shape[0] < self.N_COMPONENTS + 1:
                         self.Debug('{} train data has too few samples (<{})'.format(str(sym), self.N_COMPONENTS + 1))
                         continue

                    tmp_X_train = train_ts.values.reshape(-1, 1)
            
                    ### fit Bayesian GMM ###
                    # MODIFICATION 6: Pass weight_concentration_prior
                    bgmm = make_gmm(n_components=self.N_COMPONENTS, 
                                    weight_concentration_prior=self.WEIGHT_CONCENTRATION_PRIOR).fit(tmp_X_train)
                    hidden_states = bgmm.predict(tmp_X_train)
                    hidden_state_prob = pd.DataFrame(bgmm.predict_proba(tmp_X_train), columns=['s1','s2'], index=train_ts.index)
                    
                    state_df = train_ts.to_frame()
                    hs_prob_df = (pd.concat([state_df, hidden_state_prob],axis=1))
                    
                    s1_mu = hs_prob_df.query('abs(s1)>0.5')[sym].mean() 
                    s2_mu = hs_prob_df.query('abs(s2)>0.5')[sym].mean() 
                    s1_std = hs_prob_df.query('abs(s1)>0.5')[sym].std() 
                    s2_std = hs_prob_df.query('abs(s2)>0.5')[sym].std()           
                    
                    last_state = hidden_states[-1]
                    last_mean = bgmm.means_[last_state][0]
                    last_var = np.diag(bgmm.covariances_[last_state])[0]
                    
                    rvs = bgmm.sample(self.SAMPLES)[0] 
                    low_ci, high_ci = stats.norm.interval(confidence=self.ALPHA, loc=np.mean(rvs), scale=np.std(rvs))
            
                    tmp_ret = np.log(float(self.Securities[sym_obj].Price) / train_px[sym].iloc[-1])
                            
                    row = (train_ts.index[-1], last_state, last_mean, np.sqrt(last_var), 
                            low_ci, high_ci, tmp_ret,
                            bgmm.means_.ravel()[0], bgmm.means_.ravel()[1],
                            np.sqrt(np.diag(bgmm.covariances_[0]))[0], np.sqrt(np.diag(bgmm.covariances_[1]))[0],
                            hidden_state_prob.iloc[-1][0], hidden_state_prob.iloc[-1][1],
                            s1_mu,s2_mu,s1_std,s2_std)
                    pred_rows.append(row)
                    self.Debug('{} rowzz:\n{}'.format(str(sym), row))
                    
                if pred_rows:
                    cols = ['Dates', 'ith_state', 'ith_ret','ith_std', 'low_ci', 'high_ci', 'current_return',
                            'last_mean_class_0', 'last_mean_class_1', 'last_std_class_0', 'last_std_class_1',
                            'last_prob_class_0', 'last_prob_class_1', 'avg_class_0_mean', 'avg_class_1_mean',
                            'avg_class_0_std', 'avg_class_1_std']             
                    
                    pred_df = make_final_pred_df(pred_rows, cols, self.THRES, sym)
                    
                    if pred_df.iloc[-1].loc['buys']==1: 
                        self._longs.append(sym_obj)
                        self.Debug('>>> BUY SIGNAL GENERATED for {} (Prob > {}) <<<'.format(sym, self.THRES))

                else:
                    self.Debug('missing or invested in {}'.format(sym))
                    
            except Exception as e:
                self.Debug('{} error: {}'.format(sym, e))
                continue

        self.Debug('Final Longs List for next order run: {}'.format([s.Value for s in self._longs])) 

        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, 'Time', self.time_to_run_main_algo)                
        return 
    
    def send_orders(self):
        self.Log('\n'+'-'*77+'\n[{}] checking buy sell arrays to send orders...'.format(self.UtcTime))

        if self._longs:
            for sym_obj in self._longs:
                if not self.Portfolio[sym_obj].Invested:
                    self.Log('[{}] SENDING MARKET ORDER for {}...'.format(self.UtcTime, sym_obj.Value))                        
                    self.MarketOrder(sym_obj, self.CalculateOrderQuantity(sym_obj, self.BET_SIZE))
                else:
                    self.Debug('Skipping {} - Already invested.'.format(sym_obj.Value))
        else:
            self.Log('send_orders >> no longs listed, no orders sent...')            
        return
    
    
    def OnData(self, data):
        pass

    def CHART_RAM(self):
        self.Plot(self.splotName,'RAM', OS.ApplicationMemoryUsed/1024.)
        P = self.Portfolio
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = P.TotalAbsoluteHoldingsCost/P.TotalPortfolioValue
            self.Plot(self.splotName, 'Leverage', float(self.track_account_leverage))
        
        self.Plot(self.splotName, 'Cash', float(self.Portfolio.Cash))
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import *

# Explicitly import necessary enums from their correct locations
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn import mixture as mix

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    # Ensure 'new' is a DataFrame if it came from a multi-column History request
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
        
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

# Placeholder for functions not needed in this benchmark, but required for structural completeness
def make_returns(df):
    """Calculate log returns for a price DataFrame (Placeholder)."""
    return np.log(df / df.shift(1))

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------


class EqualWeightBenchmark(QCAlgorithm):
    """
    Equal Weight Benchmark Strategy, adjusted for current QuantConnect standards.
    """

    def Initialize(self):
        """Initial algorithm settings"""

        self.INIT_PORTFOLIO_CASH = 100000

        self.SetStartDate(2007,12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)  # Set Strategy Cash

        # init brokerage model
        #self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
        #                       AccountType.Margin)

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)


        # init custom universe (FIXED to use Symbol objects)
        self.BASE_SYMBOL_TICKER = "SPY"
        self.ticker_strings = [
            self.BASE_SYMBOL_TICKER, "QQQ", "DIA", "TLT", "GLD", 
            "EFA", "EEM", "BND", "VNQ",
        ]
        
        self.symbols = [] # This list will hold Symbol objects
        for ticker in self.ticker_strings:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol) 

        # Base symbol must be the Symbol object for scheduling
        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol

        # Algo Exchange Settings
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        # other algo parameter settings
        self._init_prices = False

        self.LOOKBACK = 252  # trading days
        self.LEVERAGE = 1.0
        # Use the number of tickers for calculation
        self.BET_SIZE = 1 / len(self.ticker_strings) * self.LEVERAGE
        self.TOLERANCE = 0.025

        self.RANDOM_STATE = 7

        ## set resolution for historical data calls
        self.HISTORY_RESOLUTION = Resolution.Daily

        # track RAM and computation time for main func, also leverage and cash
        self.splotName = "Strategy Info"
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series("RAM", SeriesType.Line, 0))
        sPlot.AddSeries(Series("Time", SeriesType.Line, 1))
        sPlot.AddSeries(Series("Cash", SeriesType.Line, 2))
        sPlot.AddSeries(Series("Leverage", SeriesType.Line, 3))
        self.AddChart(sPlot)
        self.time_to_run_main_algo = 0

        # track portfolio weights by symbol
        self.splotName3 = "Security Weights Info"
        sPlot3 = Chart(self.splotName3)

        # Use ticker strings for chart series names
        for i, sec in enumerate(self.ticker_strings):
            sPlot3.AddSeries(Series(sec, SeriesType.Line, i))

        self.AddChart(sPlot3)

        # scheduled functions (Using BASE_SYMBOL object)
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5),
            Action(self.init_prices),
        )

        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.rebalance),
        )

        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM),
        )

        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.BeforeMarketClose(self.BASE_SYMBOL, 70),
            Action(self.CHART_SECURITY_WEIGHTS),
        )

    def init_prices(self):
        """
        Initialize historical prices.
        """
        if not self.symbols:
            self.Log("no symbols")
            return

        if self._init_prices:
            return
        
        # Pass list of Symbol objects for History API
        self.prices = (
            self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"]
            .unstack(level=0)
            .astype(np.float32)
        )
        self._init_prices = True
        return

    def update_prices(self):
        """
        Update prices efficiently using integrated utility functions.
        """

        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time

        # how many periods do we need (using integrated logic)
        days_to_request = how_many_days(current_date, most_recent_date)

        # if prices up to date return (using integrated logic)
        if zero_days_to_request(days_to_request):
            return

        # get new data (using Symbol objects)
        new_prices = self.History(
            self.symbols, days_to_request, self.HISTORY_RESOLUTION
        )
        
        if "close" in new_prices.columns:
            # unstack is handled inside make_update_df for clean code
            new_prices = new_prices["close"].unstack(level=0).astype(np.float32)
        else:
            return
            
        # combine datasets using integrated utility
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return

    def check_current_weight(self, symbol):
        """
        Check symbol's current weight.
        Accepts Symbol object.
        """
        P = self.Portfolio
        
        if P.TotalPortfolioValue == 0:
            return 0.0

        # Get the security object using the Symbol object
        security = self.Securities[symbol]
        
        # Access HoldingsValue via the Security object
        current_weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight

    def rebalance(self):
        """Run main algorithm"""
        self.Log(
            "\n"
            + "-" * 77
            + "\n[{}] Begin main algorithm computation...".format(self.UtcTime)
        )

        start_time = time.time()  # timer
        self.update_prices()  # update prices

        # Iterate over Symbol objects
        for sym_obj in self.symbols:
            # get current weights
            current_weight = self.check_current_weight(sym_obj)

            # if current weights outside of tolerance send new orders
            tol = self.TOLERANCE * self.BET_SIZE
            lower_bound = self.BET_SIZE - tol
            upper_bound = self.BET_SIZE + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                self.SetHoldings(sym_obj, self.BET_SIZE)

        ## end timer
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, "Time", self.time_to_run_main_algo)
        return

    def OnData(self, data):
        """OnData event is the primary entry point for your algorithm.
        Each new data point will be pumped in here."""
        pass

    def CHART_RAM(self):
        # Once a day or something reasonable to prevent spam
        self.Plot(self.splotName, "RAM", OS.ApplicationMemoryUsed / 1024.0)
        P = self.Portfolio
        
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = (
                P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            )
            self.Plot(self.splotName, "Leverage", float(self.track_account_leverage))
        
        self.Plot(self.splotName, "Cash", float(self.Portfolio.Cash))
        return

    def CHART_SECURITY_WEIGHTS(self):
        """Plots the current weight of each security."""
        P = self.Portfolio
        
        # Iterate over Symbol objects
        for sym_obj in self.symbols:
            
            # FIX: Access the Security object first
            security = self.Securities[sym_obj] 
            
            # Ensure TotalPortfolioValue is not zero before division
            weight = 0.0
            if P.TotalPortfolioValue != 0:
                 # Access HoldingsValue via the Security object
                 weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue) * 100
            
            # Plot using the ticker string (Symbol.Value) as the series name
            self.Plot(self.splotName3, sym_obj.Value, weight)
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn.mixture import GaussianMixture # Use standard GMM for VAR calculation

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED & REPLACED calc_gmm_var) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

# Function to fit GMM (standard)
def make_gmm(n_components, random_state):
    """Factory function for GaussianMixture (standard GMM)."""
    return GaussianMixture(n_components=n_components, random_state=random_state)
    
def calc_quantile_var(data, alpha=0.05):
    """Compute VaR by quantile."""
    return data.quantile(alpha)

def calc_gmm_var(data_frame, n_components, random_state=777, n_samples=1000, alpha=0.05):
    """
    Replaces the external calc_gmm_var:
    Fits a GMM and calculates VaR based on the simulated distribution.
    """
    if data_frame.empty or data_frame.shape[0] < n_components + 1:
        return 0.0 # Return 0 if insufficient data

    # 1. Prepare data (ensure it's an array of samples)
    X = data_frame.values.reshape(-1, 1)

    # 2. Fit GMM
    gmm = make_gmm(n_components=n_components, random_state=random_state)
    gmm.fit(X)

    # 3. Sample from GMM
    rvs = gmm.sample(n_samples)[0].ravel()
    
    # 4. Calculate VaR (Alpha=0.05 for 95% VaR)
    # Note: If the GMM finds a low-return regime, the sampled VaR will reflect it.
    var = calc_quantile_var(pd.Series(rvs), alpha=alpha)
    return var


# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------


class GMMInverseVAR(QCAlgorithm):
    """
    GMM Inverse VAR Strategy, matched to standardized structure.
    """

    def Initialize(self):
        """Initial algorithm settings"""

        # Set cash to 124,000
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 124000)

        self.SetStartDate(2007,12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)  # Set Strategy Cash

        #self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
        #                       AccountType.Margin)

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)

        # -----------------------------------------------------------------------------
        # init custom universe (FIXED to use Symbol objects)
        # -----------------------------------------------------------------------------

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', "SPY")
        self.ticker_strings = [
            self.BASE_SYMBOL_TICKER, "QQQ", "DIA", "TLT", "GLD", 
            "EFA", "EEM", "BND", "VNQ",
        ]
        
        # Store Symbol objects for use in History and SetHoldings
        self.symbols = [] 
        for ticker in self.ticker_strings:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol)

        # Base symbol must be the Symbol object for scheduling
        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol

        # Algo Exchange Settings
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        # -----------------------------------------------------------------------------
        # other algo parameter settings
        # -----------------------------------------------------------------------------

        self._init_prices = False

        self.LOOKBACK = register_param('historical lookback (days)', 252)  # 252 days lookback
        self.LEVERAGE = register_param('leverage', 1.5)
        self.TOLERANCE = register_param('rebalance tolerance', 0.025)
        self.N_COMPONENTS = register_param('gmm n components', 2) # n=2 components

        self.RANDOM_STATE = register_param('random_state', 7) # Used for GMM consistency

        ## set resolution for historical data calls
        self.HISTORY_RESOLUTION = Resolution.Daily
        register_param('history api resolution', str(self.HISTORY_RESOLUTION))

        # -----------------------------------------------------------------------------
        # track RAM and computation time for main func, also leverage and cash
        # -----------------------------------------------------------------------------

        self.splotName = "Strategy Info"
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series("RAM", SeriesType.Line, 0))
        sPlot.AddSeries(Series("Time", SeriesType.Line, 1))
        sPlot.AddSeries(Series("Cash", SeriesType.Line, 2))
        sPlot.AddSeries(Series("Leverage", SeriesType.Line, 3))
        self.AddChart(sPlot)

        self.time_to_run_main_algo = 0

        # -----------------------------------------------------------------------------
        # track portfolio weights by symbol
        # -----------------------------------------------------------------------------

        self.splotName3 = "Security Weights Info"
        sPlot3 = Chart(self.splotName3)

        for i, sec in enumerate(self.ticker_strings):
            sPlot3.AddSeries(Series(sec, SeriesType.Line, i))

        self.AddChart(sPlot3)

        # -----------------------------------------------------------------------------
        # scheduled functions (Using BASE_SYMBOL object)
        # -----------------------------------------------------------------------------

        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5),
            Action(self.init_prices),
        )

        # make buy list
        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.rebalance),
        )

        # plot RAM
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM),
        )

        # plot weights by asset symbol
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.BeforeMarketClose(self.BASE_SYMBOL, 70),
            Action(self.CHART_SECURITY_WEIGHTS),
        )
        
        # Log parameters
        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))


    def init_prices(self):
        """
        Initialize historical prices.
        """
        if not self.symbols:
            self.Log("no symbols")
            return

        if self._init_prices:
            return
            
        # Pass list of Symbol objects for History API
        self.prices = (
            self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"]
            .unstack(level=0)
            .astype(np.float32)
        )
        self._init_prices = True
        return

    def update_prices(self):
        """
        Update prices efficiently using integrated utility functions.
        """

        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time

        # how many periods do we need (using integrated logic)
        days_to_request = how_many_days(current_date, most_recent_date)

        # if prices up to date return (using integrated logic)
        if zero_days_to_request(days_to_request):
            return

        # get new data (using Symbol objects)
        new_prices = self.History(
            self.symbols, days_to_request, self.HISTORY_RESOLUTION
        )
        
        if "close" in new_prices.columns:
            new_prices = new_prices["close"].unstack(level=0).astype(np.float32)
        else:
            return
            
        # combine datasets using integrated utility
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return

    def check_current_weight(self, symbol):
        """
        Check symbol's current weight.
        Accepts Symbol object.
        """
        P = self.Portfolio
        
        if P.TotalPortfolioValue == 0:
            return 0.0

        # Get the security object using the Symbol object
        security = self.Securities[symbol]
        
        # Access HoldingsValue via the Security object
        current_weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight

    def rebalance(self):
        """fn: run main algorithm (Inverse VAR Rebalancing)"""
        self.Log(
            "\n"
            + "-" * 77
            + "\n[{}] Begin main algorithm computation...".format(self.UtcTime)
        )

        start_time = time.time()  # timer
        self.update_prices()  # update prices

        returns = np.log(self.prices / self.prices.shift(1)).dropna()

        X = returns.copy()

        # compute var (using Symbol objects)
        var_dict = {}
        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # Use calc_gmm_var helper function
            var = calc_gmm_var(
                X[ticker].to_frame(), 
                self.N_COMPONENTS, 
                random_state=self.RANDOM_STATE
            )
            var_dict[ticker] = var

        self.Log("var dict:\n{}".format(var_dict))

        # compute target weights
        var_ser = pd.DataFrame.from_dict(var_dict, orient="index").squeeze()
        
        # Handle division by zero/zero VAR (e.g., set VAR to a tiny positive number)
        # We also need to normalize the absolute returns, as VaR is usually negative/low.
        # Inverse VAR weights are proportional to 1/|VaR|
        invert = 1 / (var_ser.abs() + 1e-9) 
        
        target_weights_ser = invert / invert.sum()

        self.Log("inverse var weights: {}".format(target_weights_ser))

        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # get current weights
            current_weight = self.check_current_weight(sym_obj)
            target_weight = target_weights_ser[ticker] * self.LEVERAGE

            # if current weights outside of tolerance send new orders
            # Note: Target weight calculation uses the proportional allocation * Leverage
            tol = self.TOLERANCE * target_weight
            lower_bound = target_weight - tol
            upper_bound = target_weight + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                self.SetHoldings(sym_obj, target_weight)

        ## end timer
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, "Time", self.time_to_run_main_algo)
        return

    def OnData(self, data):
        """OnData event is the primary entry point for your algorithm.
        Each new data point will be pumped in here."""
        pass

    def CHART_RAM(self):
        # Once a day or something reasonable to prevent spam
        self.Plot(self.splotName, "RAM", OS.ApplicationMemoryUsed / 1024.0)
        P = self.Portfolio
        
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = (
                P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            )
            self.Plot(self.splotName, "Leverage", float(self.track_account_leverage))
        
        self.Plot(self.splotName, "Cash", float(P.Cash))
        return

    def CHART_SECURITY_WEIGHTS(self):
        # Once a day or something reasonable to prevent spam
        P = self.Portfolio
        for sym_obj in self.symbols:
            # FIX: Access the Security object first
            security = self.Securities[sym_obj] 
            
            weight = 0.0
            if P.TotalPortfolioValue != 0:
                 weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue) * 100
            
            self.Plot(self.splotName3, sym_obj.Value, weight)
        return
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn.mixture import GaussianMixture # Use standard GMM for VAR calculation

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
# Note: This is not used in the Inverse VAR logic but is included for code consistency.
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

# Function to fit GMM (standard)
def make_gmm(n_components, random_state):
    """Factory function for GaussianMixture (standard GMM)."""
    return GaussianMixture(n_components=n_components, random_state=random_state)
    
def calc_quantile_var(data, alpha=0.05):
    """Compute VaR by quantile."""
    return data.quantile(alpha)

def calc_gmm_var(data_frame, n_components, random_state=7, n_samples=1000, alpha=0.05):
    """
    Replaces the external calc_gmm_var:
    Fits a GMM and calculates VaR based on the simulated distribution.
    """
    if data_frame.empty or data_frame.shape[0] < n_components + 1:
        return 0.0 # Return 0 if insufficient data

    # 1. Prepare data (ensure it's an array of samples)
    X = data_frame.values.reshape(-1, 1)

    # 2. Fit GMM
    gmm = make_gmm(n_components=n_components, random_state=random_state)
    gmm.fit(X)

    # 3. Sample from GMM
    # NOTE: The original code used risky=False, implying sampling from the full fitted distribution.
    rvs = gmm.sample(n_samples)[0].ravel()
    
    # 4. Calculate VaR (Alpha=0.05 for 95% VaR)
    var = calc_quantile_var(pd.Series(rvs), alpha=alpha)
    return var


# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------


class GMMInverseVAR(QCAlgorithm):
    """
    GMM Inverse VAR Strategy, matched to standardized structure.
    """

    def Initialize(self):
        """Initial algorithm settings"""

        # Set initial cash
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)

        self.SetStartDate(2007, 12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)  # Set Strategy Cash

        #self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
        #                       AccountType.Margin)

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)

        # -----------------------------------------------------------------------------
        # init custom universe (FIXED to use Symbol objects)
        # -----------------------------------------------------------------------------

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', "SPY")
        self.ticker_strings = [
            self.BASE_SYMBOL_TICKER, "QQQ", "DIA", "TLT", "GLD", 
            "EFA", "EEM", "BND", "VNQ",
        ]
        
        # Store Symbol objects for use in History and SetHoldings
        self.symbols = [] 
        for ticker in self.ticker_strings:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol)

        # Base symbol must be the Symbol object for scheduling
        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol

        # Algo Exchange Settings
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        # -----------------------------------------------------------------------------
        # other algo parameter settings (Standardized and using register_param)
        # -----------------------------------------------------------------------------

        self._init_prices = False

        self.LOOKBACK = register_param('historical lookback (days)', 60)  # 60 days lookback
        self.LEVERAGE = register_param('leverage', 1.5)
        self.TOLERANCE = register_param('rebalance tolerance', 0.025)
        self.N_COMPONENTS = register_param('gmm n components', 2) # n=2 components
        self.ALPHA = register_param('var confidence alpha', 0.05) # VaR at 95% confidence

        self.RANDOM_STATE = register_param('random_state', 7) 
        self.N_SAMPLES = register_param('gmm var n samples', 1000)

        ## set resolution for historical data calls
        self.HISTORY_RESOLUTION = Resolution.Daily
        register_param('history api resolution', str(self.HISTORY_RESOLUTION))

        # -----------------------------------------------------------------------------
        # track RAM and computation time for main func, also leverage and cash
        # -----------------------------------------------------------------------------

        self.splotName = "Strategy Info"
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series("RAM", SeriesType.Line, 0))
        sPlot.AddSeries(Series("Time", SeriesType.Line, 1))
        sPlot.AddSeries(Series("Cash", SeriesType.Line, 2))
        sPlot.AddSeries(Series("Leverage", SeriesType.Line, 3))
        self.AddChart(sPlot)

        self.time_to_run_main_algo = 0

        # -----------------------------------------------------------------------------
        # track portfolio weights by symbol
        # -----------------------------------------------------------------------------

        self.splotName3 = "Security Weights Info"
        sPlot3 = Chart(self.splotName3)

        for i, sec in enumerate(self.ticker_strings):
            sPlot3.AddSeries(Series(sec, SeriesType.Line, i))

        self.AddChart(sPlot3)

        # -----------------------------------------------------------------------------
        # scheduled functions (Using BASE_SYMBOL object)
        # -----------------------------------------------------------------------------

        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5),
            Action(self.init_prices),
        )

        # make buy list
        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.rebalance),
        )

        # plot RAM
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM),
        )

        # plot weights by asset symbol
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.BeforeMarketClose(self.BASE_SYMBOL, 70),
            Action(self.CHART_SECURITY_WEIGHTS),
        )
        
        # Log parameters
        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))


    def init_prices(self):
        """
        Initialize historical prices.
        """
        if not self.symbols:
            self.Log("no symbols")
            return

        if self._init_prices:
            return
            
        # Pass list of Symbol objects for History API
        self.prices = (
            self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"]
            .unstack(level=0)
            .astype(np.float32)
        )
        self._init_prices = True
        return

    def update_prices(self):
        """
        Update prices efficiently using integrated utility functions.
        """

        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time

        # how many periods do we need (using integrated logic)
        days_to_request = how_many_days(current_date, most_recent_date)

        # if prices up to date return (using integrated logic)
        if zero_days_to_request(days_to_request):
            return

        # get new data (using Symbol objects)
        new_prices = self.History(
            self.symbols, days_to_request, self.HISTORY_RESOLUTION
        )
        
        if "close" in new_prices.columns:
            new_prices = new_prices["close"].unstack(level=0).astype(np.float32)
        else:
            return
            
        # combine datasets using integrated utility
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return

    def check_current_weight(self, symbol):
        """
        Check symbol's current weight.
        Accepts Symbol object.
        """
        P = self.Portfolio
        
        if P.TotalPortfolioValue == 0:
            return 0.0

        # Get the security object using the Symbol object
        security = self.Securities[symbol]
        
        # Access HoldingsValue via the Security object
        current_weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight

    def rebalance(self):
        """fn: run main algorithm (Inverse VAR Rebalancing)"""
        self.Log(
            "\n"
            + "-" * 77
            + "\n[{}] Begin main algorithm computation...".format(self.UtcTime)
        )

        start_time = time.time()  # timer
        self.update_prices()  # update prices

        returns = np.log(self.prices / self.prices.shift(1)).dropna()

        # compute var
        var_dict = {}
        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # Use calc_gmm_var helper function
            var = calc_gmm_var(
                returns[ticker].to_frame(), 
                self.N_COMPONENTS, 
                random_state=self.RANDOM_STATE,
                n_samples=self.N_SAMPLES,
                alpha=self.ALPHA
            )
            var_dict[ticker] = var

        self.Log("var dict:\n{}".format(var_dict))

        # compute target weights
        var_ser = pd.DataFrame.from_dict(var_dict, orient="index").squeeze()
        
        # Inverse VAR weights are proportional to 1/|VaR|
        # Add epsilon to prevent division by zero, and take absolute value.
        invert = 1 / (var_ser.abs() + 1e-9) 
        
        target_weights_ser = invert / invert.sum()

        self.Log("inverse var weights: {}".format(target_weights_ser))

        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # get current weights
            current_weight = self.check_current_weight(sym_obj)
            target_weight = target_weights_ser[ticker] * self.LEVERAGE

            # if current weights outside of tolerance send new orders
            # Note: Target weight calculation uses the proportional allocation * Leverage
            tol = self.TOLERANCE * target_weight
            lower_bound = target_weight - tol
            upper_bound = target_weight + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                self.SetHoldings(sym_obj, target_weight)

        ## end timer
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, "Time", self.time_to_run_main_algo)
        return

    def OnData(self, data):
        """OnData event is the primary entry point for your algorithm.
        Each new data point will be pumped in here."""
        pass

    def CHART_RAM(self):
        # Once a day or something reasonable to prevent spam
        self.Plot(self.splotName, "RAM", OS.ApplicationMemoryUsed / 1024.0)
        P = self.Portfolio
        
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = (
                P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            )
            self.Plot(self.splotName, "Leverage", float(self.track_account_leverage))
        
        self.Plot(self.splotName, "Cash", float(P.Cash))
        return

    def CHART_SECURITY_WEIGHTS(self):
        """Plots the current weight of each security."""
        P = self.Portfolio
        
        for sym_obj in self.symbols:
            # FIX: Access the Security object first
            security = self.Securities[sym_obj] 
            
            weight = 0.0
            if P.TotalPortfolioValue != 0:
                 weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue) * 100
            
            self.Plot(self.splotName3, sym_obj.Value, weight)
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn import mixture as mix

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

def calc_quantile_var(data, alpha=0.05):
    """Compute VaR by quantile."""
    return data.quantile(alpha)

def calc_historical_var(data_frame, alpha=0.05):
    """Compute historical VAR."""
    if data_frame.empty:
        return 0.0
    data = data_frame.squeeze()
    return calc_quantile_var(data, alpha=alpha)

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------


class HistoricalInverseVAR(QCAlgorithm):
    """
    Historical Inverse VAR Implementation, matched to standardized structure.
    """

    def Initialize(self):
        """Initial algorithm settings"""

        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)

        self.SetStartDate(2007,12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)  # Set Strategy Cash

        # init brokerage model
        #self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage,
        #                       AccountType.Margin)

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)
        # -----------------------------------------------------------------------------
        # init custom universe (FIXED to use Symbol objects)
        # -----------------------------------------------------------------------------

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', "SPY")
        self.ticker_strings = [
            self.BASE_SYMBOL_TICKER, "QQQ", "DIA", "TLT", "GLD", 
            "EFA", "EEM", "BND", "VNQ",
        ]
        
        # Store Symbol objects for use in History and SetHoldings
        self.symbols = [] 
        for ticker in self.ticker_strings:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol)

        # Base symbol must be the Symbol object for scheduling
        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol

        # Algo Exchange Settings
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        # -----------------------------------------------------------------------------
        # other algo parameter settings
        # -----------------------------------------------------------------------------

        self._init_prices = False

        # MODIFICATION: Set lookback to 252 days
        self.LOOKBACK = register_param('historical lookback (days)', 252)
        
        self.LEVERAGE = register_param('leverage', 1.0)
        self.TOLERANCE = register_param('rebalance tolerance', 0.025)
        self.N_COMPONENTS = register_param('gmm n components', 2) 
        self.ALPHA = register_param('var confidence alpha', 0.05) 

        self.RANDOM_STATE = register_param('random_state', 7)

        ## set resolution for historical data calls
        self.HISTORY_RESOLUTION = Resolution.Daily
        register_param('history api resolution', str(self.HISTORY_RESOLUTION))

        # -----------------------------------------------------------------------------
        # track RAM and computation time for main func, also leverage and cash
        # -----------------------------------------------------------------------------

        self.splotName = "Strategy Info"
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series("RAM", SeriesType.Line, 0))
        sPlot.AddSeries(Series("Time", SeriesType.Line, 1))
        sPlot.AddSeries(Series("Cash", SeriesType.Line, 2))
        sPlot.AddSeries(Series("Leverage", SeriesType.Line, 3))
        self.AddChart(sPlot)

        self.time_to_run_main_algo = 0

        # -----------------------------------------------------------------------------
        # track portfolio weights by symbol
        # -----------------------------------------------------------------------------

        self.splotName3 = "Security Weights Info"
        sPlot3 = Chart(self.splotName3)

        for i, sec in enumerate(self.ticker_strings):
            sPlot3.AddSeries(Series(sec, SeriesType.Line, i))

        self.AddChart(sPlot3)

        # -----------------------------------------------------------------------------
        # scheduled functions (Using BASE_SYMBOL object)
        # -----------------------------------------------------------------------------

        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5),
            Action(self.init_prices),
        )

        # make buy list
        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.rebalance),
        )

        # plot RAM
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM),
        )

        # plot weights by asset symbol
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.BeforeMarketClose(self.BASE_SYMBOL, 70),
            Action(self.CHART_SECURITY_WEIGHTS),
        )
        
        # Log parameters
        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))


    def init_prices(self):
        """
        Initialize historical prices.
        """
        if not self.symbols:
            self.Log("no symbols")
            return

        if self._init_prices:
            return
            
        # Pass list of Symbol objects for History API
        self.prices = (
            self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"]
            .unstack(level=0)
            .astype(np.float32)
        )
        self._init_prices = True
        return

    def update_prices(self):
        """
        Update prices efficiently using integrated utility functions.
        """

        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time

        # how many periods do we need (using integrated logic)
        days_to_request = how_many_days(current_date, most_recent_date)

        # if prices up to date return (using integrated logic)
        if zero_days_to_request(days_to_request):
            return

        # get new data (using Symbol objects)
        new_prices = self.History(
            self.symbols, days_to_request, self.HISTORY_RESOLUTION
        )
        
        if "close" in new_prices.columns:
            new_prices = new_prices["close"].unstack(level=0).astype(np.float32)
        else:
            return
            
        # combine datasets using integrated utility
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return

    def check_current_weight(self, symbol):
        """
        Check symbol's current weight.
        Accepts Symbol object.
        """
        P = self.Portfolio
        
        if P.TotalPortfolioValue == 0:
            return 0.0

        # Get the security object using the Symbol object
        security = self.Securities[symbol]
        
        # Access HoldingsValue via the Security object
        current_weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight

    def rebalance(self):
        """fn: run main algorithm (Historical Inverse VAR Rebalancing)"""
        self.Log(
            "\n"
            + "-" * 77
            + "\n[{}] Begin main algorithm computation...".format(self.UtcTime)
        )

        start_time = time.time()  # timer
        self.update_prices()  # update prices

        returns = np.log(self.prices / self.prices.shift(1)).dropna()

        var_dict = {}
        # compute var
        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # Use calc_historical_var helper function (integrated)
            # Alpha is 0.05, calculating the 5th percentile return (VaR)
            var = calc_historical_var(returns[ticker].to_frame(), alpha=self.ALPHA)
            var_dict[ticker] = var

        self.Log("var dict: {}".format(var_dict))

        # compute target weights
        var_ser = pd.DataFrame.from_dict(var_dict, orient="index").squeeze()
        
        # Inverse VAR weights are proportional to 1/|VaR|
        # Add epsilon to prevent division by zero, and take absolute value.
        invert = 1 / (var_ser.abs() + 1e-9) 
        
        target_weights_ser = invert / invert.sum()

        self.Log("inverse var weights: {}".format(target_weights_ser))

        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # get current weights
            current_weight = self.check_current_weight(sym_obj)
            target_weight = target_weights_ser[ticker] * self.LEVERAGE

            # if current weights outside of tolerance send new orders
            tol = self.TOLERANCE * target_weight
            lower_bound = target_weight - tol
            upper_bound = target_weight + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                self.SetHoldings(sym_obj, target_weight)

        ## end timer
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, "Time", self.time_to_run_main_algo)
        return

    def OnData(self, data):
        """OnData event is the primary entry point for your algorithm.
        Each new data point will be pumped in here."""
        pass

    def CHART_RAM(self):
        # Once a day or something reasonable to prevent spam
        self.Plot(self.splotName, "RAM", OS.ApplicationMemoryUsed / 1024.0)
        P = self.Portfolio
        
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = (
                P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            )
            self.Plot(self.splotName, "Leverage", float(self.track_account_leverage))
        
        self.Plot(self.splotName, "Cash", float(P.Cash))
        return

    def CHART_SECURITY_WEIGHTS(self):
        """Plots the current weight of each security."""
        P = self.Portfolio
        
        for sym_obj in self.symbols:
            # FIX: Access the Security object first
            security = self.Securities[sym_obj] 
            
            weight = 0.0
            if P.TotalPortfolioValue != 0:
                 weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue) * 100
            
            self.Plot(self.splotName3, sym_obj.Value, weight)
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * # Imports core enums like OrderEventStatus and AccountType
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn import mixture as mix

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

def calc_quantile_var(data, alpha=0.05):
    """Compute VaR by quantile."""
    return data.quantile(alpha)

def calc_historical_var(data_frame, alpha=0.05):
    """Replaces external calc_historical_var: compute historical VAR."""
    if data_frame.empty:
        return 0.0
    data = data_frame.squeeze()
    return calc_quantile_var(data, alpha=alpha)

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------


class HistoricalInverseVAR(QCAlgorithm):
    """
    Historical Inverse VAR Implementation, matched to standardized structure.
    """

    def Initialize(self):
        """Initial algorithm settings"""

        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)

        self.SetStartDate(2007,12, 31)  # Set Start Date
        self.SetEndDate(2025, 10, 17)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)  # Set Strategy Cash

        self.set_brokerage_model(BrokerageName.ALPACA, AccountType.Margin)

        # -----------------------------------------------------------------------------
        # init custom universe (FIXED to use Symbol objects)
        # -----------------------------------------------------------------------------

        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', "SPY")
        self.ticker_strings = [
            self.BASE_SYMBOL_TICKER, "QQQ", "DIA", "TLT", "GLD", 
            "EFA", "EEM", "BND", "VNQ",
        ]
        
        # Store Symbol objects for use in History and SetHoldings
        self.symbols = [] 
        for ticker in self.ticker_strings:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol)

        # Base symbol must be the Symbol object for scheduling
        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol

        # Algo Exchange Settings
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        # -----------------------------------------------------------------------------
        # other algo parameter settings
        # -----------------------------------------------------------------------------

        self._init_prices = False

        self.LOOKBACK = register_param('historical lookback (days)', 60)
        self.LEVERAGE = register_param('leverage', 1.0)
        self.TOLERANCE = register_param('rebalance tolerance', 0.025)
        self.N_COMPONENTS = register_param('gmm n components', 2) # Parameter kept for structure, but unused in pure Historical VAR
        self.ALPHA = register_param('var confidence alpha', 0.05) # VaR at 95% confidence (1-alpha)

        self.RANDOM_STATE = register_param('random_state', 7)

        ## set resolution for historical data calls
        self.HISTORY_RESOLUTION = Resolution.Daily
        register_param('history api resolution', str(self.HISTORY_RESOLUTION))

        # -----------------------------------------------------------------------------
        # track RAM and computation time for main func, also leverage and cash
        # -----------------------------------------------------------------------------

        self.splotName = "Strategy Info"
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series("RAM", SeriesType.Line, 0))
        sPlot.AddSeries(Series("Time", SeriesType.Line, 1))
        sPlot.AddSeries(Series("Cash", SeriesType.Line, 2))
        sPlot.AddSeries(Series("Leverage", SeriesType.Line, 3))
        self.AddChart(sPlot)

        self.time_to_run_main_algo = 0

        # -----------------------------------------------------------------------------
        # track portfolio weights by symbol
        # -----------------------------------------------------------------------------

        self.splotName3 = "Security Weights Info"
        sPlot3 = Chart(self.splotName3)

        for i, sec in enumerate(self.ticker_strings):
            sPlot3.AddSeries(Series(sec, SeriesType.Line, i))

        self.AddChart(sPlot3)

        # -----------------------------------------------------------------------------
        # scheduled functions (Using BASE_SYMBOL object)
        # -----------------------------------------------------------------------------

        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5),
            Action(self.init_prices),
        )

        # make buy list
        self.Schedule.On(
            self.DateRules.MonthStart(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10),
            Action(self.rebalance),
        )

        # plot RAM
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40),
            Action(self.CHART_RAM),
        )

        # plot weights by asset symbol
        self.Schedule.On(
            self.DateRules.EveryDay(self.BASE_SYMBOL),
            self.TimeRules.BeforeMarketClose(self.BASE_SYMBOL, 70),
            Action(self.CHART_SECURITY_WEIGHTS),
        )
        
        # Log parameters
        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))


    def init_prices(self):
        """
        Initialize historical prices.
        """
        if not self.symbols:
            self.Log("no symbols")
            return

        if self._init_prices:
            return
            
        # Pass list of Symbol objects for History API
        self.prices = (
            self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"]
            .unstack(level=0)
            .astype(np.float32)
        )
        self._init_prices = True
        return

    def update_prices(self):
        """
        Update prices efficiently using integrated utility functions.
        """

        # get last date of stored prices
        most_recent_date = self.prices.index.max()
        current_date = self.Time

        # how many periods do we need (using integrated logic)
        days_to_request = how_many_days(current_date, most_recent_date)

        # if prices up to date return (using integrated logic)
        if zero_days_to_request(days_to_request):
            return

        # get new data (using Symbol objects)
        new_prices = self.History(
            self.symbols, days_to_request, self.HISTORY_RESOLUTION
        )
        
        if "close" in new_prices.columns:
            new_prices = new_prices["close"].unstack(level=0).astype(np.float32)
        else:
            return
            
        # combine datasets using integrated utility
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return

    def check_current_weight(self, symbol):
        """
        Check symbol's current weight.
        Accepts Symbol object.
        """
        P = self.Portfolio
        
        if P.TotalPortfolioValue == 0:
            return 0.0

        # Get the security object using the Symbol object
        security = self.Securities[symbol]
        
        # Access HoldingsValue via the Security object
        current_weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue)
        return current_weight

    def rebalance(self):
        """fn: run main algorithm (Historical Inverse VAR Rebalancing)"""
        self.Log(
            "\n"
            + "-" * 77
            + "\n[{}] Begin main algorithm computation...".format(self.UtcTime)
        )

        start_time = time.time()  # timer
        self.update_prices()  # update prices

        returns = np.log(self.prices / self.prices.shift(1)).dropna()

        var_dict = {}
        # compute var
        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # Use calc_historical_var helper function (integrated)
            # Alpha is 0.05, calculating the 5th percentile return (VaR)
            var = calc_historical_var(returns[ticker].to_frame(), alpha=self.ALPHA)
            var_dict[ticker] = var

        self.Log("var dict: {}".format(var_dict))

        # compute target weights
        var_ser = pd.DataFrame.from_dict(var_dict, orient="index").squeeze()
        
        # Inverse VAR weights are proportional to 1/|VaR|
        # Add epsilon to prevent division by zero, and take absolute value.
        invert = 1 / (var_ser.abs() + 1e-9) 
        
        target_weights_ser = invert / invert.sum()

        self.Log("inverse var weights: {}".format(target_weights_ser))

        for sym_obj in self.symbols:
            ticker = sym_obj.Value
            
            # get current weights
            current_weight = self.check_current_weight(sym_obj)
            target_weight = target_weights_ser[ticker] * self.LEVERAGE

            # if current weights outside of tolerance send new orders
            tol = self.TOLERANCE * target_weight
            lower_bound = target_weight - tol
            upper_bound = target_weight + tol

            if (current_weight < lower_bound) or (current_weight > upper_bound):
                self.SetHoldings(sym_obj, target_weight)

        ## end timer
        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, "Time", self.time_to_run_main_algo)
        return

    def OnData(self, data):
        """OnData event is the primary entry point for your algorithm.
        Each new data point will be pumped in here."""
        pass

    def CHART_RAM(self):
        # Once a day or something reasonable to prevent spam
        self.Plot(self.splotName, "RAM", OS.ApplicationMemoryUsed / 1024.0)
        P = self.Portfolio
        
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = (
                P.TotalAbsoluteHoldingsCost / P.TotalPortfolioValue
            )
            self.Plot(self.splotName, "Leverage", float(self.track_account_leverage))
        
        self.Plot(self.splotName, "Cash", float(P.Cash))
        return

    def CHART_SECURITY_WEIGHTS(self):
        """Plots the current weight of each security."""
        P = self.Portfolio
        
        for sym_obj in self.symbols:
            # FIX: Access the Security object first
            security = self.Securities[sym_obj] 
            
            weight = 0.0
            if P.TotalPortfolioValue != 0:
                 weight = float(security.Holdings.HoldingsValue) / float(P.TotalPortfolioValue) * 100
            
            self.Plot(self.splotName3, sym_obj.Value, weight)
        return
#region imports
from AlgorithmImports import *
#endregion
import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 

#######################################################################
# gmm functions
#######################################################################


def make_gmm(n_components=None, max_iter=150, random_state=None):
    """fn: create gmm object"""
    model_kwds = dict(n_components=n_components, 
                      max_iter=max_iter,
                      n_init=100,
                      init_params='random',
                      random_state=random_state)

    gmm = mix.GaussianMixture(**model_kwds)
    return gmm
    
def make_returns(df):
    return np.log(df/df.shift(1)).dropna()
    
#######################################################################
# pred df functions
#######################################################################    
def in_range(df):
    """fn: add binary column for predictions within CI"""
    wins = df.query("low_ci < current_return < high_ci").index
    in_list = [1 if i in wins else 0 for i in df.index]
    df = df.assign(in_range=in_list)
    return df

def get_state_prob(df):
    state_prob = []
    for row in df[['ith_state','last_prob_class_0', 'last_prob_class_1']].itertuples():
        prob = pd.eval(f'row.last_prob_class_{row.ith_state}')
        state_prob.append(prob)
    return df.assign(state_prob=state_prob)
    
def get_outlier_direction(df):
    """"""
    direction = []
    for row in df[['high_ci', 'current_return']].itertuples(index=False):
        if row[-1] > row[0]: # actual_returns > high_ci
            direction.append('too_high')
        else: 
            direction.append('too_low')
    df = df.assign(direction=direction)
    return df

def buys(df, thres=0.5):
    buys = []
    for row in df.itertuples():
        if (row.ith_state==0 
            and row.mu_diff>0 
            and row.in_range==1
            and row.state_prob>thres
            and row.direction=='too_low'):
            buys.append(1)
        elif (row.ith_state==1 
              and row.mu_diff <0
              and row.in_range==1
              and row.state_prob>thres              
              and row.direction=='too_low'):
            buys.append(1)
        else:
            buys.append(0)
    return df.assign(buys=buys) 
    
def make_final_pred_df(pred_rows, cols, thres, sym):
    pred_df = (pd.DataFrame(pred_rows, columns=cols)
               .assign(mu_diff=lambda df: df.avg_class_0_mean-df.avg_class_1_mean)
               .assign(std_diff=lambda df: df.avg_class_0_std-df.avg_class_1_std)
               .pipe(in_range)
               .pipe(get_state_prob)
               .pipe(get_outlier_direction)
               .pipe(buys, thres=thres)
               .set_index('Dates')
               .assign(Dates = lambda df: df.index))
    return pred_df    
    
#######################################################################
# updating historical timeseries dataframes
#######################################################################

def how_many_days(current_date, most_recent_date):
    """compute how many days to request from history api
    # args: both are datetime objects 
    """    
    return (current_date - most_recent_date).days
    
def zero_days_to_request(days_to_request): 
    """check if days to request is equal to 0 
       if yes exit algorithm
    """
    # request only days that are missing from our dataset
    if days_to_request==0:
        return True
        
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes
       for updates
   
    # args
        old, new: pandas dataframes
        lookback: numeric 
        
    # returns
        both: combined dataframe 
    """
    # combine datasets                      
    both = pd.concat([old, new]) 
    # clean it up and keep only lookback period
    return (both
            .drop_duplicates()
            .sort_index()
            .iloc[-lookback:]) 
     
#######################################################################
# order execution functions
#######################################################################
def get_open_order_secs(open_orders):
    """func to return list of symbols
        if open order list is populated
    """
    if open_orders: # if list is populated
        open_order_secs = [order.Symbol for order in open_orders]
    else: 
        open_order_secs = []
    return open_order_secs
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * 
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn import mixture as mix

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    # Ensures no errors when combining/unstacking history data
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

def make_returns(df):
    """Calculate log returns for a price DataFrame."""
    return np.log(df / df.shift(1))

def make_gmm(n_components, random_state):
    """Factory function for GaussianMixtureModel."""
    return mix.GaussianMixture(n_components=n_components, random_state=random_state)
    
def make_final_pred_df(pred_rows, cols, thres, sym):
    """Process prediction rows into a DataFrame and apply trading logic."""
    df = pd.DataFrame(pred_rows, columns=cols)
    df['symbol'] = sym
    
    # Determine which class is the 'high-return' state by comparing means
    high_mean_class = 1 if df['last_mean_class_1'].iloc[0] > df['last_mean_class_0'].iloc[0] else 0
    
    if high_mean_class == 1:
        # Check if the probability of the high-return state (Class 1) > threshold
        df['buys'] = np.where(df['last_prob_class_1'] > thres, 1, 0)
    else:
        # Check if the probability of the high-return state (Class 0) > threshold
        df['buys'] = np.where(df['last_prob_class_0'] > thres, 1, 0)
        
    return df.tail(1)

# --- Other Utilities (Remaining functions from algo_utils, unused by main logic) ---

def calc_quantile_var(data, alpha=0.05): return data.quantile(alpha)
def calc_historical_var(data, alpha=0.05): 
    if isinstance(data, pd.DataFrame): data = data.squeeze()
    return calc_quantile_var(data, alpha=alpha)
def get_open_order_secs(open_orders): 
    if open_orders: return [order.Symbol for order in open_orders]
    return []

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------

class TradingWithGMM(QCAlgorithm):
    def Initialize(self):
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)        

        self.SetStartDate(2007,4,10)
        self.SetEndDate(2025, 10, 13)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)
        
        self.SetBrokerageModel(BrokerageName.ALPACA, AccountType.Margin)

        # Correct Symbol Handling
        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', 'SPY')   
        self.tickers = ["SPY", "QQQ", "DIA", "TLT", "GLD", "EFA", "EEM", "BND", "VNQ"]
        
        self.symbols = []
        for ticker in self.tickers:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol) 

        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        self.openMarketOnOpenOrders = []
        self._init_prices = False
        self._longs = list() 
        self._shorts = list()
        register_param('symbols: ', [s.Value for s in self.symbols])
        self._holding_period = register_param('holding period (days)', 30)
        
        # MODIFICATION 1: Set lookback to 252 days
        self.LOOKBACK = register_param('historical lookback (days)', 252)
        
        self.BET_SIZE = register_param('bet size (%)', 1/len(self.symbols))
    
        self.RANDOM_STATE = register_param('random_state', 777)
        self.ALPHA = register_param('gmm alpha', 0.95) 
        
        # MODIFICATION 2: Set GMM components to 2 (kept from previous instruction)
        self.N_COMPONENTS = register_param('gmm n components', 2)        
        
        self.THRES = register_param('threshold probability for buy signal', 0.9) 
        self.SAMPLES = register_param('number of samples for bootstrap', 1000)
        self.HISTORY_RESOLUTION = Resolution.Daily 
        register_param('history api resolution', str(self.HISTORY_RESOLUTION)) 

        # Charting setup remains the same
        self.splotName = 'Strategy Info'
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series('RAM',  SeriesType.Line, 0))
        sPlot.AddSeries(Series('Time',  SeriesType.Line, 1))
        sPlot.AddSeries(Series('Cash',  SeriesType.Line, 2))
        sPlot.AddSeries(Series('Leverage',  SeriesType.Line, 3))
        self.AddChart(sPlot)
        self.time_to_run_main_algo = 0

        # Scheduled functions
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5), Action(self.init_prices))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10), Action(self.run_main_algo))
        
        # Changed to MarketOrder for execution at T+30 min (see send_orders)
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 30), Action(self.send_orders)) 
        
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 35), Action(self.check_liquidate))
        self.Schedule.On(self.DateRules.EveryDay(self.BASE_SYMBOL), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40), Action(self.CHART_RAM))

        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))

    def init_prices(self):
        if not self.symbols: self.Log('no symbols'); return
        if self._init_prices: return 
        
        self.prices = (self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self._init_prices=True
       
    def update_prices(self):
        most_recent_date = self.prices.index.max()
        current_date = self.Time
        days_to_request = how_many_days(current_date, most_recent_date)
        if zero_days_to_request(days_to_request): return

        new_prices = (self.History(self.symbols, days_to_request, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return
    
    def check_liquidate(self):
        """
        Robust liquidation check using GetOrderTickets() and manual filtering.
        Uses the integer value of OrderEventStatus.Filled (which is 3).
        """
        self.Log('\n'+'-'*77+'\n[{}] checking liquidation status...'.format(self.UtcTime))
                
        # Retrieve ALL order tickets once
        all_tickets = self.Transactions.GetOrderTickets()
        
        for holding in self.Portfolio.Values:
            if not holding.Invested:
                continue 
            
            # Filter all tickets to find those matching the current holding's Symbol
            symbol_tickets = [t for t in all_tickets if t.Symbol == holding.Symbol]
            
            latest_fill_event = None
            
            # Find the latest FILL event that was a BUY order
            for ticket in symbol_tickets:
                # Use the raw integer value of the status (OrderEventStatus.Filled == 3)
                fill_events = [e for e in ticket.OrderEvents if e.Status == ORDER_STATUS_FILLED and e.FillQuantity > 0]

                if fill_events:
                    # Find the latest fill event across all tickets
                    current_latest_fill = max(fill_events, key=lambda x: x.UtcTime)
                    
                    if latest_fill_event is None or current_latest_fill.UtcTime > latest_fill_event.UtcTime:
                        latest_fill_event = current_latest_fill
            
            if latest_fill_event:
                entry_time = latest_fill_event.UtcTime
                
                # Check if the current time is past the entry time + holding period
                if self.UtcTime >= (entry_time + timedelta(self._holding_period)):
                    self.Liquidate(holding.Symbol)
                    fmt_args = (self.UtcTime, holding.Symbol.Value, entry_time, self.UtcTime - entry_time)
                    self.Log('[{}] liquidating... {}, order date: {}, time delta: {}'.format(*fmt_args))
        return

    def run_main_algo(self):
        self.Log('\n'+'-'*77+'\n[{}] Begin main algorithm computation...'.format(self.UtcTime))
        
        start_time = time.time()
        self.update_prices()
        self._algo_data = False
        self._longs = list() 
        self._shorts = list()

        for sym_obj in self.symbols: 
            sym = sym_obj.Value
            
            try:
                self.Log('checking symbol: {}'.format(str(sym)))
                pred_rows = list()
                
                # Check if we should perform the GMM calculation
                if (not self.Portfolio[sym_obj].Invested):
                    
                    if sym not in self.prices.columns: continue
                        
                    train_px = self.prices.copy()
                    train_ts = make_returns(train_px)[sym].dropna()
                    train_ts = train_ts[np.isfinite(train_ts)]
                    
                    if train_ts.shape[0] < self.N_COMPONENTS + 1:
                         self.Debug('{} train data has too few samples (<{})'.format(str(sym), self.N_COMPONENTS + 1))
                         continue

                    tmp_X_train = train_ts.values.reshape(-1, 1)
            
                    ### fit GMM ###
                    gmm = make_gmm(n_components=self.N_COMPONENTS, random_state=self.RANDOM_STATE).fit(tmp_X_train)
                    hidden_states = gmm.predict(tmp_X_train)
                    hidden_state_prob = pd.DataFrame(gmm.predict_proba(tmp_X_train), columns=['s1','s2'], index=train_ts.index)
                    
                    state_df = train_ts.to_frame()
                    hs_prob_df = (pd.concat([state_df, hidden_state_prob],axis=1))
                    
                    # Variables are defined here:
                    s1_mu = hs_prob_df.query('abs(s1)>0.5')[sym].mean() 
                    s2_mu = hs_prob_df.query('abs(s2)>0.5')[sym].mean() 
                    s1_std = hs_prob_df.query('abs(s1)>0.5')[sym].std() 
                    s2_std = hs_prob_df.query('abs(s2)>0.5')[sym].std()           
                    
                    # GMM state and interval calculation
                    last_state = hidden_states[-1]
                    last_mean = gmm.means_[last_state][0]
                    last_var = np.diag(gmm.covariances_[last_state])[0]
                    
                    rvs = gmm.sample(self.SAMPLES)[0] 
                    # Corrected argument name for scipy.stats compatibility
                    low_ci, high_ci = stats.norm.interval(confidence=self.ALPHA, loc=np.mean(rvs), scale=np.std(rvs))
            
                    tmp_ret = np.log(float(self.Securities[sym_obj].Price) / train_px[sym].iloc[-1])
                            
                    ### Row creation is now safely inside the calculation block
                    row = (train_ts.index[-1], last_state, last_mean, np.sqrt(last_var), 
                            low_ci, high_ci, tmp_ret,
                            gmm.means_.ravel()[0], gmm.means_.ravel()[1],
                            np.sqrt(np.diag(gmm.covariances_[0]))[0], np.sqrt(np.diag(gmm.covariances_[1]))[0],
                            hidden_state_prob.iloc[-1][0], hidden_state_prob.iloc[-1][1],
                            s1_mu,s2_mu,s1_std,s2_std)
                    pred_rows.append(row)
                    self.Debug('{} rowzz:\n{}'.format(str(sym), row))
                    
                if pred_rows:
                    cols = ['Dates', 'ith_state', 'ith_ret','ith_std', 'low_ci', 'high_ci', 'current_return',
                            'last_mean_class_0', 'last_mean_class_1', 'last_std_class_0', 'last_std_class_1',
                            'last_prob_class_0', 'last_prob_class_1', 'avg_class_0_mean', 'avg_class_1_mean',
                            'avg_class_0_std', 'avg_class_1_std']             
                    
                    pred_df = make_final_pred_df(pred_rows, cols, self.THRES, sym)
                    
                    if pred_df.iloc[-1].loc['buys']==1: 
                        self._longs.append(sym_obj)
                        self.Debug('>>> BUY SIGNAL GENERATED for {} (Prob > {}) <<<'.format(sym, self.THRES))

                else:
                    self.Debug('missing or invested in {}'.format(sym))
                    
            except Exception as e:
                self.Debug('{} error: {}'.format(sym, e))
                continue

        self.Debug('Final Longs List for next order run: {}'.format([s.Value for s in self._longs])) 

        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, 'Time', self.time_to_run_main_algo)                
        return 
    
    def send_orders(self):
        self.Log('\n'+'-'*77+'\n[{}] checking buy sell arrays to send orders...'.format(self.UtcTime))

        if self._longs:
            for sym_obj in self._longs:
                if not self.Portfolio[sym_obj].Invested:
                    # Use MarketOrder for execution 30 minutes after open
                    self.Log('[{}] SENDING MARKET ORDER for {}...'.format(self.UtcTime, sym_obj.Value))                        
                    self.MarketOrder(sym_obj, self.CalculateOrderQuantity(sym_obj, self.BET_SIZE))
                else:
                    self.Debug('Skipping {} - Already invested.'.format(sym_obj.Value))
        else:
            self.Log('send_orders >> no longs listed, no orders sent...')            
        return
    
    
    def OnData(self, data):
        pass

    def CHART_RAM(self):
        self.Plot(self.splotName,'RAM', OS.ApplicationMemoryUsed/1024.)
        P = self.Portfolio
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = P.TotalAbsoluteHoldingsCost/P.TotalPortfolioValue
            self.Plot(self.splotName, 'Leverage', float(self.track_account_leverage))
        
        self.Plot(self.splotName, 'Cash', float(self.Portfolio.Cash))
        return
# region imports
from AlgorithmImports import *
# endregion
# The explicit AddReference is often not strictly needed but is kept for compatibility.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Indicators")
AddReference("QuantConnect.Common")

from System import *
from QuantConnect import * # Imports core enums like OrderEventStatus and AccountType
from QuantConnect.Brokerages import BrokerageName 
from QuantConnect import AccountType

from QuantConnect.Algorithm import *
from QuantConnect.Indicators import *

import pandas as pd
import numpy as np
from math import ceil, floor
import scipy.stats as stats
import sklearn.mixture as mix
from datetime import datetime, timedelta
import time
import decimal as d
import json 
from sklearn import mixture as mix

# --- Define the problematic constant globally (Value of OrderEventStatus.Filled = 3)
ORDER_STATUS_FILLED = 3 

# ------------------------------------------------------------------------------
# --- ALGO UTILS FUNCTIONS (INTEGRATED) ---
# ------------------------------------------------------------------------------

def how_many_days(current_date, most_recent_date):
    """Calculate the number of days to request history."""
    return int((current_date - most_recent_date).days) + 1

def zero_days_to_request(days_to_request):
    """Check if the history data is up to date."""
    return days_to_request < 1
    
def make_update_df(old, new, lookback):
    """combines and cleans numeric timeseries dataframes for updates"""
    # Ensures no errors when combining/unstacking history data
    if isinstance(new, pd.DataFrame) and 'close' in new.columns.names:
        new = new['close'].unstack(level=0)
    
    both = pd.concat([old, new])
    return both.drop_duplicates().sort_index().iloc[-lookback:]

def make_returns(df):
    """Calculate log returns for a price DataFrame."""
    return np.log(df / df.shift(1))

def make_gmm(n_components, random_state):
    """Factory function for GaussianMixtureModel."""
    return mix.GaussianMixture(n_components=n_components, random_state=random_state)
    
def make_final_pred_df(pred_rows, cols, thres, sym):
    """Process prediction rows into a DataFrame and apply trading logic."""
    df = pd.DataFrame(pred_rows, columns=cols)
    df['symbol'] = sym
    
    # Determine which class is the 'high-return' state by comparing means
    high_mean_class = 1 if df['last_mean_class_1'].iloc[0] > df['last_mean_class_0'].iloc[0] else 0
    
    if high_mean_class == 1:
        # Check if the probability of the high-return state (Class 1) > threshold
        df['buys'] = np.where(df['last_prob_class_1'] > thres, 1, 0)
    else:
        # Check if the probability of the high-return state (Class 0) > threshold
        df['buys'] = np.where(df['last_prob_class_0'] > thres, 1, 0)
        
    return df.tail(1)

# --- Other Utilities (Remaining functions from algo_utils, unused by main logic) ---

def calc_quantile_var(data, alpha=0.05): return data.quantile(alpha)
def calc_historical_var(data, alpha=0.05): 
    if isinstance(data, pd.DataFrame): data = data.squeeze()
    return calc_quantile_var(data, alpha=alpha)
def get_open_order_secs(open_orders): 
    if open_orders: return [order.Symbol for order in open_orders]
    return []

# ------------------------------------------------------------------------------
# init parameter registry
# ------------------------------------------------------------------------------

PARAMETER_REGISTRY = {}

def register_param(name, value):
    PARAMETER_REGISTRY[name] = value
    return value

# -----------------------------------------------------------------------------
# algorithm class
# -----------------------------------------------------------------------------

class TradingWithGMM(QCAlgorithm):
    def Initialize(self):
        self.INIT_PORTFOLIO_CASH = register_param('portfolio starting cash', 100000)        

        self.SetStartDate(2007,4,10)
        self.SetEndDate(2025, 10, 13)  # Set End Date
        self.SetCash(self.INIT_PORTFOLIO_CASH)
        
        self.SetBrokerageModel(BrokerageName.ALPACA, AccountType.Margin)

        # Correct Symbol Handling
        self.BASE_SYMBOL_TICKER = register_param('base symbol for algorithm management: ', 'SPY')   
        self.tickers = ["SPY", "QQQ", "DIA", "TLT", "GLD", "EFA", "EEM", "BND", "VNQ"]
        
        self.symbols = []
        for ticker in self.tickers:
            security = self.AddEquity(ticker, Resolution.Minute)
            self.symbols.append(security.Symbol) 

        self.BASE_SYMBOL = self.Securities[self.BASE_SYMBOL_TICKER].Symbol
        self.exchange = self.Securities[self.BASE_SYMBOL].Exchange

        self.openMarketOnOpenOrders = []
        self._init_prices = False
        self._longs = list() 
        self._shorts = list()
        register_param('symbols: ', [s.Value for s in self.symbols])
        self._holding_period = register_param('holding period (days)', 30)
        
        # MODIFICATION 1: Set lookback to 60 days
        self.LOOKBACK = register_param('historical lookback (days)', 60)
        
        self.BET_SIZE = register_param('bet size (%)', 1/len(self.symbols))
    
        self.RANDOM_STATE = register_param('random_state', 777)
        self.ALPHA = register_param('gmm alpha', 0.95) 
        
        # MODIFICATION 2: Set GMM components to 2
        self.N_COMPONENTS = register_param('gmm n components', 2)        
        
        self.THRES = register_param('threshold probability for buy signal', 0.9) 
        self.SAMPLES = register_param('number of samples for bootstrap', 1000)
        self.HISTORY_RESOLUTION = Resolution.Daily 
        register_param('history api resolution', str(self.HISTORY_RESOLUTION)) 

        # Charting setup remains the same
        self.splotName = 'Strategy Info'
        sPlot = Chart(self.splotName)
        sPlot.AddSeries(Series('RAM',  SeriesType.Line, 0))
        sPlot.AddSeries(Series('Time',  SeriesType.Line, 1))
        sPlot.AddSeries(Series('Cash',  SeriesType.Line, 2))
        sPlot.AddSeries(Series('Leverage',  SeriesType.Line, 3))
        self.AddChart(sPlot)
        self.time_to_run_main_algo = 0

        # Scheduled functions
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 5), Action(self.init_prices))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 10), Action(self.run_main_algo))
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 30), Action(self.send_orders)) 
        self.Schedule.On(self.DateRules.Every(DayOfWeek.Monday, DayOfWeek.Friday), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 35), Action(self.check_liquidate))
        self.Schedule.On(self.DateRules.EveryDay(self.BASE_SYMBOL), self.TimeRules.AfterMarketOpen(self.BASE_SYMBOL, 40), Action(self.CHART_RAM))

        self.Debug('\n'+'-'*77+'\nPARAMETER REGISTRY\n{}...'.format(json.dumps(PARAMETER_REGISTRY, indent=2)))

    def init_prices(self):
        if not self.symbols: self.Log('no symbols'); return
        if self._init_prices: return 
        
        self.prices = (self.History(self.symbols, self.LOOKBACK, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self._init_prices=True
       
    def update_prices(self):
        most_recent_date = self.prices.index.max()
        current_date = self.Time
        days_to_request = how_many_days(current_date, most_recent_date)
        if zero_days_to_request(days_to_request): return

        new_prices = (self.History(self.symbols, days_to_request, self.HISTORY_RESOLUTION)["close"].unstack(level=0).astype(np.float32))
        self.prices = make_update_df(self.prices, new_prices, self.LOOKBACK)
        return
    
    def check_liquidate(self):
        """
        Robust liquidation check using GetOrderTickets() and manual filtering.
        Uses the integer value of OrderEventStatus.Filled (which is 3).
        """
        self.Log('\n'+'-'*77+'\n[{}] checking liquidation status...'.format(self.UtcTime))
                
        # Retrieve ALL order tickets once
        all_tickets = self.Transactions.GetOrderTickets()
        
        for holding in self.Portfolio.Values:
            if not holding.Invested:
                continue 
            
            # Filter all tickets to find those matching the current holding's Symbol
            symbol_tickets = [t for t in all_tickets if t.Symbol == holding.Symbol]
            
            latest_fill_event = None
            
            # Find the latest FILL event that was a BUY order
            for ticket in symbol_tickets:
                # Use the raw integer value of the status (OrderEventStatus.Filled == 3)
                fill_events = [e for e in ticket.OrderEvents if e.Status == ORDER_STATUS_FILLED and e.FillQuantity > 0]

                if fill_events:
                    # Find the latest fill event across all tickets
                    current_latest_fill = max(fill_events, key=lambda x: x.UtcTime)
                    
                    if latest_fill_event is None or current_latest_fill.UtcTime > latest_fill_event.UtcTime:
                        latest_fill_event = current_latest_fill
            
            if latest_fill_event:
                entry_time = latest_fill_event.UtcTime
                
                # Check if the current time is past the entry time + holding period
                if self.UtcTime >= (entry_time + timedelta(self._holding_period)):
                    self.Liquidate(holding.Symbol)
                    fmt_args = (self.UtcTime, holding.Symbol.Value, entry_time, self.UtcTime - entry_time)
                    self.Log('[{}] liquidating... {}, order date: {}, time delta: {}'.format(*fmt_args))
        return

    def run_main_algo(self):
        self.Log('\n'+'-'*77+'\n[{}] Begin main algorithm computation...'.format(self.UtcTime))
        
        start_time = time.time()
        self.update_prices()
        self._algo_data = False
        self._longs = list() 
        self._shorts = list()

        for sym_obj in self.symbols: 
            sym = sym_obj.Value
            
            try:
                self.Log('checking symbol: {}'.format(str(sym)))
                pred_rows = list()
                
                # Check if we should perform the GMM calculation
                if (not self.Portfolio[sym_obj].Invested):
                    
                    if sym not in self.prices.columns: continue
                        
                    train_px = self.prices.copy()
                    train_ts = make_returns(train_px)[sym].dropna()
                    train_ts = train_ts[np.isfinite(train_ts)]
                    
                    if train_ts.shape[0] < self.N_COMPONENTS + 1: # Ensure enough data points for GMM
                         self.Debug('{} train data has too few samples (<{})'.format(str(sym), self.N_COMPONENTS + 1))
                         continue

                    tmp_X_train = train_ts.values.reshape(-1, 1)
            
                    ### fit GMM ###
                    gmm = make_gmm(n_components=self.N_COMPONENTS, random_state=self.RANDOM_STATE).fit(tmp_X_train)
                    hidden_states = gmm.predict(tmp_X_train)
                    hidden_state_prob = pd.DataFrame(gmm.predict_proba(tmp_X_train), columns=['s1','s2'], index=train_ts.index)
                    
                    state_df = train_ts.to_frame()
                    hs_prob_df = (pd.concat([state_df, hidden_state_prob],axis=1))
                    
                    # Variables are defined here:
                    s1_mu = hs_prob_df.query('abs(s1)>0.5')[sym].mean() 
                    s2_mu = hs_prob_df.query('abs(s2)>0.5')[sym].mean() 
                    s1_std = hs_prob_df.query('abs(s1)>0.5')[sym].std() 
                    s2_std = hs_prob_df.query('abs(s2)>0.5')[sym].std()           
                    
                    # GMM state and interval calculation
                    last_state = hidden_states[-1]
                    last_mean = gmm.means_[last_state][0]
                    last_var = np.diag(gmm.covariances_[last_state])[0]
                    
                    rvs = gmm.sample(self.SAMPLES)[0] 
                    # Corrected argument name for scipy.stats compatibility
                    low_ci, high_ci = stats.norm.interval(confidence=self.ALPHA, loc=np.mean(rvs), scale=np.std(rvs))
            
                    tmp_ret = np.log(float(self.Securities[sym_obj].Price) / train_px[sym].iloc[-1])
                            
                    ### Row creation is now safely inside the calculation block
                    row = (train_ts.index[-1], last_state, last_mean, np.sqrt(last_var), 
                            low_ci, high_ci, tmp_ret,
                            gmm.means_.ravel()[0], gmm.means_.ravel()[1],
                            np.sqrt(np.diag(gmm.covariances_[0]))[0], np.sqrt(np.diag(gmm.covariances_[1]))[0],
                            hidden_state_prob.iloc[-1][0], hidden_state_prob.iloc[-1][1],
                            s1_mu,s2_mu,s1_std,s2_std)
                    pred_rows.append(row)
                    self.Debug('{} rowzz:\n{}'.format(str(sym), row))
                    
                if pred_rows:
                    cols = ['Dates', 'ith_state', 'ith_ret','ith_std', 'low_ci', 'high_ci', 'current_return',
                            'last_mean_class_0', 'last_mean_class_1', 'last_std_class_0', 'last_std_class_1',
                            'last_prob_class_0', 'last_prob_class_1', 'avg_class_0_mean', 'avg_class_1_mean',
                            'avg_class_0_std', 'avg_class_1_std']             
                    
                    pred_df = make_final_pred_df(pred_rows, cols, self.THRES, sym)
                    
                    if pred_df.iloc[-1].loc['buys']==1: 
                        self._longs.append(sym_obj)
                        self.Debug('>>> BUY SIGNAL GENERATED for {} (Prob > {}) <<<'.format(sym, self.THRES))

                else:
                    self.Debug('missing or invested in {}'.format(sym))
                    
            except Exception as e:
                self.Debug('{} error: {}'.format(sym, e))
                continue

        self.Debug('Final Longs List for next order run: {}'.format([s.Value for s in self._longs])) 

        self.time_to_run_main_algo = time.time() - start_time
        self.Plot(self.splotName, 'Time', self.time_to_run_main_algo)                
        return 
    
    def send_orders(self):
        self.Log('\n'+'-'*77+'\n[{}] checking buy sell arrays to send orders...'.format(self.UtcTime))

        if self._longs:
            for sym_obj in self._longs:
                if not self.Portfolio[sym_obj].Invested:
                    # Use MarketOrder for execution 30 minutes after open
                    self.Log('[{}] SENDING MARKET ORDER for {}...'.format(self.UtcTime, sym_obj.Value))                        
                    self.MarketOrder(sym_obj, self.CalculateOrderQuantity(sym_obj, self.BET_SIZE))
                else:
                    self.Debug('Skipping {} - Already invested.'.format(sym_obj.Value))
        else:
            self.Log('send_orders >> no longs listed, no orders sent...')            
        return
    
    
    def OnData(self, data):
        pass

    def CHART_RAM(self):
        self.Plot(self.splotName,'RAM', OS.ApplicationMemoryUsed/1024.)
        P = self.Portfolio
        if P.TotalPortfolioValue != 0:
            self.track_account_leverage = P.TotalAbsoluteHoldingsCost/P.TotalPortfolioValue
            self.Plot(self.splotName, 'Leverage', float(self.track_account_leverage))
        
        self.Plot(self.splotName, 'Cash', float(self.Portfolio.Cash))
        return