# metrics.py
import numpy as np
import pandas as pd
import statsmodels.api as sm
import streamlit as st
from scipy.stats import skew, kurtosis

def calculate_sharpe_ratio(returns, rf=0.02):
    """
    Calculate the Sharpe Ratio for a given set of returns.
    """
    excess_return = returns.mean() * 252 - rf
    std_dev = returns.std() * np.sqrt(252)
    return excess_return / std_dev if std_dev != 0 else np.nan

def calculate_sortino_ratio(returns, rf=0.02):
    """
    Calculate the Sortino Ratio for a given set of returns.
    """
    excess_return = returns.mean() * 252 - rf
    downside_std = returns[returns < 0].std() * np.sqrt(252)
    return excess_return / downside_std if downside_std != 0 else np.nan

def calculate_beta(returns, benchmark_returns):
    """
    Calculate the Beta of the portfolio relative to the benchmark.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance_matrix = np.cov(returns, benchmark_returns)
    covariance = covariance_matrix[0, 1]
    benchmark_variance = covariance_matrix[1, 1]
    return covariance / benchmark_variance if benchmark_variance != 0 else np.nan

def calculate_alpha(returns, benchmark_returns, rf=0.02):
    """
    Calculate the Alpha of the portfolio relative to the benchmark.
    """
    beta = calculate_beta(returns, benchmark_returns)
    portfolio_return = returns.mean() * 252
    benchmark_return = benchmark_returns.mean() * 252
    return portfolio_return - (rf + beta * (benchmark_return - rf)) if not np.isnan(beta) else np.nan

def calculate_treynor_ratio(returns, benchmark_returns, rf=0.02):
    """
    Calculate the Treynor Ratio for a given set of returns.
    """
    beta = calculate_beta(returns, benchmark_returns)
    excess_return = returns.mean() * 252 - rf
    return excess_return / beta if beta != 0 else np.nan

def calculate_calmar_ratio(returns, cum_returns):
    """
    Calculate the Calmar Ratio for a given set of returns.
    """
    annual_return = returns.mean() * 252
    max_dd = drawdown(cum_returns)
    return annual_return / abs(max_dd) if max_dd != 0 else np.nan

def calculate_r_squared(returns, benchmark_returns):
    """
    Calculate the R-squared of the portfolio relative to the benchmark.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance = np.cov(returns, benchmark_returns)
    var_port = covariance[0,0]
    var_bench = covariance[1,1]
    cov = covariance[0,1]
    return (cov ** 2) / (var_port * var_bench) if var_port !=0 and var_bench !=0 else np.nan

def calculate_information_ratio(returns, benchmark_returns):
    """
    Calculate the Information Ratio of the portfolio relative to the benchmark.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    active_return = (returns.mean() - benchmark_returns.mean()) * 252
    tracking_error = calculate_tracking_error(returns, benchmark_returns)
    return active_return / tracking_error if tracking_error != 0 else np.nan

def calculate_tracking_error(returns, benchmark_returns):
    """
    Calculate the Tracking Error of the portfolio relative to the benchmark.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return np.std((returns - benchmark_returns)) * np.sqrt(252)

def calculate_gain_loss_ratio(returns):
    """
    Calculate the Gain/Loss Ratio of the portfolio.
    """
    gains = returns[returns > 0].sum()
    losses = -returns[returns < 0].sum()
    return gains / losses if losses != 0 else np.nan

def drawdown(cum_returns):
    """
    Calculate the Maximum Drawdown of the portfolio.
    """
    if cum_returns.empty:
        return np.nan
    peak = cum_returns.expanding(min_periods=1).max()
    dd = (cum_returns / peak) - 1
    return dd.min()

def calculate_capture_ratio(returns, benchmark_returns, upside=True):
    """
    Calculate the Upside or Downside Capture Ratio of the portfolio.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    mask = benchmark_returns > 0 if upside else benchmark_returns < 0
    if mask.sum() == 0:
        return np.nan
    portfolio = returns[mask]
    benchmark = benchmark_returns[mask]
    return (portfolio.sum() / benchmark.sum()) * 100 if benchmark.sum() != 0 else np.nan

def calculate_safe_withdrawal_rate(returns):
    """
    Calculate the Safe Withdrawal Rate based on portfolio returns.
    """
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

def calculate_perpetual_withdrawal_rate(returns):
    """
    Calculate the Perpetual Withdrawal Rate based on portfolio returns.
    """
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

def calculate_positive_periods(returns):
    """
    Calculate the number and percentage of positive return periods.
    """
    if returns.empty:
        return "N/A"
    positive = returns > 0
    return f"{positive.sum()} out of {len(returns)} ({(positive.sum()/len(returns))*100:.2f}%)"

def calculate_modigliani_miller(returns, benchmark_returns, rf=0.02):
    """
    Calculate the Modigliani–Modigliani Measure (M²) of the portfolio.
    """
    sharpe = calculate_sharpe_ratio(returns, rf)
    alpha = calculate_alpha(returns, benchmark_returns, rf)
    return alpha / sharpe if sharpe != 0 and not np.isnan(alpha) else np.nan

def calculate_beta_local(portfolio_returns, benchmark_returns):
    """
    Local version of calculate_beta to avoid naming conflicts.
    """
    if portfolio_returns.empty or benchmark_returns.empty:
        return np.nan
    covariance_matrix = np.cov(portfolio_returns, benchmark_returns)
    covariance = covariance_matrix[0, 1]
    benchmark_variance = covariance_matrix[1, 1]
    return covariance / benchmark_variance if benchmark_variance != 0 else np.nan

def calculate_var(returns, confidence_level=0.95):
    """
    Calculate the Value at Risk (VaR) for a given set of returns.
    """
    return np.percentile(returns, 100 * (1 - confidence_level))

def calculate_cvar(returns, confidence_level=0.95):
    """
    Calculate the Conditional Value at Risk (CVaR) for a given set of returns.
    """
    var = calculate_var(returns, confidence_level)
    return returns[returns <= var].mean()

def calculate_simple_metrics(returns, cum_returns, start_balance=10000):
    metrics = {}
    metrics['Start Balance'] = f"${start_balance:,.2f}"
    if not cum_returns.empty:
        try:
            end_balance = start_balance * cum_returns.iloc[-1]
            metrics['End Balance'] = f"${end_balance:,.2f}"
            start_date = cum_returns.index[0]
            end_date = cum_returns.index[-1]
            days = (end_date - start_date).days
            years = days / 365.25
            cagr = (end_balance / start_balance) ** (1 / years) - 1 if years > 0 else np.nan
            metrics['Annualized Return (CAGR)'] = f"{cagr * 100:.2f}%" if not np.isnan(cagr) else "N/A"
            yearly_returns = returns.resample('Y').apply(lambda x: (1 + x).prod() - 1)
            best_year = yearly_returns.max() * 100 if not yearly_returns.empty else np.nan
            worst_year = yearly_returns.min() * 100 if not yearly_returns.empty else np.nan
            metrics['Best Year'] = f"{best_year:.2f}%" if not np.isnan(best_year) else "N/A"
            metrics['Worst Year'] = f"{worst_year:.2f}%" if not np.isnan(worst_year) else "N/A"
            metrics['Arithmetic Mean (Monthly)'] = f"{returns.mean() * 100:.2f}%" if not returns.empty else "N/A"
            metrics['Arithmetic Mean (Annualized)'] = f"{returns.mean() * 252 * 100:.2f}%" if not returns.empty else "N/A"
            metrics['Geometric Mean (Monthly)'] = f"{(np.exp(np.log1p(returns).mean()) - 1) * 100:.2f}%" if not returns.empty else "N/A"
            metrics['Geometric Mean (Annualized)'] = f"{(np.exp(np.log1p(returns).mean() * 252) - 1) * 100:.2f}%" if not returns.empty else "N/A"
            metrics['Standard Deviation (Monthly)'] = f"{returns.std() * 100:.2f}%" if not returns.empty else "N/A"
            metrics['Standard Deviation (Annualized)'] = f"{returns.std() * np.sqrt(252) * 100:.2f}%" if not returns.empty else "N/A"
            metrics['Downside Deviation (Monthly)'] = f"{returns[returns < 0].std() * 100:.2f}%" if not returns.empty else "N/A"
            metrics['Maximum Drawdown'] = f"{drawdown(cum_returns) * 100:.2f}%" if not cum_returns.empty else "N/A"
            sharpe = calculate_sharpe_ratio(returns, 0.02)
            metrics['Sharpe Ratio'] = f"{sharpe:.2f}" if not np.isnan(sharpe) else "N/A"
            sortino = calculate_sortino_ratio(returns, 0.02)
            metrics['Sortino Ratio'] = f"{sortino:.2f}" if not np.isnan(sortino) else "N/A"
            gain_loss = calculate_gain_loss_ratio(returns)
            metrics['Gain/Loss Ratio'] = f"{gain_loss:.2f}" if not np.isnan(gain_loss) else "N/A"
            skewness = skew(returns) if not returns.empty else np.nan
            metrics['Skewness'] = f"{skewness:.2f}" if not np.isnan(skewness) else "N/A"
            excess_kurtosis = kurtosis(returns) if not returns.empty else np.nan
            metrics['Excess Kurtosis'] = f"{excess_kurtosis:.2f}" if not np.isnan(excess_kurtosis) else "N/A"
            safe_withdrawal = calculate_safe_withdrawal_rate(returns)
            metrics['Safe Withdrawal Rate'] = f"{safe_withdrawal:.2f}%" if not np.isnan(safe_withdrawal) else "N/A"
            perpetual_withdrawal = calculate_perpetual_withdrawal_rate(returns)
            metrics['Perpetual Withdrawal Rate'] = f"{perpetual_withdrawal:.2f}%" if not np.isnan(perpetual_withdrawal) else "N/A"
            metrics['Positive Periods'] = calculate_positive_periods(returns) if not returns.empty else "N/A"
        except Exception as e:
            st.error(f"Error calculating simple metrics: {e}")
            for key in ['End Balance', 'Annualized Return (CAGR)', 'Best Year', 'Worst Year',
                        'Arithmetic Mean (Monthly)', 'Arithmetic Mean (Annualized)',
                        'Geometric Mean (Monthly)', 'Geometric Mean (Annualized)',
                        'Standard Deviation (Monthly)', 'Standard Deviation (Annualized)',
                        'Downside Deviation (Monthly)', 'Maximum Drawdown',
                        'Sharpe Ratio', 'Sortino Ratio', 'Gain/Loss Ratio',
                        'Skewness', 'Excess Kurtosis', 'Safe Withdrawal Rate',
                        'Perpetual Withdrawal Rate', 'Positive Periods']:
                metrics[key] = "N/A"
    else:
        for key in ['End Balance', 'Annualized Return (CAGR)', 'Best Year', 'Worst Year',
                    'Arithmetic Mean (Monthly)', 'Arithmetic Mean (Annualized)',
                    'Geometric Mean (Monthly)', 'Geometric Mean (Annualized)',
                    'Standard Deviation (Monthly)', 'Standard Deviation (Annualized)',
                    'Downside Deviation (Monthly)', 'Maximum Drawdown',
                    'Sharpe Ratio', 'Sortino Ratio', 'Gain/Loss Ratio',
                    'Skewness', 'Excess Kurtosis', 'Safe Withdrawal Rate',
                    'Perpetual Withdrawal Rate', 'Positive Periods']:
            metrics[key] = "N/A"
    
    return metrics

def calculate_portfolio_metrics(returns, cum_returns, rf_rate=0.02, benchmark_returns=None):
    """
    Calculate various portfolio metrics.
    
    Parameters:
    - returns (pd.Series or pd.DataFrame): Portfolio's daily returns.
    - cum_returns (pd.Series): Cumulative returns of the portfolio.
    - rf_rate (float): Risk-free rate.
    - benchmark_returns (pd.Series or pd.DataFrame, optional): Benchmark's daily returns.
    
    Returns:
    - dict: Calculated metrics.
    """
    metrics = {}
    try:
        metrics['Start Balance'] = f"$100.00"  # Assuming starting with 100
        metrics['End Balance'] = f"${cum_returns.iloc[-1]:,.2f}" if not cum_returns.empty else "N/A"
        
        if not cum_returns.empty:
            start_date = cum_returns.index[0]
            end_date = cum_returns.index[-1]
            num_days = (end_date - start_date).days
            years = num_days / 365.25 if num_days > 0 else np.nan
            
            # Annualized Return (CAGR)
            start_balance = 100.0
            end_balance = cum_returns.iloc[-1]
            if years > 0:
                cagr = (end_balance / start_balance) ** (1 / years) - 1
                metrics['Annualized Return (CAGR)'] = f"{cagr * 100:.2f}%"
            else:
                metrics['Annualized Return (CAGR)'] = "N/A"
            
            # Standard Deviation (Annualized)
            std_annual = returns.std() * np.sqrt(252) * 100 if not returns.empty else np.nan
            metrics['Standard Deviation (Annualized)'] = f"{std_annual:.2f}%" if not np.isnan(std_annual) else "N/A"
            
            # Best Year
            yearly_returns = returns.resample('Y').apply(lambda x: (1 + x).prod() - 1)
            best_year = yearly_returns.max() * 100 if not yearly_returns.empty else np.nan
            metrics['Best Year'] = f"{best_year:.2f}%" if not np.isnan(best_year) else "N/A"
            
            # Worst Year
            worst_year = yearly_returns.min() * 100 if not yearly_returns.empty else np.nan
            metrics['Worst Year'] = f"{worst_year:.2f}%" if not np.isnan(worst_year) else "N/A"
            
            # Maximum Drawdown
            max_dd = drawdown(cum_returns) * 100 if not cum_returns.empty else np.nan
            metrics['Maximum Drawdown'] = f"{max_dd:.2f}%" if not np.isnan(max_dd) else "N/A"
            
            # Sharpe Ratio
            sharpe = calculate_sharpe_ratio(returns, rf_rate)
            metrics['Sharpe Ratio'] = f"{sharpe:.2f}" if not np.isnan(sharpe) else "N/A"
            
            # Sortino Ratio
            sortino = calculate_sortino_ratio(returns, rf_rate)
            metrics['Sortino Ratio'] = f"{sortino:.2f}" if not np.isnan(sortino) else "N/A"
            
            # Gain/Loss Ratio
            gain_loss = calculate_gain_loss_ratio(returns)
            metrics['Gain/Loss Ratio'] = f"{gain_loss:.2f}" if not np.isnan(gain_loss) else "N/A"
            
            # Skewness
            skewness = skew(returns) if not returns.empty else np.nan
            metrics['Skewness'] = f"{skewness:.2f}" if not np.isnan(skewness) else "N/A"
            
            # Excess Kurtosis
            excess_kurtosis = kurtosis(returns) if not returns.empty else np.nan
            metrics['Excess Kurtosis'] = f"{excess_kurtosis:.2f}" if not np.isnan(excess_kurtosis) else "N/A"
            
            # Safe Withdrawal Rate
            safe_withdrawal = calculate_safe_withdrawal_rate(returns)
            metrics['Safe Withdrawal Rate'] = f"{safe_withdrawal:.2f}%" if not np.isnan(safe_withdrawal) else "N/A"
            
            # Perpetual Withdrawal Rate
            perpetual_withdrawal = calculate_perpetual_withdrawal_rate(returns)
            metrics['Perpetual Withdrawal Rate'] = f"{perpetual_withdrawal:.2f}%" if not np.isnan(perpetual_withdrawal) else "N/A"
            
            # Positive Periods
            metrics['Positive Periods'] = calculate_positive_periods(returns) if not returns.empty else "N/A"
            
            # Information Ratio
            if benchmark_returns is not None and not benchmark_returns.empty:
                info_ratio = calculate_information_ratio(returns, benchmark_returns)
                metrics['Information Ratio'] = f"{info_ratio:.2f}" if not np.isnan(info_ratio) else "N/A"
            else:
                metrics['Information Ratio'] = "N/A"
            
            # Calmar Ratio
            calmar = calculate_calmar_ratio(returns, cum_returns)
            metrics['Calmar Ratio'] = f"{calmar:.2f}" if not np.isnan(calmar) else "N/A"
            
            # Treynor Ratio
            if benchmark_returns is not None and not benchmark_returns.empty:
                treynor = calculate_treynor_ratio(returns, benchmark_returns, rf_rate)
                metrics['Treynor Ratio'] = f"{treynor:.2f}" if not np.isnan(treynor) else "N/A"
            else:
                metrics['Treynor Ratio'] = "N/A"
            
            # Alpha (annualized)
            if benchmark_returns is not None and not benchmark_returns.empty:
                alpha = calculate_alpha(returns, benchmark_returns, rf_rate)
                metrics['Alpha (annualized)'] = f"{alpha * 100:.2f}%" if not np.isnan(alpha) else "N/A"
            else:
                metrics['Alpha (annualized)'] = "N/A"
            
            # Modigliani–Modigliani Measure
            m2 = calculate_modigliani_miller(returns, benchmark_returns, rf_rate) if benchmark_returns is not None else np.nan
            metrics['Modigliani–Modigliani Measure'] = f"{m2:.2f}" if not np.isnan(m2) else "N/A"
            
            # Upside Capture Ratio
            if benchmark_returns is not None and not benchmark_returns.empty:
                upside_capture = calculate_capture_ratio(returns, benchmark_returns, upside=True)
                metrics['Upside Capture Ratio'] = f"{upside_capture:.2f}%" if not np.isnan(upside_capture) else "N/A"
            else:
                metrics['Upside Capture Ratio'] = "N/A"
            
            # Benchmark Correlation, Beta, R-squared
            if benchmark_returns is not None and not benchmark_returns.empty:
                correlation = returns.corr(benchmark_returns)
                beta = calculate_beta(returns, benchmark_returns)
                r_squared = calculate_r_squared(returns, benchmark_returns)
                metrics['Benchmark Correlation'] = f"{correlation:.2f}" if not np.isnan(correlation) else "N/A"
                metrics['Beta'] = f"{beta:.2f}" if not np.isnan(beta) else "N/A"
                metrics['R2'] = f"{r_squared:.2f}" if not np.isnan(r_squared) else "N/A"
            else:
                metrics['Benchmark Correlation'] = "N/A"
                metrics['Beta'] = "N/A"
                metrics['R2'] = "N/A"
            
        else:
            metrics['Annualized Return (CAGR)'] = "N/A"
            metrics['End Balance'] = "N/A"
            metrics['Standard Deviation (Annualized)'] = "N/A"
            metrics['Best Year'] = "N/A"
            metrics['Worst Year'] = "N/A"
            metrics['Maximum Drawdown'] = "N/A"
            metrics['Sharpe Ratio'] = "N/A"
            metrics['Sortino Ratio'] = "N/A"
            metrics['Gain/Loss Ratio'] = "N/A"
            metrics['Skewness'] = "N/A"
            metrics['Excess Kurtosis'] = "N/A"
            metrics['Safe Withdrawal Rate'] = "N/A"
            metrics['Perpetual Withdrawal Rate'] = "N/A"
            metrics['Positive Periods'] = "N/A"
            metrics['Information Ratio'] = "N/A"
            metrics['Calmar Ratio'] = "N/A"
            metrics['Treynor Ratio'] = "N/A"
            metrics['Alpha (annualized)'] = "N/A"
            metrics['Modigliani–Modigliani Measure'] = "N/A"
            metrics['Upside Capture Ratio'] = "N/A"
            metrics['Benchmark Correlation'] = "N/A"
            metrics['Beta'] = "N/A"
            metrics['R2'] = "N/A"
        
        return metrics

def calculate_risk_factor_attribution(returns, factors, annualization_factor=252):
    """
    Decompose portfolio returns by risk factors using linear regression and provide
    comments on the factor contributions.
    
    Parameters:
    - returns (pd.Series): Portfolio returns.
    - factors (pd.DataFrame): DataFrame where each column represents a risk factor.
                             Must include 'RF' (Risk-Free Rate) if available.
    - annualization_factor (int): Annualization factor (252 for daily, 12 for monthly returns).

    Returns:
    - pd.DataFrame: Factor attribution results
    - str or None: Error message if any issues occur, else None
    """
    try:
        # Ensure returns have a name, or assign one if missing
        if returns.name is None:
            returns = returns.rename("Portfolio_Returns")

        # Convert returns to a DataFrame
        returns = returns.to_frame()

        # Ensure both returns and factors are in decimal format
        if returns['Portfolio_Returns'].abs().max() > 1:
            returns = returns / 100

        if factors.abs().max().max() > 1:
            factors = factors / 100

        # Adjust returns to excess returns by subtracting RF (if it exists in factors)
        if 'RF' in factors.columns:
            returns['Excess_Returns'] = returns['Portfolio_Returns'] - factors['RF']
            factors = factors.drop(columns=['RF'])
        else:
            returns['Excess_Returns'] = returns['Portfolio_Returns']

        # Align the data using an inner join and drop any remaining NaN values
        aligned_data = pd.merge(returns, factors, left_index=True, right_index=True, how='inner').dropna()

        # Check that we have data after merging
        if aligned_data.empty:
            raise ValueError("No overlapping dates between returns and factors after dropping NaNs.")

        # Prepare X and y for regression
        X = aligned_data[factors.columns]
        X = sm.add_constant(X)  # Add intercept
        y = aligned_data['Excess_Returns']

        # Run the regression model
        model = sm.OLS(y, X).fit()
        coefficients = model.params.drop('const', errors='ignore')

        # Calculate factor contributions based on mean returns
        factor_means = X.mean().drop('const', errors='ignore')
        contributions = coefficients * factor_means

        # Calculate total excess return
        total_return = y.mean()

        # Check if total_return is zero to avoid division by zero
        if np.isclose(total_return, 0):
            attribution_df = pd.DataFrame({
                'Factor': contributions.index,
                'Contribution (%)': ["0.00"] * len(contributions)
            })
        else:
            # Calculate percentage contributions
            attribution_df = pd.DataFrame({
                'Factor': contributions.index,
                'Contribution (%)': (contributions / total_return * 100).round(2).astype(str) + "%"
            })

        # Remove the constant term if it exists
        attribution_df = attribution_df[attribution_df['Factor'] != 'const']

        # Replace factor abbreviations with full names
        factor_full_names = {
            'Mkt-RF': 'Market Risk Premium',
            'SMB': 'Small Minus Big',
            'HML': 'High Minus Low',
            'UMD': 'Up Minus Down'
        }
        attribution_df['Factor'] = attribution_df['Factor'].replace(factor_full_names)

        # Generate comments based on factor contributions
        comments = []
        for _, row in attribution_df.iterrows():
            factor_name = row['Factor']
            contribution = float(row['Contribution (%)'].strip('%'))
            
            if factor_name == 'Market Risk Premium':
                if contribution > 75:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Extremely high sensitivity to market movements.")
                elif contribution > 50:
                    comments.append(f"{factor_name} = {contribution:.2f}%: High sensitivity to overall market movements.")
                elif contribution > 30:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Notable sensitivity to market risk.")
                elif contribution > 10:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Moderate exposure to market risk.")
                else:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Low exposure to market risk.")
            
            elif factor_name == 'Small Minus Big':
                if contribution > 5:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Strong tilt towards small-cap stocks.")
                elif contribution > 0:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Mild preference for small-cap stocks.")
                elif contribution > -5:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Slight tilt towards larger-cap stocks.")
                else:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Clear preference for large-cap stocks.")
            
            elif factor_name == 'High Minus Low':
                if contribution > 5:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Strong tilt toward value stocks.")
                elif contribution > 0:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Slight positive tilt toward value stocks.")
                elif contribution > -5:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Minor negative tilt toward growth stocks.")
                else:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Clear preference for growth stocks.")
            
            else:
                if contribution > 0:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Positive contribution to portfolio returns.")
                elif contribution < 0:
                    comments.append(f"{factor_name} = {contribution:.2f}%: Negative contribution to portfolio returns.")
                else:
                    comments.append(f"{factor_name} = {contribution:.2f}%: No significant contribution.")

        # Add comments to the DataFrame
        attribution_df['Comment'] = comments

        return attribution_df, None

    except Exception as e:
        error_message = f"Error calculating risk factor attribution: {e}"
        return pd.DataFrame(), error_message

def calculate_performance_attribution(returns, weights):
    """
    Calculate the performance attribution of the portfolio.

    Parameters:
    - returns (pd.DataFrame or pd.Series): Historical returns of assets.
    - weights (list or np.ndarray): Weights of each asset in the portfolio.

    Returns:
    - pd.DataFrame: Asset names, their contribution percentages, and explanatory comments.
    """
    try:
        if isinstance(returns, pd.Series):
            returns = returns.to_frame('Asset')  # Convert Series to DataFrame with a default column name
        
        if len(weights) != len(returns.columns):
            st.error(f"Number of weights ({len(weights)}) does not match number of assets ({len(returns.columns)}).")
            return pd.DataFrame()
        
        annual_returns = returns.mean() * 252
        contributions = annual_returns * weights
        total_contribution = contributions.sum()
        if total_contribution == 0:
            contribution_percentages = [0 for _ in contributions]
        else:
            contribution_percentages = (contributions / total_contribution * 100).round(2)
        
        attribution_df = pd.DataFrame({
            'Asset': returns.columns,
            'Contribution (%)': contribution_percentages.astype(str) + "%"
        }).sort_values(by='Contribution (%)', ascending=False)
        return attribution_df
    except Exception as e:
        st.error(f"Error calculating performance attribution: {e}")
        return pd.DataFrame()

def calculate_final_score(primary_metrics, comparison_metrics, global_skipped=None):
    """
    Calculate the final score for a portfolio based on various metrics compared to a benchmark.

    Parameters:
    - primary_metrics (dict): Metrics of the primary portfolio.
    - comparison_metrics (dict): Metrics of the comparison (benchmark) portfolio.
    - global_skipped (set, optional): A set to accumulate skipped metrics due to missing data or errors.

    Returns:
    - float: Final score out of 100.
    """
    if global_skipped is None:
        global_skipped = set()
    
    # Define metrics and their corresponding weights
    metrics_to_compare = [
        'Annualized Return (CAGR)',
        'Sharpe Ratio',
        'Sortino Ratio',
        'Treynor Ratio',
        'Calmar Ratio',
        'Alpha (annualized)',
        'Information Ratio',
        'Modigliani–Modigliani Measure',
        'Upside Capture Ratio',
        'Gain/Loss Ratio',
        'Maximum Drawdown'  # Added Maximum Drawdown to handle lower is better
    ]
    
    metric_weights = {
        'Annualized Return (CAGR)': 15,
        'Sharpe Ratio': 15,
        'Sortino Ratio': 10,
        'Treynor Ratio': 10,
        'Calmar Ratio': 10,
        'Alpha (annualized)': 10,
        'Information Ratio': 5,
        'Modigliani–Modigliani Measure': 5,
        'Upside Capture Ratio': 5,
        'Gain/Loss Ratio': 5,
        'Maximum Drawdown': 10  # Assign a weight for Maximum Drawdown
    }

    # Ensure metric weights sum to 100
    total_assigned_weight = sum(metric_weights.values())
    if not np.isclose(total_assigned_weight, 100):
        raise ValueError(
            f"Total metric weights must sum to 100, but got {total_assigned_weight}. "
            "Please adjust the weights accordingly."
        )

    # Define which metrics are better when higher or lower
    higher_is_better_metrics = {
        'Annualized Return (CAGR)',
        'Sharpe Ratio',
        'Sortino Ratio',
        'Treynor Ratio',
        'Calmar Ratio',
        'Alpha (annualized)',
        'Information Ratio',
        'Modigliani–Modigliani Measure',
        'Upside Capture Ratio',
        'Gain/Loss Ratio'
    }
    
    lower_is_better_metrics = {
        'Maximum Drawdown'
    }

    score = 0.0
    total_weight = 0.0
    skipped_metrics = set()

    for metric in metrics_to_compare:
        primary = primary_metrics.get(metric, "N/A")
        comparison = comparison_metrics.get(metric, "N/A")
        
        if primary == "N/A" or comparison == "N/A":
            skipped_metrics.add(metric)
            continue
        
        try:
            # Convert metrics to float, handling percentage strings
            def convert_to_float(value):
                if isinstance(value, str):
                    value = value.replace('$', '').replace('%', '').replace(',', '')
                return float(value)
            
            primary_val = convert_to_float(primary)
            comparison_val = convert_to_float(comparison)
            
            # Check for finite numbers
            if not (np.isfinite(primary_val) and np.isfinite(comparison_val)):
                skipped_metrics.add(metric)
                continue
            
            weight = metric_weights.get(metric, 0)
            
            if metric in higher_is_better_metrics:
                # Avoid division by zero
                if comparison_val == 0:
                    skipped_metrics.add(metric)
                    continue
                
                ratio = primary_val / comparison_val
                ratio = min(max(ratio, 0), 2)  # Cap ratio between 0 and 2
                
                # Full credit if primary >= comparison, partial otherwise
                if primary_val >= comparison_val:
                    metric_score = weight * ratio
                else:
                    metric_score = weight * ratio * 0.5  # Partial credit
                    
            elif metric in lower_is_better_metrics:
                # For lower is better, invert the ratio
                if primary_val == 0:
                    skipped_metrics.add(metric)
                    continue
                ratio = comparison_val / primary_val
                ratio = min(max(ratio, 0), 2)  # Cap ratio between 0 and 2
                
                # Full credit if primary <= comparison, partial otherwise
                if primary_val <= comparison_val:
                    metric_score = weight * ratio
                else:
                    metric_score = weight * ratio * 0.5  # Partial credit
            else:
                # If metric type is unknown, skip it
                skipped_metrics.add(metric)
                continue
            
            score += metric_score
            total_weight += weight
        
        except (ValueError, AttributeError):
            # Skip metric if conversion fails
            skipped_metrics.add(metric)
            continue

    # Update global skipped metrics
    if global_skipped is not None:
        global_skipped.update(skipped_metrics)

    # Calculate final score
    if total_weight > 0 and np.isfinite(score):
        final_score = (score / total_weight) * 100
    else:
        final_score = 0.0

    # Ensure the final score is within 0 to 100
    final_score = min(max(final_score, 0), 100)

    return final_score

def get_drawdown_details(cum_returns):
    """
    Calculate detailed drawdown information for the portfolio.

    Parameters:
    - cum_returns (pd.Series): Cumulative returns of the portfolio.

    Returns:
    - list: Detailed drawdown information.
    """
    drawdowns = []
    if cum_returns.empty:
        return drawdowns
    peak = cum_returns.iloc[0]
    peak_date = cum_returns.index[0]
    trough = cum_returns.iloc[0]
    trough_date = cum_returns.index[0]
    for date, value in cum_returns.items():
        if value > peak:
            if trough < peak:
                recovery = cum_returns[cum_returns >= peak].loc[trough_date:]
                if not recovery.empty:
                    recovery_date = recovery.index[0]
                    if recovery_date > trough_date:
                        recovery_time = (recovery_date - trough_date).days
                        underwater_period = (recovery_date - peak_date).days
                        drawdowns.append({
                            'Start': peak_date.strftime('%b %Y'),
                            'End': trough_date.strftime('%b %Y'),
                            'Length': f"{(trough_date - peak_date).days // 30} months",
                            'Recovery By': recovery_date.strftime('%b %Y'),
                            'Recovery Time': f"{recovery_time // 30} months",
                            'Underwater Period': f"{underwater_period // 30} months",
                            'Drawdown': f"{((trough / peak) - 1) * 100:.2f}%"
                        })
                    else:
                        drawdowns.append({
                            'Start': peak_date.strftime('%b %Y'),
                            'End': trough_date.strftime('%b %Y'),
                            'Length': f"{(trough_date - peak_date).days // 30} months",
                            'Recovery By': "Not Recovered",
                            'Recovery Time': "N/A",
                            'Underwater Period': "N/A",
                            'Drawdown': f"{((trough / peak) - 1) * 100:.2f}%"
                        })
            peak = value
            peak_date = date
            trough = value
            trough_date = date
        elif value < trough:
            trough = value
            trough_date = date
    if trough < peak:
        drawdowns.append({
            'Start': peak_date.strftime('%b %Y'),
            'End': trough_date.strftime('%b %Y'),
            'Length': f"{(trough_date - peak_date).days // 30} months",
            'Recovery By': "Not Recovered",
            'Recovery Time': "N/A",
            'Underwater Period': "N/A",
            'Drawdown': f"{((trough / peak) - 1) * 100:.2f}%"
        })
    try:
        drawdowns_sorted = sorted(drawdowns, key=lambda x: float(x['Drawdown'].strip('%')), reverse=False)
    except:
        drawdowns_sorted = []
    drawdowns_sorted = drawdowns_sorted[:10]
    for idx, dd in enumerate(drawdowns_sorted, start=1):
        dd['Rank'] = idx
    drawdowns_final = []
    for dd in drawdowns_sorted:
        drawdowns_final.append({
            'Rank': dd['Rank'],
            'Start': dd['Start'],
            'End': dd['End'],
            'Length': dd['Length'],
            'Recovery By': dd['Recovery By'],
            'Recovery Time': dd['Recovery Time'],
            'Underwater Period': dd['Underwater Period'],
            'Drawdown': dd['Drawdown']
        })
    return drawdowns_final

# components/run_backtest.py

import streamlit as st
import pandas as pd
import numpy as np
import plotly.express as px

# Importing all necessary functions from utils, metrics, and plots
from utils import (
    download_data, 
    generate_recommendations, 
    backtest,  
    fetch_fama_french_factors, 
)

from metrics import (
    calculate_portfolio_metrics, 
    calculate_final_score, 
    calculate_risk_factor_attribution, 
    get_drawdown_details
)

from plots import (
    plot_growth_comparison, 
    plot_drawdown_comparison, 
    plot_cagr_over_time, 
    plot_allocation_pie, 
    plot_correlation_heatmap, 
    plot_risk_return_attribution, 
    plot_rolling_metrics,
    plot_correlation_heatmap_comparison,
    plot_cagr_comparison,
    plot_allocation_comparison,
    plot_rolling_metrics_comparison,
    plot_risk_return_attribution_comparison,
    plot_efficient_frontier_comparison
)

def format_value(x):
    """
    Formats the input value to two decimal places if it's a numeric type.
    Otherwise, returns the value as is.
    """
    if isinstance(x, (int, float, np.number)):
        return f"{x:.2f}"
    else:
        return x

def run_backtest_ui():
    # Initialize portfolios in session state if not already initialized
    if 'portfolios' not in st.session_state:
        st.session_state.portfolios = []
    
    if st.session_state.portfolios:
        with st.form("compare_form"):
            st.subheader("🔍 Select Portfolios to Compare")
            comparison_type = st.selectbox(
                "Comparison Type",
                ["Portfolio vs Benchmark", "Portfolio vs Portfolio"]
            )
            portfolio_names = [p['name'] for p in st.session_state.portfolios]
            if comparison_type == "Portfolio vs Benchmark":
                portfolio1 = st.selectbox("Select Portfolio", portfolio_names)
                portfolio2 = None
            else:
                portfolio1 = st.selectbox("Select First Portfolio", portfolio_names, key="p1")
                portfolio2 = st.selectbox("Select Second Portfolio", portfolio_names, key="p2")
                if portfolio1 == portfolio2:
                    st.warning("Please select two different portfolios for comparison.")
            
            st.subheader("⚙️ Visualizations Settings")
            selected_time_frames = st.multiselect(
                "Select Time Frames for CAGR",
                options=['Weekly', 'Monthly', 'Quarterly', 'Annually'],
                default=['Annually'],
                key="selected_time_frames",
                help="Choose one or more time frames to view CAGR over different horizons."
            )
            selected_rolling_periods = st.multiselect(
                "Select Rolling Periods (Days)",
                options=[30, 90, 180, 252],
                default=[252],
                key="selected_rolling_periods",
                help="Choose one or more periods to view rolling metrics."
            )

            run = st.form_submit_button("Run Backtest")
        
        if run:
            with st.spinner("Running backtest..."):
                if comparison_type == "Portfolio vs Benchmark":
                    # Handle Portfolio vs Benchmark
                    try:
                        portfolio = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)
                    except StopIteration:
                        st.error("Selected portfolio not found.")
                        return

                    price_data = download_data(
                        portfolio['selected'],
                        portfolio['start_date'],
                        portfolio['end_date']
                    )
                    if price_data.empty:
                        st.error("No price data available for the selected portfolio.")
                    else:
                        available_selected = [ticker for ticker in portfolio['selected'] if ticker in price_data.columns]
                        missing_selected = list(set(portfolio['selected']) - set(available_selected))
                        if missing_selected:
                            st.warning(f"Excluded tickers with no data: {', '.join(missing_selected)}")
                        if not available_selected:
                            st.error(
                                "No selected tickers have available data for the chosen date range. "
                                "Please ensure that the ticker symbols are correct and data is available for the specified period."
                            )
                        else:
                            # Extract corresponding allocations
                            original_allocations = portfolio.get('allocations', [0.0]*len(portfolio.get('selected', [])))
                            adjusted_allocations = [original_allocations[i] for i, ticker in enumerate(portfolio.get('selected', [])) if ticker in available_selected]
                            
                            # Error Handling for Allocation-Axis
                            if len(adjusted_allocations) != len(available_selected):
                                st.error("Mismatch between allocations and available assets after filtering.")
                                return
                            
                            # Normalize allocations to sum to 1
                            weights = np.array(adjusted_allocations) / 100
                            total_alloc = weights.sum()
                            if not np.isclose(total_alloc, 1.0, atol=1e-4):
                                if total_alloc == 0:
                                    st.error("Total allocations sum to 0. Cannot adjust allocations.")
                                    return
                                st.warning("Allocations do not sum to 100%. Adjusting allocations proportionally.")
                                weights = weights / total_alloc
                            
                            # Final check to ensure weights match the number of assets
                            if len(weights) != len(available_selected):
                                st.error("Final allocation weights do not match the number of available assets.")
                                return
                            
                            # Calculate per-asset returns
                            asset_returns = price_data[available_selected].pct_change().dropna()
                            
                            # Run backtest to get portfolio returns
                            returns, cum_returns = backtest(
                                weights, 
                                price_data[available_selected],
                                portfolio['rebalance_freq'], 
                                portfolio['broker_fee'], 
                                debug=False
                            )
                            
                            # Align asset returns with portfolio returns
                            common_index = returns.index.intersection(asset_returns.index)
                            asset_returns = asset_returns.loc[common_index]
                            returns = returns.loc[common_index]
                            cum_returns = cum_returns.loc[common_index]
                            
                            if cum_returns.empty:
                                st.error("Cumulative returns are empty. Check the data and allocations.")
                            else:
                                benchmark_symbol = portfolio['benchmark_symbol']
                                benchmark_data = download_data([benchmark_symbol], portfolio['start_date'], portfolio['end_date'])

                                if benchmark_symbol not in benchmark_data.columns:
                                    st.error(f"The selected benchmark symbol '{benchmark_symbol}' does not have available data for the chosen period.")
                                    st.stop()
                                else:
                                    st.success(f"Benchmark '{benchmark_symbol}' data successfully downloaded and will be used for comparison.")
                                    if not benchmark_data.empty:
                                        benchmark_returns = benchmark_data[benchmark_symbol].pct_change().dropna()
                                        benchmark_returns = benchmark_returns.reindex(returns.index, method='ffill').dropna()
                                        common_index_benchmark = returns.index.intersection(benchmark_returns.index)
                                        if common_index_benchmark.empty:
                                            st.error("No overlapping dates between portfolio returns and benchmark returns after alignment.")
                                            st.stop()
                                        returns = returns.loc[common_index_benchmark]
                                        cum_returns = cum_returns.loc[common_index_benchmark]
                                        asset_returns = asset_returns.loc[common_index_benchmark]
                                        benchmark_returns = benchmark_returns.loc[common_index_benchmark]
                                        benchmark_cum_returns = (1 + benchmark_returns).cumprod()
                                        benchmark_metrics = calculate_portfolio_metrics(
                                            benchmark_returns, benchmark_cum_returns, portfolio['rf_rate'], None
                                        )
                                    else:
                                        benchmark_returns = None
                                        benchmark_cum_returns = pd.Series(dtype=float)  # Ensure it's defined
                                        benchmark_metrics = {
                                            'Start Balance': "N/A",
                                            'End Balance': "N/A",
                                            'Annualized Return (CAGR)': "N/A",
                                            'Best Year': "N/A",
                                            'Worst Year': "N/A",
                                            'Arithmetic Mean (Monthly)': "N/A",
                                            'Arithmetic Mean (Annualized)': "N/A",
                                            'Geometric Mean (Monthly)': "N/A",
                                            'Geometric Mean (Annualized)': "N/A",
                                            'Standard Deviation (Monthly)': "N/A",
                                            'Standard Deviation (Annualized)': "N/A",
                                            'Downside Deviation (Monthly)': "N/A",
                                            'Maximum Drawdown': "N/A",
                                            'Sharpe Ratio': "N/A",
                                            'Sortino Ratio': "N/A",
                                            'Gain/Loss Ratio': "N/A",
                                            'Skewness': "N/A",
                                            'Excess Kurtosis': "N/A",
                                            'Safe Withdrawal Rate': "N/A",
                                            'Perpetual Withdrawal Rate': "N/A",
                                            'Positive Periods': "N/A",
                                            'Benchmark Correlation': np.nan,
                                            'Beta': np.nan,
                                            'R2': np.nan
                                        }

                                portfolio_metrics = calculate_portfolio_metrics(
                                    returns, cum_returns, portfolio['rf_rate'], benchmark_returns
                                )

                                # Dashboard Header with Key Stats
                                with st.container():
                                    st.markdown("### 🔑 Key Metrics")
                                    key_metrics = {
                                        'Annualized Return (CAGR)': portfolio_metrics.get('Annualized Return (CAGR)', "N/A"),
                                        'Sharpe Ratio': portfolio_metrics.get('Sharpe Ratio', "N/A"),
                                        'Maximum Drawdown': portfolio_metrics.get('Maximum Drawdown', "N/A")
                                    }
                                    cols = st.columns(len(key_metrics))
                                    for col, (metric, value) in zip(cols, key_metrics.items()):
                                        with col:
                                            st.metric(label=metric, value=value)

                                # Create Tabs for Organized Sections
                                tabs = st.tabs(["Overview", "Performance Statistics", "Advanced Metrics", "Drawdowns", "Visualizations"])

                                with tabs[0]:
                                    st.header("📈 Portfolio Performance Overview")
                                    st.write(f"**Portfolio Name:** {portfolio['name']}")
                                    st.write(f"**Start Date:** {portfolio['start_date'].strftime('%Y-%m-%d')}")
                                    st.write(f"**End Date:** {portfolio['end_date'].strftime('%Y-%m-%d')}")
                                    st.write(f"**Benchmark:** {portfolio['benchmark_symbol']}")

                                with tabs[1]:
                                    st.header("📊 Performance Statistics")
                                    performance_data_stats = {
                                                'Metric': [
                                                    'Start Balance',
                                                    'End Balance',
                                                    'Annualized Return (CAGR)',
                                                    'Standard Deviation (Annualized)',
                                                    'Best Year',
                                                    'Worst Year',
                                                    'Maximum Drawdown',
                                                    'Sharpe Ratio',
                                                    'Sortino Ratio',
                                                    'Benchmark Correlation'
                                                ],
                                                'Portfolio': [
                                                    portfolio_metrics['Start Balance'],
                                                    portfolio_metrics['End Balance'],
                                                    f"{portfolio_metrics['Annualized Return (CAGR)']:.2f}%" if not np.isnan(portfolio_metrics['Annualized Return (CAGR)']) else "N/A",
                                                    f"{portfolio_metrics['Standard Deviation (Annualized)']:.2f}%" if not np.isnan(portfolio_metrics['Standard Deviation (Annualized)']) else "N/A",
                                                    f"{portfolio_metrics['Best Year']:.2f}%" if not np.isnan(portfolio_metrics['Best Year']) else "N/A",
                                                    f"{portfolio_metrics['Worst Year']:.2f}%" if not np.isnan(portfolio_metrics['Worst Year']) else "N/A",
                                                    f"{portfolio_metrics['Maximum Drawdown']:.2f}%" if not np.isnan(portfolio_metrics['Maximum Drawdown']) else "N/A",
                                                    f"{portfolio_metrics['Sharpe Ratio']:.2f}" if not np.isnan(portfolio_metrics['Sharpe Ratio']) else "N/A",
                                                    f"{portfolio_metrics['Sortino Ratio']:.2f}" if not np.isnan(portfolio_metrics['Sortino Ratio']) else "N/A",
                                                    f"{portfolio_metrics['Benchmark Correlation']:.2f}" if not np.isnan(portfolio_metrics['Benchmark Correlation']) else "N/A"
                                                ],
                                                'Benchmark': [
                                                    benchmark_metrics.get('Start Balance', "N/A"),
                                                    benchmark_metrics.get('End Balance', "N/A"),
                                                    f"{benchmark_metrics.get('Annualized Return (CAGR)', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Annualized Return (CAGR)', np.nan)) else "N/A",
                                                    f"{benchmark_metrics.get('Standard Deviation (Annualized)', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Standard Deviation (Annualized)', np.nan)) else "N/A",
                                                    f"{benchmark_metrics.get('Best Year', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Best Year', np.nan)) else "N/A",
                                                    f"{benchmark_metrics.get('Worst Year', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Worst Year', np.nan)) else "N/A",
                                                    f"{benchmark_metrics.get('Maximum Drawdown', np.nan):.2f}%" if not pd.isna(benchmark_metrics.get('Maximum Drawdown', np.nan)) else "N/A",
                                                    f"{benchmark_metrics.get('Sharpe Ratio', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Sharpe Ratio', np.nan)) else "N/A",
                                                    f"{benchmark_metrics.get('Sortino Ratio', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Sortino Ratio', np.nan)) else "N/A",
                                                    f"{benchmark_metrics.get('Benchmark Correlation', np.nan):.2f}" if not pd.isna(benchmark_metrics.get('Benchmark Correlation', np.nan)) else "N/A"
                                                ]
                                            }
                                    performance_df_stats = pd.DataFrame(performance_data_stats).set_index('Metric')
                                    
                                    st.table(performance_df_stats)

                                with tabs[2]:
                                    st.header("📋 Detail Comparisons")
                                    with st.container():
                                        st.subheader("🧮 Advanced Metrics")
                                        performance_data_advanced = {
                                            'Metric': list(portfolio_metrics.keys()),
                                            'Portfolio': list(portfolio_metrics.values()),
                                            'Benchmark': list(benchmark_metrics.values())
                                        }
                                        performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')

                                        # Replace NaN with empty strings for better visualization
                                        performance_df_advanced = performance_df_advanced.replace(np.nan, "")
                                        # Drop rows where both Portfolio and Benchmark values are NaN
                                        performance_df_advanced_clean = performance_df_advanced.dropna(how='all')

                                        # Format metrics to two decimal places
                                        performance_df_advanced_clean = performance_df_advanced_clean.applymap(format_value)
                                        
                                        st.table(performance_df_advanced_clean)
                                        st.markdown("---")
                                        st.write("**Note:** Some metrics in the benchmark column are empty because they are portfolio-specific measurements that can't be calculated for the benchmark alone...")

                                    st.subheader("🔍 Risk Factor Attribution Analysis")
                                    # Define date range based on the overlapping index
                                    start_date = returns.index.min().strftime('%Y-%m-%d')
                                    end_date = returns.index.max().strftime('%Y-%m-%d')
                                    # Fetch actual factor data
                                    factors = fetch_fama_french_factors(start_date, end_date)
                                    # Ensure factors are fetched
                                    if factors.empty:
                                        st.write("Failed to retrieve factor data.")       
                                    else:
                                        # Align factors frequency with portfolios
                                        if isinstance(returns.index, pd.DatetimeIndex):
                                            if returns.index.freq is None:
                                                inferred_freq = pd.infer_freq(returns.index)
                                                if inferred_freq is None:
                                                    inferred_freq = 'D'  # Set default frequency to daily
                                            else:
                                                inferred_freq = returns.index.freq
                                            
                                            try:
                                                factors = factors.asfreq(inferred_freq, method='ffill')
                                            except TypeError as e:
                                                st.error(f"Error setting frequency for factors: {e}")
                                                st.stop()
                                        else:
                                            st.error("Returns index is not a DatetimeIndex.")
                                            st.stop()
                                        
                                        # Calculate Attribution for Portfolio
                                        attribution, error = calculate_risk_factor_attribution(returns, factors)
                                        
                                        if error:
                                            st.error("Risk factor attribution analysis is unavailable for the portfolio.")
                                        else:
                                            if attribution.empty:
                                                st.write("Risk factor attribution analysis is unavailable for the portfolio.")
                                            else:
                                                st.write(f"### Risk Factor Attribution for {portfolio['name']}")
                                                
                                                st.table(attribution.fillna(0))
                                                
                                                # Visualization
                                                fig_attribution = px.bar(
                                                    attribution,
                                                    x='Factor',
                                                    y='Contribution (%)',
                                                    title='Risk Factor Attribution',
                                                    template='plotly_dark'
                                                )
                                                st.plotly_chart(fig_attribution, use_container_width=True)

                                with tabs[3]:
                                    st.header("📉 Detailed Drawdowns")
                                    st.markdown("### 📈 Drawdowns for Portfolio")
                                    portfolio_drawdowns = get_drawdown_details(cum_returns)
                                    if portfolio_drawdowns:
                                        portfolio_drawdowns_df = pd.DataFrame(portfolio_drawdowns)
                                        st.table(portfolio_drawdowns_df)
                                    else:
                                        st.write("No drawdowns detected for the portfolio.")

                                    st.markdown("### 📈 Drawdowns for Benchmark")
                                    benchmark_drawdowns = get_drawdown_details(benchmark_cum_returns)
                                    if benchmark_drawdowns:
                                        benchmark_drawdowns_df = pd.DataFrame(benchmark_drawdowns)
                                        st.table(benchmark_drawdowns_df)
                                    else:
                                        st.write("No drawdowns detected for the benchmark.")

                                with tabs[4]:
                                    st.header("📊 Visualizations")
                                    if not cum_returns.empty and not benchmark_cum_returns.empty:
                                        plot_growth_comparison(cum_returns, benchmark_cum_returns, labels=["Portfolio", "Benchmark"])
                                    else:
                                        st.warning("Insufficient data to display Growth Comparison. Ensure both portfolio and benchmark have data.")
                                    
                                    portfolio_drawdown_series = (cum_returns / cum_returns.expanding().max() - 1) * 100
                                    benchmark_drawdown_series = (benchmark_cum_returns / benchmark_cum_returns.expanding().max() - 1) * 100
                                    plot_drawdown_comparison(portfolio_drawdown_series, benchmark_drawdown_series, labels=["Portfolio", "Benchmark"])

                                    st.subheader("📈 Compound Annual Growth Rate (CAGR) Over Time")
                                    if selected_time_frames:
                                        plot_cagr_over_time(cum_returns, time_frames=selected_time_frames, label="Portfolio")
                                    else:
                                        st.warning("Please select at least one time frame for CAGR.")

                                    # Box Plot for Returns Distribution
                                    st.subheader("📦 Returns Distribution Box Plot")
                                    if isinstance(asset_returns, pd.DataFrame):
                                        # Assuming the index is named 'Date'
                                        if asset_returns.index.name != 'Date':
                                            asset_returns = asset_returns.copy()
                                            asset_returns.index.name = 'Date'
                                        
                                        melted_returns = asset_returns.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
                                        fig_box_plot = px.box(
                                            melted_returns,
                                            x='Asset',
                                            y='Return',
                                            title='Returns Distribution Box Plot',
                                            labels={'Return': 'Returns', 'Asset': 'Asset'},
                                            points='all',
                                            hover_data=['Return'],
                                            template='plotly_dark',
                                            color='Asset',
                                            color_discrete_sequence=px.colors.qualitative.Set3
                                        )
                                        st.plotly_chart(fig_box_plot, use_container_width=True)
                                        st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in the portfolio. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                                    elif isinstance(asset_returns, pd.Series):
                                        # Assuming the index is named 'Date'
                                        if asset_returns.index.name != 'Date':
                                            asset_returns = asset_returns.copy()
                                            asset_returns.index.name = 'Date'
                                        
                                        melted_returns = asset_returns.reset_index().rename(columns={'Date': 'Date', 0: 'Return'})
                                        fig_box_plot = px.box(
                                            melted_returns,
                                            y='Return',
                                            title='Returns Distribution Box Plot',
                                            labels={'Return': 'Returns'},
                                            points='all',
                                            hover_data=['Return'],
                                            template='plotly_dark'
                                        )
                                        st.plotly_chart(fig_box_plot, use_container_width=True)
                                        st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for the portfolio. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                                    else:
                                        st.warning("Returns data is neither a DataFrame nor a Series.")

                                    # Heatmap of Correlations Between Assets
                                    st.subheader("🔥 Correlation Heatmap of Assets")
                                    plot_correlation_heatmap(asset_returns)

                                    # Add Risk-Return Attribution Analysis
                                    st.subheader("🔍 Risk-Return Attribution Analysis")
                                    plot_risk_return_attribution(returns, weights, label='Portfolio')

                                    # Cumulative Returns Heatmap
                                    st.subheader("🔥 Cumulative Returns Heatmap")
                                    try:
                                        cum_returns_normalized = cum_returns / cum_returns.expanding().max()
                                        fig_cum_heatmap = px.imshow(
                                            cum_returns_normalized.to_frame().T,
                                            labels=dict(x="Date", y="Portfolio", color="Normalized Cumulative Return"),
                                            title="Cumulative Returns Heatmap",
                                            aspect="auto",
                                            color_continuous_scale='Viridis',
                                            template='plotly_dark'
                                        )
                                        st.plotly_chart(fig_cum_heatmap, use_container_width=True)
                                        st.markdown("**Interpretation:** The heatmap visualizes the normalized cumulative returns over time, allowing for an intuitive comparison of portfolio performance across different periods.")
                                    except Exception as e:
                                        st.error(f"Error plotting cumulative returns heatmap: {e}")

                                    # Portfolio Allocation Pie Chart
                                    st.subheader("🥧 Portfolio Allocation Pie Chart")
                                    plot_allocation_pie(weights, available_selected, title="Portfolio Allocation", hover_info="percent+name")

                                    # Rolling Metrics Chart with User-Defined Periods
                                    st.subheader("📉 Rolling Metrics")
                                    if selected_rolling_periods:
                                        plot_rolling_metrics(returns, windows=selected_rolling_periods, label='Portfolio')
                                    else:
                                        st.warning("Please select at least one rolling period to display metrics.")

                                    # Recommendations
                                    st.markdown("### 💡 Recommendations")
                                    recommendations = generate_recommendations(
                                        allocations=[alloc for alloc in portfolio['allocations'] if alloc > 0],
                                        available_selected=[ticker for ticker in available_selected if portfolio['allocations'][available_selected.index(ticker)] > 0]
                                    )
                                    
                                    if recommendations:
                                        for rec in recommendations:
                                            st.write(rec)
                                    else:
                                        st.success("Your portfolio allocations are well-balanced!")
                                        
                                    # Final Score
                                    st.markdown("### 📝 Final Score")
                                    skipped_metrics_set = set()
                                    portfolio_score = calculate_final_score(portfolio_metrics, benchmark_metrics, global_skipped=skipped_metrics_set)
                                    benchmark_score = calculate_final_score(benchmark_metrics, portfolio_metrics, global_skipped=skipped_metrics_set)
                                    st.write(f"**Portfolio:** {portfolio_score:.2f} / 100")
                                    st.write(f"**Benchmark:** {benchmark_score:.2f} / 100")
                                    
                                    # Show skipped metrics once
                                    if skipped_metrics_set:
                                        st.warning(f"Skipped metrics due to invalid values: {', '.join(skipped_metrics_set)}")

                                    # Store results
                                    st.session_state.backtest_results = {
                                        'returns': returns,
                                        'cum_returns': cum_returns,
                                        'weights_a': weights,
                                        'price_data': price_data[available_selected],
                                        'metrics': portfolio_metrics,
                                        'benchmark_metrics': benchmark_metrics,
                                        'performance_df_stats': performance_df_stats,  # Corrected variable name
                                        'performance_df_advanced': performance_df_advanced_clean,  # Corrected to use the cleaned DataFrame
                                        'benchmark_cum_returns': benchmark_cum_returns
                                    }

                elif comparison_type == "Portfolio vs Portfolio" and portfolio1 and portfolio2 and portfolio1 != portfolio2:
                    # Handle Portfolio vs Portfolio
                    try:
                        portfolio_a = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)
                        portfolio_b = next(p for p in st.session_state.portfolios if p['name'] == portfolio2)
                    except StopIteration:
                        st.error("One or both selected portfolios were not found.")
                        return

                    # Initialize cum_returns_a and cum_returns_b to prevent UnboundLocalError
                    cum_returns_a = pd.Series(dtype=float)
                    cum_returns_b = pd.Series(dtype=float)

                    # Backtest Portfolio A
                    price_data_a = download_data(
                        portfolio_a['selected'],
                        portfolio_a['start_date'],
                        portfolio_a['end_date']
                    )
                    # Backtest Portfolio B
                    price_data_b = download_data(
                        portfolio_b['selected'],
                        portfolio_b['start_date'],
                        portfolio_b['end_date']
                    )

                    if price_data_a.empty or price_data_b.empty:
                        st.error("No price data available for one or both selected portfolios.")
                    else:
                        # Process Portfolio A
                        available_a = [ticker for ticker in portfolio_a['selected'] if ticker in price_data_a.columns]
                        missing_a = list(set(portfolio_a['selected']) - set(available_a))
                        if missing_a:
                            st.warning(f"Excluded tickers from {portfolio_a['name']} with no data: {', '.join(missing_a)}")
                        if not available_a:
                            st.error(f"No selected tickers have available data for the portfolio '{portfolio_a['name']}'.")
                            return
                        # Extract corresponding allocations for Portfolio A
                        original_allocations_a = portfolio_a.get('allocations', [0.0]*len(portfolio_a.get('selected', [])))
                        adjusted_allocations_a = [original_allocations_a[i] for i, ticker in enumerate(portfolio_a.get('selected', [])) if ticker in available_a]
                        
                        if len(adjusted_allocations_a) != len(available_a):
                            st.error(f"Mismatch between allocations and available assets after filtering for '{portfolio_a['name']}'.")
                            return
                        
                        # Normalize allocations to sum to 1
                        weights_a = np.array(adjusted_allocations_a) / 100
                        total_alloc_a = weights_a.sum()
                        if not np.isclose(total_alloc_a, 1.0, atol=1e-4):
                            if total_alloc_a == 0:
                                st.error(f"Total allocations for '{portfolio_a['name']}' sum to 0. Cannot adjust allocations.")
                                return
                            st.warning(f"Allocations for '{portfolio_a['name']}' do not sum to 100%. Adjusting allocations proportionally.")
                            weights_a = weights_a / total_alloc_a
                        
                        # Calculate per-asset returns for Portfolio A
                        asset_returns_a = price_data_a[available_a].pct_change().dropna()
                        
                        # Run backtest for Portfolio A
                        returns_a, cum_returns_a = backtest(
                            weights_a, 
                            price_data_a[available_a],
                            portfolio_a['rebalance_freq'], 
                            portfolio_a['broker_fee'], 
                            debug=False
                        )
                        
                        # Process Portfolio B
                        available_b = [ticker for ticker in portfolio_b['selected'] if ticker in price_data_b.columns]
                        missing_b = list(set(portfolio_b['selected']) - set(available_b))
                        if missing_b:
                            st.warning(f"Excluded tickers from {portfolio_b['name']} with no data: {', '.join(missing_b)}")
                        if not available_b:
                            st.error(f"No selected tickers have available data for the portfolio '{portfolio_b['name']}'.")
                            return
                        # Extract corresponding allocations for Portfolio B
                        original_allocations_b = portfolio_b.get('allocations', [0.0]*len(portfolio_b.get('selected', [])))
                        adjusted_allocations_b = [original_allocations_b[i] for i, ticker in enumerate(portfolio_b.get('selected', [])) if ticker in available_b]
                        
                        if len(adjusted_allocations_b) != len(available_b):
                            st.error(f"Mismatch between allocations and available assets after filtering for '{portfolio_b['name']}'.")
                            return
                        
                        # Normalize allocations to sum to 1
                        weights_b = np.array(adjusted_allocations_b) / 100
                        total_alloc_b = weights_b.sum()
                        if not np.isclose(total_alloc_b, 1.0, atol=1e-4):
                            if total_alloc_b == 0:
                                st.error(f"Total allocations for '{portfolio_b['name']}' sum to 0. Cannot adjust allocations.")
                                return
                            st.warning(f"Allocations for '{portfolio_b['name']}' do not sum to 100%. Adjusting allocations proportionally.")
                            weights_b = weights_b / total_alloc_b
                        
                        # Calculate per-asset returns for Portfolio B
                        asset_returns_b = price_data_b[available_b].pct_change().dropna()
                        
                        # Run backtest for Portfolio B
                        returns_b, cum_returns_b = backtest(
                            weights_b, 
                            price_data_b[available_b],
                            portfolio_b['rebalance_freq'], 
                            portfolio_b['broker_fee'], 
                            debug=False
                        )
                        
                        # Align the data for comparison
                        common_dates = returns_a.index.intersection(returns_b.index)
                        if common_dates.empty:
                            st.error("No overlapping dates between the two portfolios after alignment.")
                            return
                        cum_returns_a = cum_returns_a.loc[common_dates]
                        cum_returns_b = cum_returns_b.loc[common_dates]
                        returns_a = returns_a.loc[common_dates]
                        returns_b = returns_b.loc[common_dates]
                        asset_returns_a = asset_returns_a.loc[common_dates]
                        asset_returns_b = asset_returns_b.loc[common_dates]
                        
                        # Calculate Metrics
                        metrics_a = calculate_portfolio_metrics(
                            returns_a, cum_returns_a, portfolio_a['rf_rate'], None
                        )
                        metrics_b = calculate_portfolio_metrics(
                            returns_b, cum_returns_b, portfolio_b['rf_rate'], None
                        )
                        
                        # Dashboard Header with Key Stats
                        with st.container():
                            st.markdown("### 🔑 Key Metrics")
                            key_metrics = {
                                'Annualized Return (CAGR)': ('CAGR', metrics_a.get('Annualized Return (CAGR)', "N/A"), metrics_b.get('Annualized Return (CAGR)', "N/A")),
                                'Sharpe Ratio': ('Sharpe Ratio', metrics_a.get('Sharpe Ratio', "N/A"), metrics_b.get('Sharpe Ratio', "N/A")),
                                'Maximum Drawdown': ('Maximum Drawdown', metrics_a.get('Maximum Drawdown', "N/A"), metrics_b.get('Maximum Drawdown', "N/A"))
                            }
                            columns = st.columns(len(key_metrics))
                            for idx, (metric, (label, value_a, value_b)) in enumerate(key_metrics.items()):
                                with columns[idx]:
                                    st.metric(label=f"{label} ({portfolio_a['name']})", value=value_a)
                                    st.metric(label=f"{label} ({portfolio_b['name']})", value=value_b)

                        # Create Tabs for Organized Sections
                        tabs = st.tabs(["Overview", "Performance Statistics", "Advanced Metrics", "Drawdowns", "Visualizations"])

                        with tabs[0]:
                            st.header("📈 Portfolios Performance Overview")
                            # Determine the aligned start and end dates
                            aligned_start_date = common_dates.min().strftime('%Y-%m-%d')
                            aligned_end_date = common_dates.max().strftime('%Y-%m-%d')
                            
                            st.write(f"**Aligned Date Range:** {aligned_start_date} to {aligned_end_date}")
                            st.write(f"**Portfolio A:** {portfolio_a['name']}")
                            st.write(f"**Original Start Date:** {portfolio_a['start_date'].strftime('%Y-%m-%d')}")
                            st.write(f"**Portfolio B:** {portfolio_b['name']}")
                            st.write(f"**Original Start Date:** {portfolio_b['start_date'].strftime('%Y-%m-%d')}")

                        with tabs[1]:
                            st.header("📊 Performance Statistics")
                            
                            with st.container():
                                st.subheader("🛠️ Basic Metrics")
                                performance_data_stats = {
                                    'Metric': [
                                        'Start Balance',
                                        'End Balance',
                                        'Annualized Return (CAGR)',
                                        'Standard Deviation (Annualized)',
                                        'Best Year',
                                        'Worst Year',
                                        'Maximum Drawdown',
                                        'Sharpe Ratio',
                                        'Sortino Ratio',
                                        'Benchmark Correlation'
                                    ],
                                    f'{portfolio_a["name"]}': [
                                        metrics_a['Start Balance'],
                                        metrics_a['End Balance'],
                                        metrics_a['Annualized Return (CAGR)'],
                                        metrics_a['Standard Deviation (Annualized)'],
                                        metrics_a['Best Year'],
                                        metrics_a['Worst Year'],
                                        metrics_a['Maximum Drawdown'],
                                        metrics_a['Sharpe Ratio'],
                                        metrics_a['Sortino Ratio'],
                                        metrics_a['Benchmark Correlation']
                                    ],
                                    f'{portfolio_b["name"]}': [
                                        metrics_b['Start Balance'],
                                        metrics_b['End Balance'],
                                        metrics_b['Annualized Return (CAGR)'],
                                        metrics_b['Standard Deviation (Annualized)'],
                                        metrics_b['Best Year'],
                                        metrics_b['Worst Year'],
                                        metrics_b['Maximum Drawdown'],
                                        metrics_b['Sharpe Ratio'],
                                        metrics_b['Sortino Ratio'],
                                        metrics_b['Benchmark Correlation']
                                    ]
                                }
                                performance_df_stats = pd.DataFrame(performance_data_stats).set_index('Metric')
                                st.table(performance_df_stats)

                        with tabs[2]:
                            st.header("📋 Detail Comparisons")
                            with st.container():
                                st.subheader("🧮 Advanced Metrics")
                                performance_data_advanced = {
                                    'Metric': list(metrics_a.keys()),
                                    f'{portfolio_a["name"]}': list(metrics_a.values()),
                                    f'{portfolio_b["name"]}': list(metrics_b.values())
                                }
                                performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')

                                # Replace NaN with empty strings for better visualization
                                performance_df_advanced = performance_df_advanced.replace(np.nan, "")
                                # Drop rows where both Portfolio A and Portfolio B values are NaN
                                performance_df_advanced_clean = performance_df_advanced.dropna(how='all', subset=[f'{portfolio_a["name"]}', f'{portfolio_b["name"]}'])

                                st.table(performance_df_advanced_clean)
                                st.markdown("---")
                                st.write("**Note:** Some metrics may be specific to certain portfolios or may not be directly comparable. Ensure to interpret the metrics in the context of each portfolio's strategy and objectives.")

                            st.subheader("🔍 Risk Factor Attribution Analysis")
                            
                            # Define date range based on the overlapping index
                            start_date = common_dates.min().strftime('%Y-%m-%d')
                            end_date = common_dates.max().strftime('%Y-%m-%d')
                            # Fetch actual factor data
                            factors = fetch_fama_french_factors(start_date, end_date)
                            # Ensure factors are fetched
                            if factors.empty:
                                st.write("Failed to retrieve factor data.")       
                            else:
                                # Align factors frequency with portfolios
                                if isinstance(returns_a.index, pd.DatetimeIndex):
                                    if returns_a.index.freq is None:
                                        inferred_freq = pd.infer_freq(returns_a.index)
                                        if inferred_freq is None:
                                            inferred_freq = 'D'  # Set default frequency to daily
                                    else:
                                        inferred_freq = returns_a.index.freq
                                    
                                    try:
                                        factors = factors.asfreq(inferred_freq, method='ffill')
                                    except TypeError as e:
                                        st.error(f"Error setting frequency for factors: {e}")
                                        st.stop()
                                else:
                                    st.error("Returns index is not a DatetimeIndex.")
                                    st.stop()
                                
                                # Calculate Attribution for Portfolio A
                                attribution_a, error_a = calculate_risk_factor_attribution(returns_a, factors)
                                
                                # Calculate Attribution for Portfolio B
                                attribution_b, error_b = calculate_risk_factor_attribution(returns_b, factors)
                                
                                if error_a or error_b:
                                    st.error("Risk factor attribution analysis is unavailable for one or both portfolios.")
                                else:
                                    if attribution_a.empty or attribution_b.empty:
                                        st.write("Risk factor attribution analysis is unavailable for one or both portfolios.")
                                    else:
                                        st.write(f"**Risk Factor Attribution for {portfolio_a['name']}:**")
                                        st.table(attribution_a)
                                        fig_attribution_a = px.bar(
                                            attribution_a,
                                            x='Factor',
                                            y='Contribution (%)',
                                            title=f'Risk Factor Attribution for {portfolio_a["name"]}',
                                            labels={'Contribution (%)': 'Contribution (%)'},
                                            template='plotly_dark'
                                        )
                                        st.plotly_chart(fig_attribution_a, use_container_width=True)

                                        st.write(f"**Risk Factor Attribution for {portfolio_b['name']}:**")
                                        st.table(attribution_b)
                                        fig_attribution_b = px.bar(
                                            attribution_b,
                                            x='Factor',
                                            y='Contribution (%)',
                                            title=f'Risk Factor Attribution for {portfolio_b["name"]}',
                                            labels={'Contribution (%)': 'Contribution (%)'},
                                            template='plotly_dark'
                                        )
                                        st.plotly_chart(fig_attribution_b, use_container_width=True)

                                        # Combined Bar Chart for Comparison
                                        combined_attribution = pd.concat([
                                            attribution_a.set_index('Factor')['Contribution (%)'],
                                            attribution_b.set_index('Factor')['Contribution (%)']
                                        ], axis=1)
                                        combined_attribution.columns = [portfolio_a['name'], portfolio_b['name']]
                                        combined_attribution = combined_attribution.fillna(0)

                                        st.markdown(f"### 📊 Risk Factor Attribution Comparison")
                                        fig_combined_attribution = px.bar(
                                            combined_attribution,
                                            x=combined_attribution.index,
                                            y=combined_attribution.columns,
                                            title='Risk Factor Attribution Comparison',
                                            labels={'value': 'Contribution (%)', 'Factor': 'Risk Factor'},
                                            template='plotly_dark',
                                            barmode='group'
                                        )
                                        st.plotly_chart(fig_combined_attribution, use_container_width=True)

                        with tabs[3]:
                            st.header("📉 Detailed Drawdowns")
                            st.markdown("### 📈 Drawdowns for Portfolio A")
                            portfolio_drawdowns = get_drawdown_details(cum_returns_a)
                            if portfolio_drawdowns:
                                portfolio_drawdowns_df = pd.DataFrame(portfolio_drawdowns)
                                st.table(portfolio_drawdowns_df)
                            else:
                                st.write("No drawdowns detected for Portfolio A.")

                            st.markdown("### 📈 Drawdowns for Portfolio B")
                            portfolio_b_drawdowns = get_drawdown_details(cum_returns_b)
                            if portfolio_b_drawdowns:
                                portfolio_b_drawdowns_df = pd.DataFrame(portfolio_b_drawdowns)
                                st.table(portfolio_b_drawdowns_df)
                            else:
                                st.write("No drawdowns detected for Portfolio B.")

                        with tabs[4]:
                            st.header("📊 Visualizations")
                            if not cum_returns_a.empty and not cum_returns_b.empty:
                                plot_growth_comparison(cum_returns_a, cum_returns_b, labels=[portfolio_a['name'], portfolio_b['name']])
                            else:
                                st.warning("Insufficient data to display Growth Comparison. Ensure both portfolios have data.")
                        
                            portfolio_a_drawdown_series = (cum_returns_a / cum_returns_a.expanding().max() - 1) * 100
                            portfolio_b_drawdown_series = (cum_returns_b / cum_returns_b.expanding().max() - 1) * 100
                            plot_drawdown_comparison(portfolio_a_drawdown_series, portfolio_b_drawdown_series, labels=[portfolio_a['name'], portfolio_b['name']])
                        
                            st.subheader("📈 Compound Annual Growth Rate (CAGR) Over Time")
                            if selected_time_frames:
                                plot_cagr_comparison(cum_returns_a, cum_returns_b, time_frames=selected_time_frames)
                            else:
                                st.warning("Please select at least one time frame for CAGR.")

                            # Box Plot for Returns Distribution
                            st.subheader("📦 Returns Distribution Box Plot")
                            # For portfolio A
                            if isinstance(asset_returns_a, pd.DataFrame):
                                # Ensure the index is named 'Date'
                                if asset_returns_a.index.name != 'Date':
                                    asset_returns_a = asset_returns_a.copy()
                                    asset_returns_a.index.name = 'Date'
                                
                                melted_returns_a = asset_returns_a.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
                                fig_box_plot_a = px.box(
                                    melted_returns_a,
                                    x='Asset',
                                    y='Return',
                                    title='Returns Distribution Box Plot - Portfolio A',
                                    labels={'Return': 'Returns', 'Asset': 'Asset'},
                                    points='all',
                                    hover_data=['Return'],
                                    template='plotly_dark',
                                    color='Asset',
                                    color_discrete_sequence=px.colors.qualitative.Set3
                                )
                                st.plotly_chart(fig_box_plot_a, use_container_width=True)
                                st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in Portfolio A. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                            elif isinstance(asset_returns_a, pd.Series):
                                # Ensure the index is named 'Date'
                                if asset_returns_a.index.name != 'Date':
                                    asset_returns_a = asset_returns_a.copy()
                                    asset_returns_a.index.name = 'Date'
                                
                                melted_returns_a = asset_returns_a.reset_index().rename(columns={'Date': 'Date', 'Return': 'Return'})
                                fig_box_plot_a = px.box(
                                    melted_returns_a,
                                    y='Return',
                                    title='Returns Distribution Box Plot - Portfolio A',
                                    labels={'Return': 'Returns'},
                                    points='all',
                                    hover_data=['Return'],
                                    template='plotly_dark'
                                )
                                st.plotly_chart(fig_box_plot_a, use_container_width=True)
                                st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for Portfolio A. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                            else:
                                st.warning("Returns data for Portfolio A is neither a DataFrame nor a Series.")

                            # For portfolio B
                            if isinstance(asset_returns_b, pd.DataFrame):
                                # Ensure the index is named 'Date'
                                if asset_returns_b.index.name != 'Date':
                                    asset_returns_b = asset_returns_b.copy()
                                    asset_returns_b.index.name = 'Date'
                                
                                melted_returns_b = asset_returns_b.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
                                fig_box_plot_b = px.box(
                                    melted_returns_b,
                                    x='Asset',
                                    y='Return',
                                    title='Returns Distribution Box Plot - Portfolio B',
                                    labels={'Return': 'Returns', 'Asset': 'Asset'},
                                    points='all',
                                    hover_data=['Return'],
                                    template='plotly_dark',
                                    color='Asset',
                                    color_discrete_sequence=px.colors.qualitative.Set3
                                )
                                st.plotly_chart(fig_box_plot_b, use_container_width=True)
                                st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in Portfolio B. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                            elif isinstance(asset_returns_b, pd.Series):
                                # Ensure the index is named 'Date'
                                if asset_returns_b.index.name != 'Date':
                                    asset_returns_b = asset_returns_b.copy()
                                    asset_returns_b.index.name = 'Date'
                                
                                melted_returns_b = asset_returns_b.reset_index().rename(columns={'Date': 'Date', 'Return': 'Return'})
                                fig_box_plot_b = px.box(
                                    melted_returns_b,
                                    y='Return',
                                    title='Returns Distribution Box Plot - Portfolio B',
                                    labels={'Return': 'Returns'},
                                    points='all',
                                    hover_data=['Return'],
                                    template='plotly_dark'
                                )
                                st.plotly_chart(fig_box_plot_b, use_container_width=True)
                                st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for Portfolio B. The box represents the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                            else:
                                st.warning("Returns data for Portfolio B is neither a DataFrame nor a Series.")

                            # Correlation Heatmap Comparison
                            st.subheader("🔥 Correlation Heatmap Comparison")
                            plot_correlation_heatmap_comparison(asset_returns_a, asset_returns_b)
                            
                            # Efficient Frontier Comparison
                            st.subheader("📈 Efficient Frontier Comparison")
                            plot_efficient_frontier_comparison(asset_returns_a, asset_returns_b, num_portfolios=1000, rf=portfolio_a['rf_rate'])

                            # Risk-Return Attribution Comparison
                            st.subheader("🔍 Risk-Return Attribution Comparison")
                            plot_risk_return_attribution_comparison(
                                returns_a, weights_a, 
                                returns_b, weights_b, 
                                label_a=portfolio_a['name'], 
                                label_b=portfolio_b['name']
                            )

                            # Recommendations
                            st.markdown("### 💡 Recommendations")
                            recommendations_a = generate_recommendations(
                                allocations=[alloc for alloc in portfolio_a['allocations'] if alloc > 0],
                                available_selected=[ticker for ticker in available_a if portfolio_a['allocations'][available_a.index(ticker)] > 0]
                            )
                            recommendations_b = generate_recommendations(
                                allocations=[alloc for alloc in portfolio_b['allocations'] if alloc > 0],
                                available_selected=[ticker for ticker in available_b if portfolio_b['allocations'][available_b.index(ticker)] > 0]
                            )
                            
                            if recommendations_a:
                                st.write(f"**{portfolio_a['name']} Recommendations:**")
                                for rec in recommendations_a:
                                    st.write(rec)
                            else:
                                st.success(f"{portfolio_a['name']} allocations are well-balanced!")
                            
                            if recommendations_b:
                                st.write(f"**{portfolio_b['name']} Recommendations:**")
                                for rec in recommendations_b:
                                    st.write(rec)
                            else:
                                st.success(f"{portfolio_b['name']} allocations are well-balanced!")
                                
                            # Final Score
                            st.markdown("### 📝 Final Score")
                            portfolio_score_a = calculate_final_score(metrics_a, metrics_b)
                            portfolio_score_b = calculate_final_score(metrics_b, metrics_a)
                            st.write(f"**{portfolio_a['name']}:** {portfolio_score_a:.2f} / 100")
                            st.write(f"**{portfolio_b['name']}:** {portfolio_score_b:.2f} / 100")
                            
                            # Store results
                            st.session_state.backtest_results = {
                                'returns_a': returns_a,
                                'cum_returns_a': cum_returns_a,
                                'weights_a': weights_a,
                                'price_data_a': price_data_a[available_a],
                                'metrics_a': metrics_a,
                                'returns_b': returns_b,
                                'cum_returns_b': cum_returns_b,
                                'weights_b': weights_b,
                                'price_data_b': price_data_b[available_b],
                                'metrics_b': metrics_b,
                                'performance_df_stats_a': performance_df_stats.loc[:, portfolio_a['name']],
                                'performance_df_advanced_a': performance_df_advanced_clean[[f'{portfolio_a["name"]}']],
                                'performance_df_stats_b': performance_df_stats.loc[:, portfolio_b['name']],
                                'performance_df_advanced_b': performance_df_advanced_clean[[f'{portfolio_b["name"]}']]
                            }

            st.success("Backtest completed successfully!")
    else:
        st.warning("Please add at least one portfolio to run backtest.")

fix it and return full code to me:
SyntaxError: File "/www/wwwroot/portfolio_optimizer/metrics.py", line 400 def calculate_risk_factor_attribution(returns, factors, annualization_factor=252): ^ SyntaxError: expected 'except' or 'finally' block
Traceback:
File "/www/server/pyporject_evn/portfolio_optimizer_venv/lib/python3.12/site-packages/streamlit/runtime/scriptrunner/exec_code.py", line 88, in exec_func_with_error_handling
    result = func()
             ^^^^^^
File "/www/server/pyporject_evn/portfolio_optimizer_venv/lib/python3.12/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 579, in code_to_exec
    exec(code, module.__dict__)
File "/www/wwwroot/portfolio_optimizer/pages/2_Run_Backtest.py", line 3, in <module>
    from components.run_backtest import run_backtest_ui
File "/www/wwwroot/portfolio_optimizer/components/run_backtest.py", line 16, in <module>
    from metrics import (