Welcome! Let's get you started with your first portfolio.

Configure your portfolio by entering a name, start date, end date, and other details.
Select the assets you want to include and set their allocations.
Run a backtest to see how your portfolio performs.
Compare your portfolio with a benchmark or another portfolio.
Explore the performance metrics and visualizations.
Got U, Let's Explore

hide the above after clicked the button Got U, Let's Explore

Please review my code and provide the original code alongside the updated one. Include the exact instructions for where and how to apply the corrections in the original code by referencing specific function names, variable names, or line numbers if possible. Dont give me general corrections as I am a newbie in coding, so make your response concise and simple, showing only the revised code snippets with a brief explanation for each change.

%%writefile portfolio_optimizer.py
import streamlit as st
import pandas as pd
import plotly.graph_objects as go
import numpy as np
import yfinance as yf
import statsmodels.api as sm
import datetime
from datetime import datetime, timedelta
from pandas_datareader import data as pdr
import plotly.express as px
import warnings
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from scipy.stats import skew, kurtosis
from joblib import Parallel, delayed
import json
from sklearn.linear_model import LinearRegression
from statsmodels.stats.stattools import durbin_watson
from urllib3.util.retry import Retry
from joblib import Parallel, delayed
from functools import lru_cache, wraps
from concurrent.futures import ProcessPoolExecutor
from scipy.optimize import minimize
from scipy.stats import norm, skew, kurtosis
from requests.packages.urllib3.exceptions import InsecureRequestWarning

warnings.filterwarnings('ignore')

# ----------------------------
# Frequency Mapping
# ----------------------------
frequency_mapping = {
    "Daily": "D",
    "Weekly": "W",
    "Monthly": "M",
    "Quarterly": "Q",
    "Yearly": "Y"
}

# Inverse mapping to get frequency name from code
rebalance_freq_inverse_mapping = {v: k for k, v in frequency_mapping.items()}
# ----------------------------
# Global Settings and Configurations
# ----------------------------

# Suppress only the InsecureRequestWarning, if bypassing SSL
warnings.simplefilter('ignore', InsecureRequestWarning)
requests.packages.urllib3.disable_warnings()  # Use cautiously
# ----------------------------
# Initialize Session State
# ----------------------------
default_session_state = {
    'portfolios': [],
    'backtest_results': {},
    'step': "Configure Portfolio",
    'edit_portfolio': None,
    'default_config': {
        'rf_rate': 0.02,
        'broker_fee': 0.0,
        'benchmark_symbol': '^GSPC'
    }
}
for key, value in default_session_state.items():
    st.session_state.setdefault(key, value)
# ----------------------------
# Utility Decorators
# ----------------------------

def handle_exceptions(func):
    """
    Decorator to handle exceptions in Streamlit applications.
    Displays errors using Streamlit's error message system.
    """
    @wraps(func)
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            st.error(f"Error in {func.__name__}: {e}")
            return None
    return wrapper

# ----------------------------
# Helper Functions
# ----------------------------

@handle_exceptions
def get_company_name(ticker_df: pd.DataFrame, ticker: str) -> str:
    """
    Retrieve the company name for a given ticker.

    Parameters:
    - ticker_df (pd.DataFrame): DataFrame containing 'Ticker' and 'Company Name' columns.
    - ticker (str): Stock ticker symbol.

    Returns:
    - str: Company name or "Unknown" if not found.
    """
    match = ticker_df[ticker_df['Ticker'] == ticker]
    if not match.empty:
        return match.iloc[0]['Company Name']
    return "Unknown"

def format_asset_option(ticker: str, company_name: str) -> str:
    """
    Format the asset option display string.

    Parameters:
    - ticker (str): Stock ticker symbol.
    - company_name (str): Name of the company.

    Returns:
    - str: Formatted string combining ticker and company name.
    """
    return f"{ticker} - {company_name}"

def run_portfolio_vs_benchmark(portfolio):
    """Runs backtest for Portfolio vs Benchmark comparison."""
    try:
        price_data = download_data(portfolio['selected'], portfolio['start_date'], portfolio['end_date'])
        if price_data.empty:
            return {"error": "No price data available for the selected portfolio."}

        available_selected = [ticker for ticker in portfolio['selected'] if ticker in price_data.columns]
        missing_selected = set(portfolio['selected']) - set(available_selected)
        if missing_selected:
            st.warning(f"Excluded tickers with no data: {', '.join(missing_selected)}")
        if not available_selected:
            return {"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."}

        allocations = [portfolio.get('allocations', [0.0]*len(portfolio.get('selected', [])))[i]
                      for i, ticker in enumerate(portfolio.get('selected', [])) if ticker in available_selected]
        allocations = adjust_allocations(allocations)

        returns, cum_returns = backtest(
            weights=np.array(allocations) / 100,
            prices=price_data[available_selected],  # Changed from 'price_data' to 'prices'
            rebalance_freq=portfolio['rebalance_freq'],
            broker_fee=portfolio['broker_fee'],
            debug=False
        )
        if cum_returns.empty:
            return {"error": "Cumulative returns are empty. Check the data and allocations."}

        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:
            return {"error": f"The selected benchmark symbol '{benchmark_symbol}' does not have available data for the chosen period."}
        if benchmark_data.empty:
            benchmark_returns, benchmark_cum_returns, benchmark_metrics = initialize_benchmark_metrics()
        else:
            benchmark_returns, benchmark_cum_returns, benchmark_metrics = process_benchmark(benchmark_data, returns, portfolio)

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

        backtest_results = {
            'returns': returns,
            'cum_returns': cum_returns,
            'weights': allocations,
            'price_data': price_data[available_selected],
            'metrics': portfolio_metrics,
            'benchmark_metrics': benchmark_metrics,
            'benchmark_cum_returns': benchmark_cum_returns
        }
        return {"backtest_results": backtest_results}
    except Exception as e:
        return {"error": f"An unexpected error occurred: {e}"}

def run_portfolio_vs_portfolio(portfolio_a, portfolio_b):
    """Runs backtest for Portfolio vs Portfolio comparison."""
    try:
        overlap_start = max(portfolio_a['start_date'], portfolio_b['start_date'])
        overlap_end = min(portfolio_a['end_date'], portfolio_b['end_date'])

        if overlap_start >= overlap_end:
            return {"error": "The selected portfolios do not have overlapping date ranges."}

        price_data_a = download_data(portfolio_a['selected'], overlap_start, overlap_end)
        price_data_b = download_data(portfolio_b['selected'], overlap_start, overlap_end)

        if price_data_a.empty:
            return {"error": f"No price data available for portfolio '{portfolio_a['name']}' in the overlapping period."}
        if price_data_b.empty:
            return {"error": f"No price data available for portfolio '{portfolio_b['name']}' in the overlapping period."}

        allocs_a, available_a, missing_a = process_portfolio_allocations(portfolio_a, price_data_a)
        allocs_b, available_b, missing_b = process_portfolio_allocations(portfolio_b, price_data_b)

        returns_a, cum_returns_a = backtest(
            weights=np.array(allocs_a) / 100,
            prices=price_data_a[available_a],  # Changed from 'price_data' to 'prices'
            rebalance_freq=portfolio_a['rebalance_freq'],
            broker_fee=portfolio_a['broker_fee'],
            debug=False
        )
        returns_b, cum_returns_b = backtest(
            weights=np.array(allocs_b) / 100,
            prices=price_data_b[available_b],  # Changed from 'price_data' to 'prices'
            rebalance_freq=portfolio_b['rebalance_freq'],
            broker_fee=portfolio_b['broker_fee'],
            debug=False
        )

        if cum_returns_a.empty or cum_returns_b.empty:
            return {"error": "One of the portfolios has empty cumulative returns. Check the data and allocations."}

        common_index = cum_returns_a.index.intersection(cum_returns_b.index)
        if common_index.empty:
            return {"error": "No overlapping dates between the two portfolios after backtesting."}

        returns_a = returns_a.loc[common_index]
        cum_returns_a = cum_returns_a.loc[common_index]
        returns_b = returns_b.loc[common_index]
        cum_returns_b = cum_returns_b.loc[common_index]

        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)

        backtest_results = {
            'returns_a': returns_a,
            'cum_returns_a': cum_returns_a,
            'weights_a': allocs_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': allocs_b,
            'price_data_b': price_data_b[available_b],
            'metrics_b': metrics_b,
            'benchmark_cum_returns': pd.Series(dtype=float)
        }
        return {"backtest_results": backtest_results}
    except Exception as e:
        return {"error": f"An unexpected error occurred: {e}"}

def adjust_allocations(allocations):
    """Adjust allocations to sum to 100% if necessary."""
    total = sum(allocations)
    if not np.isclose(total, 100.0, atol=1e-4):
        st.warning("Allocations do not sum to 100%. Adjusting allocations proportionally.")
        allocations = [a / total * 100 for a in allocations]
    return allocations

def process_portfolio_allocations(portfolio, price_data):
    """Processes portfolio allocations and handles missing tickers."""
    available = [ticker for ticker in portfolio['selected'] if ticker in price_data.columns]
    missing = set(portfolio['selected']) - set(available)
    if missing:
        st.warning(f"Excluded tickers from '{portfolio['name']}': {', '.join(missing)}")
    if not available:
        st.error(f"No valid tickers for portfolio '{portfolio['name']}' in the overlapping period.")
        st.stop()
    allocations = [portfolio['allocations'][i] for i, ticker in enumerate(portfolio['selected']) if ticker in available]
    allocations = adjust_allocations(allocations)
    return allocations, available, missing

def initialize_benchmark_metrics():
    """Initializes benchmark metrics when benchmark data is unavailable."""
    metrics = {metric: "N/A" for metric in [
        'Start Balance', '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', 'Benchmark Correlation', 'Beta',
        'Alpha (annualized)', 'R2', 'Treynor Ratio', 'Calmar Ratio',
        'Modigliani–Modigliani Measure', 'Information Ratio', 'Tracking Error',
        'Active Return', 'Upside Capture Ratio', 'Downside Capture Ratio'
    ]}
    return None, pd.Series(dtype=float), metrics

def process_benchmark(benchmark_data, returns, portfolio):
    """Processes benchmark data and calculates benchmark metrics."""
    benchmark_returns = benchmark_data[portfolio['benchmark_symbol']].pct_change().dropna()
    benchmark_returns = benchmark_returns.reindex(returns.index, method='ffill').dropna()
    common_index = returns.index.intersection(benchmark_returns.index)
    if common_index.empty:
        st.error("No overlapping dates between portfolio returns and benchmark returns after alignment.")
        st.stop()
    returns_aligned = returns.loc[common_index]
    cum_returns_aligned = (1 + benchmark_returns.loc[common_index]).cumprod()
    benchmark_metrics = calculate_portfolio_metrics(
        benchmark_returns, cum_returns_aligned, portfolio['rf_rate'], None
    )
    return benchmark_returns, cum_returns_aligned, benchmark_metrics

def display_backtest_results(results, comparison_type, portfolio1, portfolio2):
    """Displays the backtest results in the Streamlit app."""
    if comparison_type == "Portfolio vs Benchmark":
        display_portfolio_vs_benchmark(results)
    else:
        display_portfolio_vs_portfolio(results, portfolio1, portfolio2)

def display_portfolio_vs_benchmark(results):
    """Displays results for Portfolio vs Benchmark comparison."""
    returns = results['returns']
    cum_returns = results['cum_returns']
    weights = results['weights']
    price_data = results['price_data']
    metrics = results['metrics']
    benchmark_metrics = results['benchmark_metrics']
    benchmark_cum_returns = results['benchmark_cum_returns']

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

    # 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:** {portfolio1}")
        st.write(f"**Start Date:** {price_data.index.min().strftime('%Y-%m-%d')}")
        st.write(f"**End Date:** {price_data.index.max().strftime('%Y-%m-%d')}")
        st.write(f"**Benchmark:** {results.get('benchmark_symbol', 'N/A')}")

    with tabs[1]:
        st.header("📊 Performance Statistics")
        st.subheader("🛠️ Basic Metrics")
        performance_df_stats = create_performance_stats_df(metrics, benchmark_metrics)
        st.table(performance_df_stats)

    with tabs[2]:
        st.header("📋 Detail Comparisons")
        st.subheader("🧮 Advanced Metrics")
        performance_df_advanced = create_advanced_metrics_df(metrics, benchmark_metrics)
        st.table(performance_df_advanced)

        st.subheader("🔍 Risk Factor Attribution Analysis")
        perform_risk_factor_attribution(returns)

    with tabs[3]:
        st.header("📉 Detailed Drawdowns")
        display_drawdowns(cum_returns, benchmark_cum_returns)

    with tabs[4]:
        st.header("📊 Visualizations")
        display_visualizations(cum_returns, benchmark_cum_returns, returns, price_data, weights, metrics, benchmark_metrics)

    # Store results
    st.session_state.backtest_results = results
    st.success("Backtest completed!")

def display_portfolio_vs_portfolio(results, portfolio1, portfolio2):
    """Displays results for Portfolio vs Portfolio comparison."""
    returns_a = results['returns_a']
    cum_returns_a = results['cum_returns_a']
    weights_a = results['weights_a']
    price_data_a = results['price_data_a']
    metrics_a = results['metrics_a']

    returns_b = results['returns_b']
    cum_returns_b = results['cum_returns_b']
    weights_b = results['weights_b']
    price_data_b = results['price_data_b']
    metrics_b = results['metrics_b']

    # Key Metrics
    st.markdown("### 🔑 Key Metrics")
    key_metrics_a = {
        'Annualized Return (CAGR)': metrics_a.get('Annualized Return (CAGR)', "N/A"),
        'Sharpe Ratio': metrics_a.get('Sharpe Ratio', "N/A"),
        'Maximum Drawdown': metrics_a.get('Maximum Drawdown', "N/A")
    }
    key_metrics_b = {
        'Annualized Return (CAGR)': metrics_b.get('Annualized Return (CAGR)', "N/A"),
        'Sharpe Ratio': metrics_b.get('Sharpe Ratio', "N/A"),
        'Maximum Drawdown': metrics_b.get('Maximum Drawdown', "N/A")
    }
    cols = st.columns(len(key_metrics_a))
    for col, (metric, value) in zip(cols, key_metrics_a.items()):
        col.metric(label=f"{portfolio1} {metric}", value=value)
    for col, (metric, value) in zip(cols, key_metrics_b.items()):
        col.metric(label=f"{portfolio2} {metric}", value=value)

    # 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 A Name:** {portfolio1}")
        st.write(f"**Portfolio B Name:** {portfolio2}")
        st.write(f"**Start Date:** {min(returns_a.index.min(), returns_b.index.min()).strftime('%Y-%m-%d')}")
        st.write(f"**End Date:** {max(returns_a.index.max(), returns_b.index.max()).strftime('%Y-%m-%d')}")

    with tabs[1]:
        st.header("📊 Performance Statistics")
        st.subheader("🛠️ Basic Metrics")
        performance_df_stats = create_performance_stats_df_portfolio_vs_portfolio(metrics_a, metrics_b, portfolio1, portfolio2)
        st.table(performance_df_stats)

    with tabs[2]:
        st.header("📉 Advanced Metrics")
        performance_df_advanced = create_advanced_metrics_df_portfolio_vs_portfolio(metrics_a, metrics_b, portfolio1, portfolio2)
        st.table(performance_df_advanced)

    with tabs[3]:
        st.header("📉 Drawdowns")
        st.markdown("### 📉 Detailed Drawdowns")
        st.markdown(f"#### 📉 Drawdowns for {portfolio1}")
        drawdowns_a = get_drawdown_details(cum_returns_a)
        if drawdowns_a:
            st.table(pd.DataFrame(drawdowns_a))
        else:
            st.write("No drawdowns detected.")

        st.markdown(f"#### 📉 Drawdowns for {portfolio2}")
        drawdowns_b = get_drawdown_details(cum_returns_b)
        if drawdowns_b:
            st.table(pd.DataFrame(drawdowns_b))
        else:
            st.write("No drawdowns detected.")

    with tabs[4]:
        st.header("📊 Visualizations")
        display_visualizations_portfolio_vs_portfolio(cum_returns_a, cum_returns_b, returns_a, returns_b, price_data_a, price_data_b, weights_a, weights_b, portfolio1, portfolio2)

    # Store results
    st.session_state.backtest_results = results
    st.success("Backtest completed!")

def create_performance_stats_df(metrics, benchmark_metrics):
    """Creates a DataFrame for performance statistics."""
    return pd.DataFrame({
        'Metric': [
            'Start Balance', 'End Balance', 'Annualized Return (CAGR)',
            'Standard Deviation (Annualized)', 'Best Year', 'Worst Year',
            'Maximum Drawdown', 'Sharpe Ratio', 'Sortino Ratio', 'Benchmark Correlation'
        ],
        'Portfolio': [
            metrics.get('Start Balance', "N/A"),
            metrics.get('End Balance', "N/A"),
            metrics.get('Annualized Return (CAGR)', "N/A"),
            metrics.get('Standard Deviation (Annualized)', "N/A"),
            metrics.get('Best Year', "N/A"),
            metrics.get('Worst Year', "N/A"),
            metrics.get('Maximum Drawdown', "N/A"),
            metrics.get('Sharpe Ratio', "N/A"),
            metrics.get('Sortino Ratio', "N/A"),
            metrics.get('Benchmark Correlation', "N/A")
        ],
        'Benchmark': [
            benchmark_metrics.get('Start Balance', "N/A"),
            benchmark_metrics.get('End Balance', "N/A"),
            benchmark_metrics.get('Annualized Return (CAGR)', "N/A"),
            benchmark_metrics.get('Standard Deviation (Annualized)', "N/A"),
            benchmark_metrics.get('Best Year', "N/A"),
            benchmark_metrics.get('Worst Year', "N/A"),
            benchmark_metrics.get('Maximum Drawdown', "N/A"),
            benchmark_metrics.get('Sharpe Ratio', "N/A"),
            benchmark_metrics.get('Sortino Ratio', "N/A"),
            benchmark_metrics.get('Benchmark Correlation', "N/A")
        ]
    }).set_index('Metric')

def create_advanced_metrics_df(metrics, benchmark_metrics):
    """Creates a DataFrame for advanced metrics."""
    return pd.DataFrame({
        'Metric': list(metrics.keys()),
        'Portfolio': list(metrics.values()),
        'Benchmark': list(benchmark_metrics.values())
    }).set_index('Metric')

def create_performance_stats_df_portfolio_vs_portfolio(metrics_a, metrics_b, portfolio1, portfolio2):
    """Creates a DataFrame for Portfolio vs Portfolio performance statistics."""
    return pd.DataFrame({
        'Metric': [
            'Start Balance', 'End Balance', 'Annualized Return (CAGR)',
            'Standard Deviation (Annualized)', 'Best Year', 'Worst Year',
            'Maximum Drawdown', 'Sharpe Ratio', 'Sortino Ratio', 'Benchmark Correlation'
        ],
        portfolio1: [
            metrics_a.get('Start Balance', "N/A"),
            metrics_a.get('End Balance', "N/A"),
            metrics_a.get('Annualized Return (CAGR)', "N/A"),
            metrics_a.get('Standard Deviation (Annualized)', "N/A"),
            metrics_a.get('Best Year', "N/A"),
            metrics_a.get('Worst Year', "N/A"),
            metrics_a.get('Maximum Drawdown', "N/A"),
            metrics_a.get('Sharpe Ratio', "N/A"),
            metrics_a.get('Sortino Ratio', "N/A"),
            metrics_a.get('Benchmark Correlation', "N/A")
        ],
        portfolio2: [
            metrics_b.get('Start Balance', "N/A"),
            metrics_b.get('End Balance', "N/A"),
            metrics_b.get('Annualized Return (CAGR)', "N/A"),
            metrics_b.get('Standard Deviation (Annualized)', "N/A"),
            metrics_b.get('Best Year', "N/A"),
            metrics_b.get('Worst Year', "N/A"),
            metrics_b.get('Maximum Drawdown', "N/A"),
            metrics_b.get('Sharpe Ratio', "N/A"),
            metrics_b.get('Sortino Ratio', "N/A"),
            metrics_b.get('Benchmark Correlation', "N/A")
        ]
    }).set_index('Metric')

def create_advanced_metrics_df_portfolio_vs_portfolio(metrics_a, metrics_b, portfolio1, portfolio2):
    """Creates a DataFrame for Portfolio vs Portfolio advanced metrics."""
    return pd.DataFrame({
        'Metric': list(metrics_a.keys()),
        portfolio1: list(metrics_a.values()),
        portfolio2: list(metrics_b.values())
    }).set_index('Metric')

def display_drawdowns(cum_returns, benchmark_cum_returns):
    """Displays drawdown details for portfolio and benchmark."""
    st.markdown("### 📈 Drawdowns for Portfolio")
    portfolio_drawdowns = get_drawdown_details(cum_returns)
    if portfolio_drawdowns:
        st.table(pd.DataFrame(portfolio_drawdowns))
    else:
        st.write("No drawdowns detected for the portfolio.")

    if not benchmark_cum_returns.empty:
        st.markdown("### 📈 Drawdowns for Benchmark")
        benchmark_drawdowns = get_drawdown_details(benchmark_cum_returns)
        if benchmark_drawdowns:
            st.table(pd.DataFrame(benchmark_drawdowns))
        else:
            st.write("No drawdowns detected for the benchmark.")
    else:
        st.write("Benchmark data not available for drawdown analysis.")

def perform_risk_factor_attribution(returns):
    """Performs and displays risk factor attribution analysis."""
    start_date = returns.index.min().strftime('%Y-%m-%d')
    end_date = returns.index.max().strftime('%Y-%m-%d')
    factors = fetch_fama_french_factors(start_date, end_date)
    if factors.empty:
        st.write("Failed to retrieve factor data.")
        return
    factors = factors.asfreq(returns.index.freq, method='ffill')
    attribution, error = calculate_risk_factor_attribution(returns, factors)

    if error:
        st.error(error)
        st.write("Risk factor attribution analysis is unavailable.")
    elif attribution.empty:
        st.write("Risk factor attribution analysis is unavailable.")
    else:
        st.write("Risk Factor Attribution:")
        st.table(attribution)
        fig_attribution = px.bar(
            attribution,
            x='Factor',
            y='Contribution (%)',
            title='Risk Factor Attribution',
            labels={'Contribution (%)': 'Contribution (%)'},
            template='plotly_dark'
        )
        st.plotly_chart(fig_attribution, use_container_width=True)

def display_visualizations(cum_returns, benchmark_cum_returns, returns, price_data, weights, metrics, benchmark_metrics):
    """Displays various visualizations for Portfolio vs Benchmark."""
    if not cum_returns.empty and not benchmark_cum_returns.empty:
        plot_growth_comparison(cum_returns, benchmark_cum_returns)
    else:
        st.warning("Insufficient data to display Growth Comparison. Ensure both portfolio and benchmark have data.")

    plot_drawdown_comparison(
        (cum_returns / cum_returns.expanding().max() - 1) * 100,
        (benchmark_cum_returns / benchmark_cum_returns.expanding().max() - 1) * 100
    )

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

    st.subheader("📦 Returns Distribution Box Plot")
    plot_returns_box_plot(returns)

    st.subheader("🔥 Correlation Heatmap of Assets")
    plot_correlation_heatmap(price_data.pct_change().dropna())

    st.subheader("🔍 Risk-Return Attribution Analysis")
    plot_risk_return_attribution(returns, weights)

    st.subheader("🔥 Cumulative Returns Heatmap")
    plot_cumulative_returns_heatmap(cum_returns)

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

    st.subheader("📉 Rolling Metrics")
    if st.session_state.selected_rolling_periods:
        plot_rolling_metrics(returns, windows=st.session_state.selected_rolling_periods)
    else:
        st.warning("Please select at least one rolling period to display metrics.")

    st.markdown("### 💡 Recommendations")
    recommendations = generate_recommendations(
        allocations=[alloc for alloc in weights if alloc > 0],
        available_selected=price_data.columns
    )
    if recommendations:
        for rec in recommendations:
            st.write(rec)
    else:
        st.success("Your portfolio allocations are well-balanced!")

    st.markdown("### 📝 Final Score")
    portfolio_score = calculate_final_score(metrics, benchmark_metrics)
    st.write(f"**Portfolio:** {portfolio_score:.2f} / 100")

def display_visualizations_portfolio_vs_portfolio(cum_returns_a, cum_returns_b, returns_a, returns_b, price_data_a, price_data_b, weights_a, weights_b, portfolio1, portfolio2):
    """Displays various visualizations for Portfolio vs Portfolio."""
    # Growth Comparison
    fig_growth = px.line(title='Growth Comparison')
    fig_growth.add_scatter(x=cum_returns_a.index, y=cum_returns_a, mode='lines', name=portfolio1)
    fig_growth.add_scatter(x=cum_returns_b.index, y=cum_returns_b, mode='lines', name=portfolio2)
    st.plotly_chart(fig_growth, use_container_width=True)

    # Drawdown Comparison
    plot_drawdown_comparison(
        (cum_returns_a / cum_returns_a.expanding().max() - 1) * 100,
        (cum_returns_b / cum_returns_b.expanding().max() - 1) * 100
    )

    # Box Plot for Returns Distribution
    st.subheader("📦 Returns Distribution Box Plot")
    plot_returns_box_plot_portfolios(returns_a, returns_b, portfolio1, portfolio2)

    # Heatmap of Correlations Between Assets
    st.subheader("🔥 Correlation Heatmap of Assets")
    plot_correlation_heatmap(price_data_a.pct_change().dropna())
    plot_correlation_heatmap(price_data_b.pct_change().dropna())

    # Portfolio Allocation Pie Chart
    st.subheader("🥧 Portfolio Allocation Pie Chart")
    plot_allocation_pie(weights_a, price_data_a.columns, title=f"{portfolio1} Allocation", hover_info="percent+name")
    plot_allocation_pie(weights_b, price_data_b.columns, title=f"{portfolio2} Allocation", hover_info="percent+name")

    st.markdown("### 📝 Final Score")
    score_a = calculate_final_score(metrics_a, metrics_b)
    score_b = calculate_final_score(metrics_b, metrics_a)
    st.write(f"**{portfolio1}:** {score_a:.2f} / 100")
    st.write(f"**{portfolio2}:** {score_b:.2f} / 100")

def plot_returns_box_plot(returns):
    """Plots the returns distribution box plot."""
    if isinstance(returns, pd.DataFrame):
        melted_returns = returns.reset_index().melt(id_vars='Date', var_name='Asset', value_name='Return')
        fig_box = 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.Pastel
        )
    elif isinstance(returns, pd.Series):
        fig_box = px.box(
            returns.to_frame(name='Return'),
            y='Return',
            title='Returns Distribution Box Plot',
            labels={'Return': 'Returns'},
            points='all',
            hover_data=['Return'],
            template='plotly_dark',
            color_discrete_sequence=px.colors.qualitative.Pastel
        )
    else:
        st.warning("Returns data is neither a DataFrame nor a Series.")
        return
    st.plotly_chart(fig_box, 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."
    )

def plot_returns_box_plot_portfolios(returns_a, returns_b, portfolio1, portfolio2):
    """Plots the returns distribution box plot for two portfolios."""
    fig_box_a = px.box(
        returns_a.to_frame(name='Return'),
        y='Return',
        title=f'Returns Distribution Box Plot for {portfolio1}',
        labels={'Return': 'Returns'},
        points='all',
        hover_data=['Return'],
        template='plotly_dark',
        color_discrete_sequence=px.colors.qualitative.Pastel
    )
    fig_box_b = px.box(
        returns_b.to_frame(name='Return'),
        y='Return',
        title=f'Returns Distribution Box Plot for {portfolio2}',
        labels={'Return': 'Returns'},
        points='all',
        hover_data=['Return'],
        template='plotly_dark',
        color_discrete_sequence=px.colors.qualitative.Pastel
    )
    st.plotly_chart(fig_box_a, use_container_width=True)
    st.plotly_chart(fig_box_b, 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."
    )

def plot_cumulative_returns_heatmap(cum_returns):
    """Plots the cumulative returns heatmap."""
    try:
        cum_returns_normalized = cum_returns / cum_returns.max()
        fig_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_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}")

def plot_returns_correlation_heatmap(price_data):
    """Plots the correlation heatmap of asset returns."""
    correlation_data = price_data.pct_change().dropna()
    plot_correlation_heatmap(correlation_data)

def plot_correlation_heatmap(correlation_data):
    """Plots a correlation heatmap using Plotly."""
    if correlation_data.empty:
        st.warning("No data available to plot correlation heatmap.")
        return
    corr = correlation_data.corr()
    fig_heatmap = px.imshow(
        corr,
        title="Correlation Heatmap of Assets",
        labels=dict(x="Asset", y="Asset", color="Correlation"),
        color_continuous_scale='Viridis',
        template='plotly_dark'
    )
    st.plotly_chart(fig_heatmap, use_container_width=True)

def plot_growth_comparison(cum_returns, benchmark_cum_returns):
    """Plots the growth comparison between portfolio and benchmark."""
    fig_growth = px.line(title='Growth Comparison')
    fig_growth.add_scatter(x=cum_returns.index, y=cum_returns, mode='lines', name='Portfolio')
    fig_growth.add_scatter(x=benchmark_cum_returns.index, y=benchmark_cum_returns, mode='lines', name='Benchmark')
    st.plotly_chart(fig_growth, use_container_width=True)

def plot_drawdown_comparison(drawdown_a, drawdown_b):
    """Plots the drawdown comparison between two series."""
    fig_drawdown = px.line(title='Drawdown Comparison')
    fig_drawdown.add_scatter(x=drawdown_a.index, y=drawdown_a, mode='lines', name='Portfolio')
    if not drawdown_b.empty:
        fig_drawdown.add_scatter(x=drawdown_b.index, y=drawdown_b, mode='lines', name='Benchmark')
    st.plotly_chart(fig_drawdown, use_container_width=True)

def plot_cagr_over_time(cum_returns, time_frames):
    """Plots CAGR over different time frames."""
    for frame in time_frames:
        plot_cagr(cum_returns, frame)

def plot_allocation_pie(weights, assets, title, hover_info):
    """Plots the portfolio allocation pie chart."""
    fig_pie = px.pie(
        names=assets,
        values=weights,
        title=title,
        hover_data=[hover_info],
        template='plotly_dark'
    )
    st.plotly_chart(fig_pie, use_container_width=True)

def plot_risk_return_attribution(returns, weights):
    """Plots the risk-return attribution analysis."""
    fig_rra = px.scatter(
        x=returns.mean() * 252,
        y=returns.std() * np.sqrt(252),
        size=weights*100,
        color=returns.mean() / returns.std(),
        hover_name=returns.index,
        title='Risk-Return Attribution',
        labels={'x': 'Annualized Return', 'y': 'Annualized Risk (Std Dev)'},
        template='plotly_dark',
        size_max=60
    )
    st.plotly_chart(fig_rra, use_container_width=True)

def plot_rolling_metrics(returns, windows):
    """Plots rolling metrics based on selected window periods."""
    for window in windows:
        rolling_mean = returns.rolling(window=window).mean()
        rolling_std = returns.rolling(window=window).std()
        fig = px.line(title=f'Rolling {window}-Day Mean and Std Dev')
        fig.add_scatter(x=rolling_mean.index, y=rolling_mean, mode='lines', name='Rolling Mean')
        fig.add_scatter(x=rolling_std.index, y=rolling_std, mode='lines', name='Rolling Std Dev')
        st.plotly_chart(fig, use_container_width=True)

def plot_growth_comparison(cum_returns, benchmark_cum_returns):
    """Plots growth comparison between portfolio and benchmark."""
    fig_growth = px.line(title='Growth Comparison')
    fig_growth.add_scatter(x=cum_returns.index, y=cum_returns, mode='lines', name='Portfolio')
    fig_growth.add_scatter(x=benchmark_cum_returns.index, y=benchmark_cum_returns, mode='lines', name='Benchmark')
    st.plotly_chart(fig_growth, use_container_width=True)

def initialize_benchmark_metrics():
    """Initializes benchmark metrics when benchmark data is unavailable."""
    metrics = {metric: "N/A" for metric in [
        'Start Balance', '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', 'Benchmark Correlation', 'Beta',
        'Alpha (annualized)', 'R2', 'Treynor Ratio', 'Calmar Ratio',
        'Modigliani–Modigliani Measure', 'Information Ratio', 'Tracking Error',
        'Active Return', 'Upside Capture Ratio', 'Downside Capture Ratio'
    ]}
    return None, pd.Series(dtype=float), metrics

@handle_exceptions
def calculate_risk_factor_attribution(returns: pd.Series, factors: pd.DataFrame, annualization_factor: int = 252) -> tuple:
    """
    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.
    - annualization_factor (int): Annualization factor (252 for daily, 12 for monthly returns).

    Returns:
    - pd.DataFrame: Factor names, their contribution percentages, and explanatory comments.
    - None: If an exception occurs, handled by the decorator.
    """
    # Ensure returns has 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'].max() > 1:
        returns /= 100

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

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

    # 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['Portfolio_Returns']

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

    # Calculate factor contributions (non-annualized)
    factor_means = X.mean().drop('const', errors='ignore')
    contributions = coefficients * factor_means

    # Calculate total return
    total_return = returns['Portfolio_Returns'].mean()

    # Check if total_return is zero to avoid division by zero
    if np.isclose(total_return, 0):
        st.warning("Total portfolio return is zero. Cannot compute contribution percentages.")
        attribution_df = pd.DataFrame({
            'Factor': contributions.index,
            'Contribution (%)': [0.0] * len(contributions)
        })
    else:
        # Calculate percentage contributions
        attribution_df = pd.DataFrame({
            'Factor': contributions.index,
            'Contribution (%)': (contributions / total_return * 100).round(2)
        })

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

    # Use full names for factors
    factor_full_names = {
        'Mkt-RF': 'Market Risk Premium',
        'SMB': 'Small Minus Big',
        'HML': 'High Minus Low'
    }
    attribution_df['Factor'] = attribution_df['Factor'].replace(factor_full_names)

    # Generate detailed comments based on factor contributions
    comments = []
    for _, row in attribution_df.iterrows():
        factor_name = row['Factor']
        contribution = row['Contribution (%)']

        # Market Risk Premium (Mkt-RF) Analysis
        if factor_name == 'Market Risk Premium':
            if contribution > 75:
                comments.append(f"{factor_name} = {contribution}%: Extremely high sensitivity to market movements.")
            elif contribution > 50:
                comments.append(f"{factor_name} = {contribution}%: High sensitivity to overall market movements.")
            elif contribution > 30:
                comments.append(f"{factor_name} = {contribution}%: Notable sensitivity to market risk.")
            elif contribution > 10:
                comments.append(f"{factor_name} = {contribution}%: Moderate exposure to market risk.")
            else:
                comments.append(f"{factor_name} = {contribution}%: Low exposure to market risk.")

        # Small Minus Big (SMB) Analysis
        elif factor_name == 'Small Minus Big':
            if contribution > 5:
                comments.append(f"{factor_name} = {contribution}%: Strong tilt towards small-cap stocks.")
            elif contribution > 0:
                comments.append(f"{factor_name} = {contribution}%: Mild preference for small-cap stocks.")
            elif contribution > -5:
                comments.append(f"{factor_name} = {contribution}%: Slight tilt towards larger-cap stocks.")
            else:
                comments.append(f"{factor_name} = {contribution}%: Clear preference for large-cap stocks.")

        # High Minus Low (HML) Analysis
        elif factor_name == 'High Minus Low':
            if contribution > 5:
                comments.append(f"{factor_name} = {contribution}%: Strong tilt toward value stocks.")
            elif contribution > 0:
                comments.append(f"{factor_name} = {contribution}%: Slight positive tilt toward value stocks.")
            elif contribution > -5:
                comments.append(f"{factor_name} = {contribution}%: Minor negative tilt toward growth stocks.")
            else:
                comments.append(f"{factor_name} = {contribution}%: Clear preference for growth stocks.")

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

    return attribution_df, None

@handle_exceptions
def fetch_fama_french_factors(start_date: str, end_date: str) -> pd.DataFrame:
    """
    Fetch Fama-French daily factor data between specified dates.

    Parameters:
    - start_date (str): Start date in 'YYYY-MM-DD' format.
    - end_date (str): End date in 'YYYY-MM-DD' format.

    Returns:
    - pd.DataFrame: DataFrame containing Fama-French factors.
    """
    # [Function Implementation Remains Unchanged]
    try:
        # Fetch daily Fama-French factors
        ff_data = pdr.get_data_famafrench('F-F_Research_Data_Factors_daily', start=start_date, end=end_date)
        if not ff_data:
            st.error("Fama-French data not found.")
            return pd.DataFrame()

        factors = ff_data[0]
        factors = factors.rename(columns=lambda x: x.strip())  # Remove any leading/trailing spaces
        factors /= 100  # Convert percentage returns to decimal format

        # Fill missing data by forward and backward filling
        factors = factors.fillna(method='ffill').fillna(method='bfill')

        # Check for necessary columns
        required_columns = {'Mkt-RF', 'SMB', 'HML', 'RF'}
        if not required_columns.issubset(factors.columns):
            missing = required_columns - set(factors.columns)
            st.error(f"Missing factor columns: {', '.join(missing)}")
            return pd.DataFrame()

        return factors
    except Exception as e:
        st.error(f"Error fetching factor data: {e}")
        return pd.DataFrame()

@handle_exceptions
def calculate_final_score(primary_metrics: dict, comparison_metrics: dict) -> float:
    """
    Calculate the final portfolio score based on weighted metric comparisons.

    Parameters:
    - primary_metrics (dict): Metrics of the primary portfolio.
    - comparison_metrics (dict): Metrics of the comparison benchmark.

    Returns:
    - float: Final portfolio score scaled to 100.
    """
    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'
    ]
    metric_weights = {
        'Annualized Return (CAGR)': 15,
        'Sharpe Ratio': 15,
        'Sortino Ratio': 10,
        'Treynor Ratio': 10,
        'Calmar Ratio': 10,
        'Alpha (annualized)': 10,
        'Information Ratio': 10,
        'Modigliani–Modigliani Measure': 10,
        'Upside Capture Ratio': 5,
        'Gain/Loss Ratio': 5
    }

    score = 0
    total = 0

    for metric in metrics_to_compare:
        primary = primary_metrics.get(metric, "N/A")
        comparison = comparison_metrics.get(metric, "N/A")
        if primary != "N/A" and comparison != "N/A":
            try:
                primary_val = float(primary.strip('%')) if '%' in primary else float(primary)
                comparison_val = float(comparison.strip('%')) if '%' in comparison else float(comparison)
                weight = metric_weights.get(metric, 1)
                if comparison_val == 0:
                    st.warning(f"Comparison metric '{metric}' has a value of zero. Skipping this metric to avoid division by zero.")
                    continue
                if primary_val > comparison_val:
                    score += weight * (primary_val / comparison_val)
                else:
                    score += weight * (primary_val / comparison_val) * 0.5  # Partial credit
                total += weight
            except ValueError:
                st.warning(f"Invalid numeric conversion for metric '{metric}'. Skipping.")
                continue
    return (score / total) * 100 if total > 0 else 0

@handle_exceptions
def plot_growth_comparison(cum_returns: pd.Series, benchmark_cum_returns: pd.Series) -> None:
    """
    Plot cumulative returns comparison between portfolio and benchmark.

    Parameters:
    - cum_returns (pd.Series): Cumulative returns of the portfolio.
    - benchmark_cum_returns (pd.Series): Cumulative returns of the benchmark.
    """
    # Align the indices to ensure matching dates
    common_index = cum_returns.index.intersection(benchmark_cum_returns.index)
    cum_returns = cum_returns.loc[common_index]
    benchmark_cum_returns = benchmark_cum_returns.loc[common_index]

    if cum_returns.empty or benchmark_cum_returns.empty:
        st.warning("No overlapping data to plot growth comparison.")
        return

    df = pd.DataFrame({
        'Date': cum_returns.index,
        'Portfolio': cum_returns.values,
        'Benchmark': benchmark_cum_returns.values
    })

    fig = px.line(
        df,
        x='Date',
        y=['Portfolio', 'Benchmark'],
        title='Growth Comparison',
        labels={'value': 'Cumulative Returns', 'Date': 'Date'},
        hover_data={'Date': '|%B %d, %Y'},
        template='plotly_dark'
    )
    fig.update_layout(
        hovermode='x unified',
        xaxis=dict(rangeslider=dict(visible=True), type='date'),
    )
    fig.update_traces(line=dict(width=2))
    st.plotly_chart(fig, use_container_width=True, config={
        'scrollZoom': True
    })

@handle_exceptions
def plot_drawdown_comparison(drawdown_portfolio: pd.Series, drawdown_benchmark: pd.Series) -> None:
    """
    Plot drawdown comparison between portfolio and benchmark.

    Parameters:
    - drawdown_portfolio (pd.Series): Drawdown series of the portfolio.
    - drawdown_benchmark (pd.Series): Drawdown series of the benchmark.
    """
    df = pd.DataFrame({
        'Portfolio Drawdown': drawdown_portfolio,
        'Benchmark Drawdown': drawdown_benchmark
    })
    fig = px.line(df, title='Drawdown Comparison', labels={'value': 'Drawdown (%)', 'index': 'Date'}, template='plotly_dark')
    fig.update_layout(
        hovermode='x unified',
        xaxis=dict(rangeslider=dict(visible=True), type='date'),
    )
    fig.update_traces(line=dict(width=2))
    st.plotly_chart(fig, use_container_width=True, config={
        'scrollZoom': True})
    
@handle_exceptions
def plot_cagr_over_time(cum_returns: pd.Series, time_frames: list = ['Weekly', 'Monthly', 'Quarterly', 'Annually']) -> None:
    """
    Plot CAGR over multiple time frames.

    Parameters:
    - cum_returns (pd.Series): Cumulative returns of the portfolio.
    - time_frames (list): List of time frames to calculate CAGR.
    """
    frequency_map = {
        'Weekly': 'W',
        'Monthly': 'M',
        'Quarterly': 'Q',
        'Annually': 'Y'
    }
    fig = go.Figure()
    for tf in time_frames:
        freq = frequency_map.get(tf)
        if not freq:
            continue
        rolled = cum_returns.resample(freq).last()
        if rolled.empty:
            st.warning(f"No data available for {tf} CAGR calculation.")
            continue
        years = (rolled.index[-1] - rolled.index[0]).days / 365.25
        if years <= 0:
            st.warning(f"Not enough data to calculate {tf} CAGR.")
            continue
        cagr = (rolled / rolled.iloc[0]) ** (1/years) - 1
        fig.add_trace(go.Scatter(x=rolled.index, y=cagr, mode='lines', name=f'{tf} CAGR'))
    fig.update_layout(
        title='CAGR Over Multiple Time Frames',
        xaxis_title='Date',
        yaxis_title='CAGR',
        hovermode='x unified'
    )
    st.plotly_chart(fig, use_container_width=True)

@handle_exceptions
def backtest(weights: list, prices: pd.DataFrame, rebalance_freq: str = 'M', broker_fee: float = 0.0, debug: bool = False) -> tuple:
    """
    Backtest the portfolio based on weights and price data.

    Parameters:
    - weights (list): Allocation weights for each asset.
    - prices (pd.DataFrame): Adjusted closing prices of assets.
    - rebalance_freq (str): Rebalancing frequency (e.g., 'D', 'W', 'M').
    - broker_fee (float): Broker fee as a percentage.
    - debug (bool): If True, prints debug information.

    Returns:
    - tuple: (portfolio_returns: pd.Series, cum_returns: pd.Series)
    """
    try:
        # Calculate returns and drop any NaN values
        returns = prices.pct_change().dropna()
        if returns.empty:
            st.error("Returns data is empty after calculating percentage changes.")
            return pd.Series(dtype=float), pd.Series(dtype=float)

        # Calculate portfolio returns
        portfolio_returns = returns.dot(weights)
        if not isinstance(portfolio_returns, pd.Series):
            portfolio_returns = portfolio_returns.squeeze()

        # Identify rebalancing dates
        if rebalance_freq.upper() == 'D':
            rebalance_dates = returns.index
        else:
            rebalance_dates = returns.resample(rebalance_freq.upper()).last().dropna().index

        # Debug: Show rebalancing dates
        if debug:
            st.write(f"Rebalancing Dates: {rebalance_dates.tolist()}")

        # Ensure rebalance_dates are in the portfolio_returns index
        valid_rebalance_dates = rebalance_dates.intersection(portfolio_returns.index)
        if debug:
            st.write(f"Valid Rebalancing Dates: {valid_rebalance_dates.tolist()}")

        # Apply broker fees on rebalancing dates
        if not valid_rebalance_dates.empty:
            portfolio_returns.loc[valid_rebalance_dates] -= broker_fee / 100  # Convert to decimal
        else:
            st.warning("No valid rebalancing dates found within the returns data.")

        # Calculate cumulative returns
        cum_returns = (1 + portfolio_returns).cumprod()
        if cum_returns.empty:
            st.error("Cumulative returns are empty. Check the data and allocations.")
            return pd.Series(dtype=float), pd.Series(dtype=float)

        # Debug: Show cumulative returns
        if debug:
            st.write("Cumulative Returns:")
            st.write(cum_returns)

        return portfolio_returns, cum_returns
    except Exception as e:
        st.error(f"Error during backtesting: {e}")
        return pd.Series(dtype=float), pd.Series(dtype=float)

@handle_exceptions
def calculate_simple_metrics(returns: pd.Series, cum_returns: pd.Series, start_balance: float = 10000) -> dict:
    """
    Calculate a variety of performance metrics for the portfolio.

    Parameters:
    - returns (pd.Series): Portfolio returns.
    - cum_returns (pd.Series): Cumulative returns of the portfolio.
    - start_balance (float): Starting balance for the portfolio.

    Returns:
    - dict: Dictionary of calculated metrics.
    """
    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}%"
            metrics['Arithmetic Mean (Annualized)'] = f"{returns.mean() * 252 * 100:.2f}%"
            metrics['Geometric Mean (Monthly)'] = f"{(np.exp(np.log1p(returns).mean()) - 1) * 100:.2f}%"
            metrics['Geometric Mean (Annualized)'] = f"{(np.exp(np.log1p(returns).mean() * 252) - 1) * 100:.2f}%"
            metrics['Standard Deviation (Monthly)'] = f"{returns.std() * 100:.2f}%"
            metrics['Standard Deviation (Annualized)'] = f"{returns.std() * np.sqrt(252) * 100:.2f}%"
            metrics['Downside Deviation (Monthly)'] = f"{returns[returns < 0].std() * 100:.2f}%"
            metrics['Maximum Drawdown'] = f"{drawdown(cum_returns) * 100:.2f}%" if not cum_returns.empty else "N/A"
            metrics['Sharpe Ratio'] = f"{calculate_sharpe_ratio(returns, 0.02):.2f}" if not cum_returns.empty else "N/A"
            metrics['Sortino Ratio'] = f"{calculate_sortino_ratio(returns, 0.02):.2f}" if not cum_returns.empty else "N/A"
            metrics['Gain/Loss Ratio'] = f"{calculate_gain_loss_ratio(returns):.2f}" if not cum_returns.empty else "N/A"
            metrics['Skewness'] = f"{skew(returns):.2f}" if not returns.empty else "N/A"
            metrics['Excess Kurtosis'] = f"{kurtosis(returns):.2f}" if not returns.empty else "N/A"
            metrics['Safe Withdrawal Rate'] = f"{calculate_safe_withdrawal_rate(returns):.6f}%" if not returns.empty else "N/A"
            metrics['Perpetual Withdrawal Rate'] = f"{calculate_perpetual_withdrawal_rate(returns):.6f}%" if not returns.empty 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}")
            # Assign "N/A" to all metrics in case of failure
            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:
        # Assign "N/A" to all metrics if cum_returns is empty
        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_sharpe_ratio(returns: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Sharpe Ratio for a given set of returns.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - rf (float): Risk-free rate (default is 2%).

    Returns:
    - float: Sharpe Ratio.
    """
    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: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Sortino Ratio for a given set of returns.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - rf (float): Risk-free rate (default is 2%).

    Returns:
    - float: Sortino Ratio.
    """
    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_treynor_ratio(returns: pd.Series, benchmark_returns: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Treynor Ratio for a given set of returns.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.
    - rf (float): Risk-free rate (default is 2%).

    Returns:
    - float: Treynor Ratio.
    """
    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: pd.Series, cum_returns: pd.Series) -> float:
    """
    Calculate the Calmar Ratio for a given set of returns.

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

    Returns:
    - float: Calmar Ratio.
    """
    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_beta(returns: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the Beta of the portfolio relative to the benchmark.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.

    Returns:
    - float: Beta value.
    """
    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: pd.Series, benchmark_returns: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Alpha of the portfolio relative to the benchmark.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.
    - rf (float): Risk-free rate (default is 2%).

    Returns:
    - float: Alpha value.
    """
    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_r_squared(returns: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the R-squared of the portfolio relative to the benchmark.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.

    Returns:
    - float: R-squared value.
    """
    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: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the Information Ratio of the portfolio relative to the benchmark.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.

    Returns:
    - float: Information Ratio.
    """
    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 generate_recommendations(allocations: list, available_selected: list) -> list:
    """
    Generate basic portfolio recommendations based on current allocations.

    Parameters:
    - allocations (list of float): Current allocation percentages.
    - available_selected (list of str): List of selected asset tickers.

    Returns:
    - list of str: Recommendations.
    """
    recommendations = []
    max_alloc = max(allocations)
    min_alloc = min(allocations)
    total_alloc = sum(allocations)

    if max_alloc > 40.0:
        idx = allocations.index(max_alloc)
        ticker = available_selected[idx]
        recommendations.append(f"🔹 Consider reducing the allocation to **{ticker}** since it constitutes **{max_alloc:.2f}%** of your portfolio.")

    if min_alloc < 5.0:
        idx = allocations.index(min_alloc)
        ticker = available_selected[idx]
        recommendations.append(f"🔸 Consider increasing the allocation to **{ticker}** to at least **5.00%** for better diversification.")

    if total_alloc < 100.0:
        recommendations.append("🔹 Consider allocating the remaining funds to additional assets to reach a total of **100%**.")

    return recommendations

def get_common_benchmarks(selected_tickers: list) -> dict:
    """
    Define benchmark suggestions based on asset sectors or indices.

    Parameters:
    - selected_tickers (list of str): List of selected asset tickers.

    Returns:
    - dict: Dictionary of benchmark names and their corresponding tickers.
    """
    # Example benchmark sets
    sp500 = {"AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA"}  # Example S&P 500 tech companies
    nasdaq_tech = {"AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA", "AMD"}

    selected_set = set(selected_tickers)

    if selected_set.issubset(sp500):
        return {
            "S&P 500": "^GSPC",
            "Dow Jones Industrial Average": "^DJI",
            "Russell 2000": "^RUT",
            "Custom": "CUSTOM"
        }
    elif selected_set.issubset(nasdaq_tech):
        return {
            "NASDAQ Composite": "^IXIC",
            "QQQ (Invesco QQQ ETF)": "QQQ",
            "Custom": "CUSTOM"
        }
    else:
        return {
            "S&P 500": "^GSPC",
            "NASDAQ Composite": "^IXIC",
            "Dow Jones Industrial Average": "^DJI",
            "Russell 2000": "^RUT",
            "Custom": "CUSTOM"
        }

def calculate_tracking_error(returns: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the Tracking Error of the portfolio relative to the benchmark.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.

    Returns:
    - float: Tracking Error.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return np.std((returns - benchmark_returns)) * np.sqrt(252)

@handle_exceptions
def calculate_performance_attribution(returns: pd.Series, weights: list) -> pd.DataFrame:
    """
    Calculate performance attribution based on asset contributions.

    Parameters:
    - returns (pd.Series or pd.DataFrame): Returns of assets.
    - weights (list): Allocation weights for each asset.

    Returns:
    - pd.DataFrame: DataFrame containing asset names and their contribution percentages.
    """
    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
    attribution_df = pd.DataFrame({
        'Asset': returns.columns,
        'Contribution (%)': (contributions / contributions.sum() * 100).round(2)
    }).sort_values(by='Contribution (%)', ascending=False)
    return attribution_df

def calculate_active_return(returns: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the Active Return of the portfolio relative to the benchmark.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.

    Returns:
    - float: Active Return percentage.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return (returns.mean() - benchmark_returns.mean()) * 252 * 100

def calculate_gain_loss_ratio(returns: pd.Series) -> float:
    """
    Calculate the Gain/Loss Ratio of the portfolio.

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

    Returns:
    - float: Gain/Loss Ratio.
    """
    gains = returns[returns > 0].sum()
    losses = -returns[returns < 0].sum()
    return gains / losses if losses != 0 else np.nan

def drawdown(cum_returns: pd.Series) -> float:
    """
    Calculate the Maximum Drawdown of the portfolio.

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

    Returns:
    - float: Maximum Drawdown value.
    """
    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: pd.Series, benchmark_returns: pd.Series, upside: bool = True) -> float:
    """
    Calculate the Upside or Downside Capture Ratio.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.
    - upside (bool): If True, calculates Upside Capture Ratio; otherwise, Downside.

    Returns:
    - float: Capture Ratio percentage.
    """
    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: pd.Series) -> float:
    """
    Calculate the Safe Withdrawal Rate based on portfolio returns.

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

    Returns:
    - float: Safe Withdrawal Rate percentage.
    """
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

def calculate_perpetual_withdrawal_rate(returns: pd.Series) -> float:
    """
    Calculate the Perpetual Withdrawal Rate based on portfolio returns.

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

    Returns:
    - float: Perpetual Withdrawal Rate percentage.
    """
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

def calculate_positive_periods(returns: pd.Series) -> str:
    """
    Calculate the number and percentage of positive return periods.

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

    Returns:
    - str: String describing positive 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: pd.Series, benchmark_returns: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Modigliani–Modigliani Measure (M2).

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.
    - rf (float): Risk-free rate (default is 2%).

    Returns:
    - float: M2 value.
    """
    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 get_drawdown_details(cum_returns: pd.Series) -> list:
    """
    Generate detailed drawdown information.

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

    Returns:
    - list: List of dictionaries containing drawdown details.
    """
    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

@handle_exceptions
def optimize_portfolio(
    returns: pd.DataFrame,
    benchmark_returns: pd.Series = None,
    objectives: list = ['sharpe'],
    rf: float = 0.02,
    max_weight: float = 1.0,
    min_weight: float = 0.0,
    target_return: float = None
) -> np.ndarray:
    """
    Optimize a portfolio based on specified objectives.

    Parameters:
    - returns (pd.DataFrame): Historical returns of assets.
    - benchmark_returns (pd.Series, optional): Returns of the benchmark index.
    - objectives (list): Objectives to optimize.
        Options include 'sharpe', 'min_variance', 'max_return', 'min_drawdown', 'maximize_alpha', 'minimize_beta'.
    - rf (float): Risk-free rate for alpha calculation.
    - max_weight (float): Maximum weight per asset.
    - min_weight (float): Minimum weight per asset.
    - target_return (float, optional): Target return for the portfolio.

    Returns:
    - np.ndarray: Optimized asset weights or None if optimization fails.
    """
    from scipy.optimize import minimize

    def calculate_beta(portfolio_returns: pd.Series, benchmark_returns: pd.Series) -> float:
        covariance = np.cov(portfolio_returns, benchmark_returns)
        variance = covariance[1, 1]
        return covariance[0, 1] / variance if variance != 0 else 0

    def calculate_var(portfolio_returns: np.ndarray, confidence_level: float = 0.95) -> float:
        return np.percentile(portfolio_returns, (1 - confidence_level) * 100)

    def calculate_cvar(portfolio_returns: np.ndarray, confidence_level: float = 0.95) -> float:
        var = calculate_var(portfolio_returns, confidence_level)
        return portfolio_returns[portfolio_returns <= var].mean()

    # Define individual objective functions
    def sharpe_ratio(weights: np.ndarray) -> float:
        portfolio_return = np.dot(returns.mean(), weights) * 252
        portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))
        return -portfolio_return / portfolio_volatility if portfolio_volatility > 1e-6 else 0  # Negative for maximization

    def min_variance(weights: np.ndarray) -> float:
        return np.dot(weights.T, np.dot(returns.cov() * 252, weights))

    def max_return(weights: np.ndarray) -> float:
        return -np.dot(returns.mean(), weights) * 252  # Negative for maximization

    def min_drawdown(weights: np.ndarray) -> float:
        portfolio_returns = returns.dot(weights)
        cvar = calculate_cvar(portfolio_returns, 0.95)
        return cvar  # Minimizing CVaR as a proxy for drawdown

    # Initialize list of objective functions
    objective_functions = []

    # Map objectives to functions
    for obj in objectives:
        if obj == 'sharpe':
            objective_functions.append(sharpe_ratio)
        elif obj == 'min_variance':
            objective_functions.append(min_variance)
        elif obj == 'max_return':
            objective_functions.append(max_return)
        elif obj == 'min_drawdown':
            objective_functions.append(min_drawdown)
        elif obj == 'maximize_alpha':
            if benchmark_returns is None:
                st.error("benchmark_returns must be provided for 'maximize_alpha' objective.")
                return None

            benchmark_return = benchmark_returns.mean() * 252  # Assuming daily returns annualized

            def maximize_alpha(weights: np.ndarray) -> float:
                portfolio_return = np.dot(returns.mean(), weights) * 252
                portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))
                portfolio_returns = returns.dot(weights)
                beta = calculate_beta(portfolio_returns, benchmark_returns)
                alpha = portfolio_return - (rf + beta * (benchmark_return - rf))
                return -alpha  # Negative for maximization

            objective_functions.append(maximize_alpha)
        elif obj == 'minimize_beta':
            if benchmark_returns is None:
                st.error("benchmark_returns must be provided for 'minimize_beta' objective.")
                return None

            def minimize_beta(weights: np.ndarray) -> float:
                portfolio_returns = returns.dot(weights)
                beta = calculate_beta(portfolio_returns, benchmark_returns)
                return beta

            objective_functions.append(minimize_beta)
        else:
            st.error(f"Invalid optimization objective: {obj}")
            return None

    # Composite objective function: weighted sum of individual objectives
    def composite_objective(weights: np.ndarray) -> float:
        return sum(fn(weights) for fn in objective_functions)

    # Define constraints
    constraints = [
        {'type': 'eq', 'fun': lambda x: np.sum(x) - 1},
    ]

    if target_return is not None:
        def target_return_constraint(x: np.ndarray) -> float:
            return np.dot(x, returns.mean()) * 252 - target_return
        constraints.append({
            'type': 'ineq',
            'fun': target_return_constraint
        })

    # Define bounds for weights
    bounds = tuple((min_weight, max_weight) for _ in range(returns.shape[1]))

    # Initial guess (equally distributed weights)
    initial_guess = np.array([1.0 / returns.shape[1]] * returns.shape[1])

    # Perform optimization
    result = minimize(
        composite_objective,
        initial_guess,
        method='SLSQP',
        bounds=bounds,
        constraints=constraints
    )

    if result.success:
        optimized_weights = result.x
        portfolio_volatility = np.sqrt(np.dot(optimized_weights.T, np.dot(returns.cov() * 252, optimized_weights)))
        portfolio_return = np.dot(returns.mean(), optimized_weights) * 252
        sharpe_ratio_val = (portfolio_return - rf) / portfolio_volatility if portfolio_volatility > 0 else np.nan

        if portfolio_volatility <= 1e-6:
            st.error("Optimized portfolio has near-zero volatility. Optimization constraints may be too restrictive.")
            return None

        # Store optimized results in session state
        st.session_state.backtest_results['optimized_weights'] = optimized_weights
        st.session_state.backtest_results['optimized_return'] = portfolio_return
        st.session_state.backtest_results['optimized_volatility'] = portfolio_volatility
        st.session_state.backtest_results['optimized_sharpe'] = sharpe_ratio_val

        return optimized_weights
    else:
        st.error("Optimization failed. Try adjusting your constraints or target return.")
        return None

def monte_carlo_simulation(
    returns: pd.DataFrame,
    num_simulations: int = 1000,
    periods: int = 252,
    mean_returns: pd.Series = None,
    cov_matrix: pd.DataFrame = None,
    mean_reversion: bool = False,
    mean_reversion_speed: float = 0.1,
    long_term_mean: pd.Series = None,
    time_varying_vol: bool = False,
    vol_change_rate: float = 0.0,
    stress_shocks: dict = None,
    stress_period: int = None,
    return_distribution: str = 'normal'
) -> np.ndarray:
    """
    Perform Monte Carlo simulations for portfolio returns.

    Parameters:
    - returns (pd.DataFrame): Historical returns of assets.
    - num_simulations (int): Number of simulation paths.
    - periods (int): Number of periods to simulate.
    - mean_returns (pd.Series, optional): Mean returns of assets.
    - cov_matrix (pd.DataFrame, optional): Covariance matrix of asset returns.
    - mean_reversion (bool): If True, applies mean reversion.
    - mean_reversion_speed (float): Speed of mean reversion.
    - long_term_mean (pd.Series, optional): Long-term mean for mean reversion.
    - time_varying_vol (bool): If True, allows volatility to change over time.
    - vol_change_rate (float): Rate at which volatility changes.
    - stress_shocks (dict, optional): Dict of shocks to apply at specified period.
    - stress_period (int, optional): Period at which to apply stress shocks.
    - return_distribution (str): Distribution type ('normal' or 'log-normal').

    Returns:
    - np.ndarray: Array of simulated cumulative returns.
    """
    if mean_returns is None:
        mean_returns = returns.mean()
    if cov_matrix is None:
        cov_matrix = returns.cov()

    num_assets = returns.shape[1]
    available_selected = returns.columns.tolist()

    def simulate():
        weights = np.random.random(num_assets)
        weights /= np.sum(weights)
        simulated_prices = [1]  # Initial price
        current_mean = mean_returns.copy()
        current_vol = np.sqrt(np.diag(cov_matrix))
        for t in range(periods):
            if mean_reversion and long_term_mean is not None:
                current_mean += mean_reversion_speed * (long_term_mean - current_mean)
            if time_varying_vol:
                current_vol += vol_change_rate
                current_vol = np.clip(current_vol, 0, None)  # Ensure volatility doesn't go negative

            adjusted_cov_matrix = np.outer(current_vol, current_vol) * np.corrcoef(returns.T)
            if return_distribution == 'log-normal':
                simulated_returns = np.random.lognormal(mean=np.log(1 + current_mean), sigma=current_vol) - 1
            else:
                simulated_returns = np.random.multivariate_normal(current_mean, adjusted_cov_matrix)

            # Apply stress shocks if applicable
            if stress_shocks and t == stress_period:
                for idx, ticker in enumerate(available_selected):
                    simulated_returns[idx] += stress_shocks.get(ticker, 0)

            portfolio_return = np.dot(simulated_returns, weights)
            simulated_prices.append(simulated_prices[-1] * (1 + portfolio_return))
        cumulative_return = simulated_prices[-1] - 1
        return cumulative_return

    portfolio_returns = Parallel(n_jobs=-1)(
        delayed(simulate)() for _ in range(num_simulations)
    )

    return np.array(portfolio_returns)

@handle_exceptions
def plot_efficient_frontier(returns: pd.DataFrame, num_portfolios: int = 1000, rf: float = 0.0) -> None:
    """
    Plot the Efficient Frontier based on Monte Carlo simulations.

    Parameters:
    - returns (pd.DataFrame): Historical returns of assets.
    - num_portfolios (int): Number of portfolios to simulate.
    - rf (float): Risk-free rate for Sharpe Ratio calculation.
    """
    from scipy.optimize import minimize

    num_assets = returns.shape[1]

    def generate_portfolio_metrics(weights: np.ndarray) -> tuple:
        portfolio_return = np.dot(returns.mean(), weights) * 252
        portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))
        sharpe_ratio = (portfolio_return - rf) / portfolio_volatility if portfolio_volatility > 0 else 0
        return portfolio_volatility, portfolio_return, sharpe_ratio

    # Generate random portfolios
    weights_list = np.random.dirichlet(np.ones(num_assets), num_portfolios)
    portfolio_metrics = np.array([generate_portfolio_metrics(w) for w in weights_list])

    # Filter out invalid portfolios
    portfolio_metrics = portfolio_metrics[~np.isnan(portfolio_metrics).any(axis=1)]

    if portfolio_metrics.size == 0:
        st.warning("No valid portfolios to plot on the Efficient Frontier.")
        return

    ef_df = pd.DataFrame({
        'Std Dev': portfolio_metrics[:, 0],
        'Return': portfolio_metrics[:, 1],
        'Sharpe Ratio': portfolio_metrics[:, 2]
    })
    fig = px.scatter(
        ef_df,
        x='Std Dev',
        y='Return',
        color='Sharpe Ratio',
        color_continuous_scale='Viridis',
        title='Efficient Frontier',
        labels={
            'Std Dev': 'Annualized Volatility (Std Dev)',
            'Return': 'Annualized Return',
            'Sharpe Ratio': 'Sharpe Ratio'
        },
        hover_data={
            'Sharpe Ratio': ':.2f',
            'Std Dev': ':.2f',
            'Return': ':.2f'
        },
        template='plotly_dark'
    )

    # Add annotation for the maximum Sharpe ratio portfolio
    max_sharpe_idx = ef_df['Sharpe Ratio'].idxmax()
    max_sharpe = ef_df.loc[max_sharpe_idx]
    fig.add_annotation(
        x=max_sharpe['Std Dev'],
        y=max_sharpe['Return'],
        text="Max Sharpe",
        showarrow=True,
        arrowhead=1
    )

    # Check for Optimized Portfolio in Session State
    optimized_weights = st.session_state.backtest_results.get('optimized_weights', None)
    optimized_return = st.session_state.backtest_results.get('optimized_return', None)
    optimized_volatility = st.session_state.backtest_results.get('optimized_volatility', None)
    optimized_sharpe = st.session_state.backtest_results.get('optimized_sharpe', None)

    if all(v is not None for v in [optimized_weights, optimized_return, optimized_volatility, optimized_sharpe]):
        if optimized_volatility > 1e-6 and np.isfinite(optimized_sharpe):
            # Plot optimized portfolio
            fig.add_trace(go.Scatter(
                x=[optimized_volatility],
                y=[optimized_return],
                mode='markers+text',
                marker=dict(color='red', size=14, symbol='star'),
                name='Optimized Portfolio',
                text=["Optimized"],
                textposition="top center",
                hoverinfo='text'
            ))
            fig.add_annotation(
                x=optimized_volatility,
                y=optimized_return,
                text="Optimized",
                showarrow=True,
                arrowhead=2,
                ax=0,
                ay=-40
            )

    fig.update_layout(
        hovermode='x unified',
        xaxis=dict(rangeslider=dict(visible=True), type='date'),
    )
    st.plotly_chart(fig, use_container_width=True, config={
        'responsive': True,
        'scrollZoom': True,
        'displayModeBar': True
    })

@handle_exceptions
def generate_portfolio(returns: pd.DataFrame, rf: float = 0.0) -> tuple:
    """
    Generate a random portfolio and calculate its metrics.

    Parameters:
    - returns (pd.DataFrame): Historical returns of assets.
    - rf (float): Risk-free rate for Sharpe Ratio calculation.

    Returns:
    - tuple: (volatility: float, return: float, sharpe_ratio: float)
    """
    try:
        weights = np.random.dirichlet(np.ones(returns.shape[1]))
        portfolio_return = np.dot(returns.mean(), weights) * 252
        portfolio_volatility = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))
        sharpe_ratio = (portfolio_return - rf) / portfolio_volatility if portfolio_volatility > 0 else 0
        return portfolio_volatility, portfolio_return, sharpe_ratio
    except Exception as e:
        st.error(f"Error generating portfolio: {e}")
        return np.nan, np.nan, np.nan  # Return NaNs to indicate failure

def plot_allocation_pie(weights: list, assets: list, title: str = 'Portfolio Allocation', hover_info: str = None) -> None:
    """
    Plot a pie chart of the portfolio allocation.

    Parameters:
    - weights (list): Allocation weights for each asset.
    - assets (list): List of asset names or tickers.
    - title (str): Title of the pie chart.
    - hover_info (str): Type of hover information ('percent+name', 'name', 'percent').
    """
    allocation_df = pd.DataFrame({
        'Asset': assets,
        'Weight': weights
    })

    # Configure hover data based on hover_info parameter
    if hover_info == "percent+name":
        hover_data = ['Weight']
    elif hover_info == "name":
        hover_data = []
    elif hover_info == "percent":
        hover_data = ['Weight']
    else:
        hover_data = []

    fig = px.pie(
        allocation_df,
        names='Asset',
        values='Weight',
        title=title,
        color='Asset',
        color_discrete_sequence=px.colors.qualitative.Set3,
        hover_data=hover_data
    )
    st.plotly_chart(fig, use_container_width=True)

def plot_rolling_cagr(cum_returns: pd.Series, window: int = 252) -> None:
    """
    Plot Rolling CAGR over a specified window.

    Parameters:
    - cum_returns (pd.Series): Cumulative returns of the portfolio.
    - window (int): Window size for rolling CAGR calculation.
    """
    try:
        rolling_years = window / 252  # Assuming daily data
        rolling_cagr = (cum_returns / cum_returns.shift(window)) ** (1 / rolling_years) - 1
        fig = px.line(
            rolling_cagr.dropna(),
            title=f'Rolling {int(rolling_years)}-Year CAGR',
            labels={'index': 'Date', 'value': 'Rolling CAGR'},
            template='plotly_dark'
        )
        st.plotly_chart(fig, use_container_width=True)
    except Exception as e:
        st.error(f"Error plotting rolling CAGR: {e}")

@handle_exceptions
def plot_correlation_heatmap(returns: pd.DataFrame) -> None:
    """
    Plot a heatmap of asset correlations.

    Parameters:
    - returns (pd.DataFrame): Daily returns of assets.
    """
    if isinstance(returns, pd.DataFrame) and returns.shape[1] >= 2:
        valid_columns = [col for col in returns.columns if returns[col].nunique() > 1 and returns[col].notna().sum() >= 10]
        if len(valid_columns) < 2:
            st.warning("Correlation heatmap requires at least two assets with variability and sufficient data.")
            return
        corr_matrix = returns[valid_columns].corr()
        fig = px.imshow(
            corr_matrix,
            title='Asset Correlation Heatmap',
            labels={'x': 'Asset', 'y': 'Asset', 'color': 'Correlation'},
            color_continuous_scale='Portland',
            zmin=-1,
            zmax=1,
            text_auto=True,
            aspect="auto",
            template='plotly_dark'
        )
        fig.update_xaxes(side="top")
        st.plotly_chart(fig, use_container_width=True)
    else:
        st.warning("Correlation heatmap requires at least two assets with variability.")

@handle_exceptions
def plot_rolling_metrics(returns: pd.Series, windows: list = [30, 90, 180, 252]) -> None:
    """
    Plot rolling metrics (Volatility and Sharpe Ratio) over multiple windows.

    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - windows (list): List of window sizes for rolling calculations.
    """
    metrics = {}
    for window in windows:
        rolling_return = returns.rolling(window).mean() * 252
        rolling_vol = returns.rolling(window).std() * np.sqrt(252)
        rolling_sharpe = (rolling_return - 0.02) / rolling_vol
        metrics[f'Rolling {window}-Day Volatility'] = rolling_vol
        metrics[f'Rolling {window}-Day Sharpe Ratio'] = rolling_sharpe
    fig = go.Figure()
    for metric_name, metric_series in metrics.items():
        fig.add_trace(go.Scatter(x=metric_series.index, y=metric_series, mode='lines', name=metric_name))
    fig.update_layout(
        title='Rolling Metrics Over User-Defined Periods',
        xaxis_title='Date',
        yaxis_title='Value',
        hovermode='x unified',
        template='plotly_dark'
    )
    st.plotly_chart(fig, use_container_width=True)

@handle_exceptions
def plot_risk_return_attribution(returns: pd.Series, weights: list) -> None:
    """
    Plot Risk-Return Attribution for the portfolio.

    Parameters:
    - returns (pd.Series or pd.DataFrame): Returns of assets.
    - weights (list): Allocation weights for each asset.
    """
    try:
        if isinstance(returns, pd.Series):
            asset_returns = returns.mean() * 252
            asset_volatility = returns.std() * np.sqrt(252)
            asset_contribution = weights[0] * asset_volatility if len(weights) > 0 else 0
            hover_names = [returns.name] if returns.name else ['Asset']
            x = [asset_volatility]
            y = [asset_returns]
            size = [asset_contribution]
            color = [weights[0]]
        else:
            asset_returns = returns.mean() * 252
            asset_volatility = returns.std() * np.sqrt(252)
            asset_contribution = np.array(weights) * asset_volatility
            hover_names = returns.columns.tolist()
            x = asset_volatility.values
            y = asset_returns.values
            size = asset_contribution
            color = weights

        fig = px.scatter(
            x=x,
            y=y,
            size=size,
            color=color,
            hover_name=hover_names,
            title='Risk-Return Attribution',
            labels={'x': 'Annualized Volatility', 'y': 'Annualized Return', 'color': 'Weight (%)'},
            size_max=60,
            color_continuous_scale='Viridis'
        )
        fig.update_layout(template='plotly_dark')
        st.plotly_chart(fig, use_container_width=True)
    except Exception as e:
        st.error(f"Error plotting risk-return attribution: {e}")

def plot_var_cvar_distribution(portfolio_returns: np.ndarray, var: float, cvar: float) -> None:
    """
    Plot the distribution of portfolio returns with VaR and CVaR lines.

    Parameters:
    - portfolio_returns (np.ndarray): Simulated portfolio returns.
    - var (float): Value at Risk.
    - cvar (float): Conditional Value at Risk.
    """
    fig = px.histogram(portfolio_returns, nbins=50, title='Returns Distribution with VaR and CVaR',
                       labels={'value': 'Returns', 'count': 'Frequency'})
    fig.add_vline(x=var, line_dash="dash", line_color="red", annotation_text=f"VaR: {var:.2f}", annotation_position="top left")
    fig.add_vline(x=cvar, line_dash="dash", line_color="blue", annotation_text=f"CVaR: {cvar:.2f}", annotation_position="top left")
    st.plotly_chart(fig, use_container_width=True)

def plot_var_cvar_over_time(portfolio_returns: pd.Series, var: float, cvar: float) -> None:
    """
    Plot cumulative returns with VaR and CVaR over time.

    Parameters:
    - portfolio_returns (pd.Series): Daily returns of the portfolio.
    - var (float): Value at Risk.
    - cvar (float): Conditional Value at Risk.
    """
    cumulative_returns = (1 + portfolio_returns).cumprod()
    fig = px.line(cumulative_returns, title='Cumulative Returns with VaR and CVaR Over Time', labels={'value': 'Cumulative Returns', 'index': 'Date'})
    fig.add_hline(y=var, line_dash="dash", line_color="red", annotation_text=f"VaR: {var:.2f}", annotation_position="bottom right")
    fig.add_hline(y=cvar, line_dash="dash", line_color="blue", annotation_text=f"CVaR: {cvar:.2f}", annotation_position="bottom right")
    st.plotly_chart(fig, use_container_width=True)

def calculate_var(returns: np.ndarray, confidence_level: float = 0.95) -> float:
    """
    Calculate Value at Risk (VaR) for a given set of returns.

    Parameters:
    - returns (np.ndarray): Portfolio returns.
    - confidence_level (float): Confidence level for VaR.

    Returns:
    - float: VaR value.
    """
    return np.percentile(returns, 100 * (1 - confidence_level))

def calculate_cvar(returns: np.ndarray, confidence_level: float = 0.95) -> float:
    """
    Calculate Conditional Value at Risk (CVaR) for a given set of returns.

    Parameters:
    - returns (np.ndarray): Portfolio returns.
    - confidence_level (float): Confidence level for CVaR.

    Returns:
    - float: CVaR value.
    """
    var = calculate_var(returns, confidence_level)
    return returns[returns <= var].mean()

# ----------------------------
# Caching Functions
# ----------------------------

@st.cache_data(show_spinner=False, persist=True)
def get_tickers() -> pd.DataFrame:
    """
    Fetch and combine S&P 500 and NASDAQ-100 tickers from Wikipedia.

    Returns:
    - pd.DataFrame: DataFrame containing combined tickers and company names.
    """
    try:
        # Fetch S&P 500 companies
        sp500_url = 'https://en.wikipedia.org/wiki/List_of_S%26P_500_companies'
        sp500_response = requests.get(sp500_url, verify=False)
        sp500_table = pd.read_html(sp500_response.text)[0]
        sp500 = sp500_table[['Symbol', 'Security']].rename(columns={'Symbol': 'Ticker', 'Security': 'Company Name'})
        sp500['Ticker'] = sp500['Ticker'].str.replace('.', '-', regex=False)

        # Fetch NASDAQ-100 companies
        nasdaq100_url = 'https://en.wikipedia.org/wiki/NASDAQ-100'
        nasdaq100_response = requests.get(nasdaq100_url, verify=False)
        nasdaq100_tables = pd.read_html(nasdaq100_response.text)

        nasdaq100 = pd.DataFrame()
        for table in nasdaq100_tables:
            if 'Ticker' in table.columns and 'Company' in table.columns:
                nasdaq100 = table[['Ticker', 'Company']].rename(columns={'Ticker': 'Ticker', 'Company': 'Company Name'})
                nasdaq100['Ticker'] = nasdaq100['Ticker'].str.replace('.', '-', regex=False)
                break

        combined = pd.concat([sp500, nasdaq100], ignore_index=True)
        combined = combined.drop_duplicates(subset=['Ticker'])
        return combined.sort_values('Ticker').reset_index(drop=True)
    except Exception as e:
        st.error(f"Error fetching tickers: {e}")
        return pd.DataFrame(columns=['Ticker', 'Company Name'])

@st.cache_data(show_spinner=False, persist=True)
def download_data(tickers: list, start: str, end: str, retries: int = 3, backoff_factor: float = 0.3) -> pd.DataFrame:
    """
    Download historical adjusted closing prices for specified tickers.

    Parameters:
    - tickers (list): List of ticker symbols.
    - start (str): Start date in 'YYYY-MM-DD' format.
    - end (str): End date in 'YYYY-MM-DD' format.
    - retries (int): Number of retries for failed requests.
    - backoff_factor (float): Backoff factor for retries.

    Returns:
    - pd.DataFrame: DataFrame containing adjusted closing prices.
    """
    try:
        # Configure retry strategy for requests (used by yfinance internally)
        session = requests.Session()
        retry = Retry(
            total=retries,
            read=retries,
            connect=retries,
            backoff_factor=backoff_factor,
            status_forcelist=(500, 502, 504),
        )
        adapter = HTTPAdapter(max_retries=retry)
        session.mount('http://', adapter)
        session.mount('https://', adapter)

        # Attempt to download data
        data = yf.download(tickers, start=start, end=end, progress=False, session=session)['Adj Close']

        # Handle potential empty data
        if isinstance(data, pd.Series):
            data = data.to_frame()
        if data.empty:
            st.warning("No price data available for the selected portfolio. Please check the ticker symbols and date range.")
            return pd.DataFrame()
        if data.index.tz is not None:
            data.index = data.index.tz_localize(None)

        # Fill missing data
        data = data.fillna(method='ffill').fillna(method='bfill')
        if data.isnull().values.any():
            st.warning("Data contains missing values after filling. Some calculations may be affected.")
        return data
    except Exception as e:
        st.error(f"Error downloading data: {e}")
        return pd.DataFrame()

@st.cache_data(show_spinner=False, persist=True)
def calculate_portfolio_metrics(returns: pd.Series, cum_returns: pd.Series, rf: float, benchmark_returns: pd.Series = None) -> dict:
    """
    Calculate comprehensive portfolio metrics, optionally relative to a benchmark.

    Parameters:
    - returns (pd.Series): Portfolio returns.
    - cum_returns (pd.Series): Cumulative returns of the portfolio.
    - rf (float): Risk-free rate.
    - benchmark_returns (pd.Series, optional): Benchmark returns.

    Returns:
    - dict: Dictionary of calculated metrics.
    """
    metrics = calculate_simple_metrics(returns, cum_returns)

    if benchmark_returns is not None and not benchmark_returns.empty:
        correlation = returns.corr(benchmark_returns)
        beta = calculate_beta(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"

        alpha = calculate_alpha(returns, benchmark_returns, rf)
        metrics['Alpha (annualized)'] = f"{alpha * 100:.2f}%" if not np.isnan(alpha) else "N/A"

        r2 = calculate_r_squared(returns, benchmark_returns)
        metrics['R2'] = f"{r2 * 100:.2f}%" if not np.isnan(r2) else "N/A"

        treynor = calculate_treynor_ratio(returns, benchmark_returns, rf)
        metrics['Treynor Ratio'] = f"{treynor:.2f}" if not np.isnan(treynor) else "N/A"

        calmar = calculate_calmar_ratio(returns, cum_returns)
        metrics['Calmar Ratio'] = f"{calmar:.2f}" if not np.isnan(calmar) else "N/A"

        m2 = calculate_modigliani_miller(returns, benchmark_returns, rf)
        metrics['Modigliani–Modigliani Measure'] = f"{m2 * 100:.2f}%" if not np.isnan(m2) else "N/A"

        info_ratio = calculate_information_ratio(returns, benchmark_returns)
        metrics['Information Ratio'] = f"{info_ratio:.2f}" if not np.isnan(info_ratio) else "N/A"

        tracking_error = calculate_tracking_error(returns, benchmark_returns)
        metrics['Tracking Error'] = f"{tracking_error * 100:.2f}%" if not np.isnan(tracking_error) else "N/A"

        active_return = calculate_active_return(returns, benchmark_returns)
        metrics['Active Return'] = f"{active_return:.2f}%" if not np.isnan(active_return) else "N/A"

        upside_capture = calculate_capture_ratio(returns, benchmark_returns, upside=True)
        downside_capture = calculate_capture_ratio(returns, benchmark_returns, upside=False)
        metrics['Upside Capture Ratio'] = f"{upside_capture:.2f}%" if not np.isnan(upside_capture) else "N/A"
        metrics['Downside Capture Ratio'] = f"{downside_capture:.2f}%" if not np.isnan(downside_capture) else "N/A"
    else:
        # Assign "N/A" to all metrics if no benchmark is provided
        metrics.update({
            'Benchmark Correlation': "N/A",
            'Beta': "N/A",
            'Alpha (annualized)': "N/A",
            'R2': "N/A",
            'Treynor Ratio': "N/A",
            'Calmar Ratio': "N/A",
            'Modigliani–Modigliani Measure': "N/A",
            'Information Ratio': "N/A",
            'Tracking Error': "N/A",
            'Active Return': "N/A",
            'Upside Capture Ratio': "N/A",
            'Downside Capture Ratio': "N/A"
        })

    return metrics

# ----------------------------
# Additional Helper Functions
# ----------------------------

@handle_exceptions
def plot_efficient_frontier_comparison(
    simulated_metrics: pd.DataFrame,
    optimized_metrics: tuple
) -> None:
    """
    Plot Efficient Frontier and mark the optimized portfolio.

    Parameters:
    - simulated_metrics (pd.DataFrame): Simulated portfolio metrics.
    - optimized_metrics (tuple): Metrics of the optimized portfolio.
    """
    if simulated_metrics.empty:
        st.warning("No simulated metrics available to plot.")
        return

    fig = px.scatter(
        simulated_metrics,
        x='Std Dev',
        y='Return',
        color='Sharpe Ratio',
        color_continuous_scale='Viridis',
        title='Efficient Frontier',
        labels={
            'Std Dev': 'Annualized Volatility (Std Dev)',
            'Return': 'Annualized Return',
            'Sharpe Ratio': 'Sharpe Ratio'
        },
        hover_data={
            'Sharpe Ratio': ':.2f',
            'Std Dev': ':.2f',
            'Return': ':.2f'
        },
        template='plotly_dark'
    )

    # Add optimized portfolio if available
    if optimized_metrics:
        vol, ret, sharpe = optimized_metrics
        if vol > 0 and np.isfinite(sharpe):
            fig.add_trace(go.Scatter(
                x=[vol],
                y=[ret],
                mode='markers+text',
                marker=dict(color='red', size=14, symbol='star'),
                name='Optimized Portfolio',
                text=["Optimized"],
                textposition="top center",
                hoverinfo='text'
            ))
            fig.add_annotation(
                x=vol,
                y=ret,
                text="Optimized",
                showarrow=True,
                arrowhead=2,
                ax=0,
                ay=-40
            )

    fig.update_layout(
        hovermode='x unified',
        xaxis=dict(rangeslider=dict(visible=True), type='linear'),
    )
    st.plotly_chart(fig, use_container_width=True, config={
        'responsive': True,
        'scrollZoom': True,
        'displayModeBar': True
    })

# ----------------------------
# End of Helper Functions
# ----------------------------

# ----------------------------
# Streamlit Layout
# ----------------------------
st.set_page_config(page_title="🎯 Portfolio Optimizer", layout="wide")

# Display loading spinner
with st.spinner("🎯 Portfolio Optimizer is loading, please be patient..."):
    # Simulate loading delay if necessary
    pass

st.sidebar.header("📂 Navigation")
nav_steps = [
    "Configure Portfolio",
    "Run Backtest",
    "Optimize Portfolio",
    "Monte Carlo Simulations",
    "Risk Analysis"
]
current_step = st.sidebar.radio(
    "Navigate to",
    nav_steps,
    index=nav_steps.index(st.session_state.step),
    key="sidebar_radio"
)

tickers = get_tickers()

if current_step == "Configure Portfolio":
    st.title("🎯 Portfolio Optimizer")
    # Initialize show_proceed in session state
    if 'show_proceed' not in st.session_state:
        st.session_state.show_proceed = False

    # Display onboarding message and button if not proceeded
    if not st.session_state.show_proceed:
        st.info("Welcome! Let's get you started with your first portfolio.")
        st.write("""
            1. **Configure your portfolio** by entering a name, start date, end date, and other details.
            2. **Select the assets** you want to include and set their allocations.
            3. **Run a backtest** to see how your portfolio performs.
            4. **Compare your portfolio** with a benchmark or another portfolio.
            5. **Explore the performance metrics** and visualizations.
        """)
        if st.button("Got U, Let's Explore"):
            st.session_state.show_proceed = True

    # Define and display tabs only if proceeded
    if st.session_state.show_proceed:
        tabs_portfolio = st.tabs(["📁 Existing Portfolios", "➕ Add New Portfolio"])

        with tabs_portfolio[0]:
            st.subheader("📁 Manage Existing Portfolios")
            st.markdown("⚙️ Edit, Delete, or Duplicate your existing portfolios here.")
            
            if st.session_state.portfolios:
                portfolio_names = [p['name'] for p in st.session_state.portfolios]
                selected_portfolio = st.selectbox("Select a Portfolio", portfolio_names, key="select_portfolio")
                st.session_state.selected_portfolio = selected_portfolio
                if selected_portfolio:
                    portfolio = next(p for p in st.session_state.portfolios if p['name'] == selected_portfolio)
                    
                    st.markdown("### Selected Portfolio: **{0}**".format(selected_portfolio))
                    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']}")
                    st.write(f"**Rebalance Frequency:** {rebalance_freq_inverse_mapping.get(portfolio['rebalance_freq'], portfolio['rebalance_freq'])}")
                    
                    # Action Buttons
                    action_col1, action_col2, action_col3 = st.columns(3)
                    
                    with action_col1:
                        if st.button("🗑️ Delete Portfolio", key=f"delete_{selected_portfolio}"):
                            st.session_state.portfolios = [p for p in st.session_state.portfolios if p['name'] != selected_portfolio]
                            st.success(f"Portfolio '{selected_portfolio}' deleted.")
                            if st.session_state.edit_portfolio == selected_portfolio:
                                st.session_state.edit_portfolio = None
                            # Reset the selected portfolio
                            st.session_state.selected_portfolio = None
                            # Force Streamlit to rerun and update the UI immediately
                            st.experimental_rerun()

                    with action_col2:
                        if st.button("✏️ Edit Portfolio"):
                            st.session_state.edit_portfolio = selected_portfolio

                    with action_col3:
                        if st.button("📄 Duplicate Portfolio", key=f"duplicate_{selected_portfolio}"):
                            duplicated_portfolio = portfolio.copy()
                            duplicated_portfolio['name'] = f"{selected_portfolio}_Copy"
                            # Ensure the new name is unique
                            if duplicated_portfolio['name'] in portfolio_names:
                                counter = 1
                                while f"{selected_portfolio}_Copy{counter}" in portfolio_names:
                                    counter += 1
                                duplicated_portfolio['name'] = f"{selected_portfolio}_Copy{counter}"
                            st.session_state.portfolios.append(duplicated_portfolio)
                            st.success(f"Portfolio '{duplicated_portfolio['name']}' duplicated successfully.")
            else:
                st.write("No portfolios saved.")

        with tabs_portfolio[1]:
            st.subheader("➕ Create a New Portfolio")
            st.markdown("🛠️ **Configure your new portfolio by filling out the details below.**")
            
            # Portfolio Details
            st.markdown("#### 📂 Portfolio Details")
            name = st.text_input("📛 Portfolio Name", help="Enter a unique name for your portfolio.")
            
            dates_col1, dates_col2 = st.columns(2)
            with dates_col1:
                start_date_input = st.date_input(
                    "📅 Start Date:",
                    datetime.today() - timedelta(days=365 * 15),
                    min_value=datetime(1900, 1, 1),
                    help="Choose the start date for backtesting."
                )
            with dates_col2:
                end_date_input = st.date_input(
                    "📅 End Date:",
                    datetime.today(),
                    min_value=datetime(1900, 1, 1),
                    help="Choose the end date for backtesting."
                )
            
            # Financial Settings
            settings_col1, settings_col2 = st.columns(2)
            with settings_col1:
                rf_rate = st.number_input(
                    "📈 Risk-Free Rate (%)",
                    0.0,
                    10.0,
                    st.session_state.default_config['rf_rate'] * 100,
                    help="Enter the risk-free rate as a percentage."
                ) / 100
            with settings_col2:
                broker_fee = st.number_input(
                    "💸 Broker Fee (%)",
                    0.0,
                    10.0,
                    st.session_state.default_config['broker_fee'] * 100,
                    step=0.000001,
                    format="%.6f",
                    help="Enter the broker fee as a percentage per transaction."
                ) / 100

            # Benchmark and Rebalancing
            benchmark_col, rebalance_col = st.columns(2)
            with benchmark_col:
                benchmark_symbol = st.text_input(
                    "📊 Benchmark Symbol (e.g., ^GSPC)",
                    st.session_state.default_config['benchmark_symbol'],
                    help="Enter the ticker symbol for your benchmark index."
                )
            with rebalance_col:
                rebalance_freq = st.selectbox(
                    "🔄 Rebalance Frequency",
                    ["Daily", "Weekly", "Monthly", "Quarterly", "Yearly"],
                    index=2,
                    help="Choose how often to rebalance the portfolio."
                )

            # Asset Allocation
            st.markdown("#### 📦 Asset Allocation")
            asset_options = tickers.apply(lambda row: format_asset_option(row['Ticker'], row['Company Name']), axis=1).tolist()
            selected_assets_new = st.multiselect(
                "🗂️ Select Assets:",
                options=asset_options,
                help="Choose the assets you want to include in your portfolio."
            )
            selected_tickers_new = [option.split(' - ')[0] for option in selected_assets_new if ' - ' in option]

            allocations_new = {}
            for ticker in selected_tickers_new:
                allocations_new[ticker] = st.number_input(
                    f"📊 {ticker} Allocation (%)",
                    min_value=0.0,
                    max_value=100.0,
                    value=0.0,
                    step=0.000001,
                    format="%.6f",
                    key=f"alloc_new_{ticker}",
                    help=f"Set the allocation percentage for {ticker}. The sum of all allocations must equal 100%."
                )

            total_allocation_new = sum(allocations_new.values())
            st.markdown(f"**🧮 Total Allocation:** {total_allocation_new:.6f}%")

            # Display warning only if allocations have been entered and do not sum to 100%
            if any(alloc > 0 for alloc in allocations_new.values()) and not np.isclose(total_allocation_new, 100.0, atol=1e-4):
                st.warning("⚠️ Allocations must sum to 100%. Please adjust the allocations.")

            # Submit Button
            if st.button("✅ Add Portfolio"):
                if not name:
                    st.error("❌ Please provide a portfolio name.")
                elif name in [p['name'] for p in st.session_state.portfolios]:
                    st.error("❌ Portfolio name already exists. Please choose a unique name.")
                elif not selected_assets_new:
                    st.error("❌ Please select at least one asset.")
                elif not np.isclose(total_allocation_new, 100.0, atol=1e-4):
                    st.error(f"❌ Allocations must sum to 100%. Currently sum to {total_allocation_new:.6f}%.")
                else:
                    new_portfolio = {
                        'name': name,
                        'start_date': pd.to_datetime(start_date_input),
                        'end_date': pd.to_datetime(end_date_input),
                        'rf_rate': rf_rate,
                        'broker_fee': broker_fee,
                        'benchmark_symbol': benchmark_symbol,
                        'rebalance_freq': frequency_mapping.get(rebalance_freq, 'M'),
                        'selected': selected_tickers_new,
                        'allocations': list(allocations_new.values())
                    }
                    st.session_state.portfolios.append(new_portfolio)
                    st.success(f"🎉 Portfolio '{name}' added successfully!")
                    st.balloons()
                    st.json({ticker: f"{alloc:.6f}%" for ticker, alloc in allocations_new.items()})

    # ----------------------------
    # Documentation
    # ----------------------------
    with st.expander("📖 Documentation"):  
        st.markdown("""
        ### Documentation

        **Portfolio Configuration:**
        - **Portfolio Name:** Enter a unique name for your portfolio.
        - **Risk-Free Rate:** The theoretical return of an investment with zero risk, often based on government bonds.
        - **Sharpe Ratio:** Measures the performance of an investment compared to a risk-free asset, after adjusting for its risk.
            *Formula:* $$(R_p - R_f) / \sigma_p$$
        - **VaR (Value at Risk):** Estimates the maximum potential loss over a specific time frame at a given confidence level.
        - **CVaR (Conditional Value at Risk):** The expected loss exceeding the VaR, providing insight into tail risk.
        - **Sortino Ratio:** Similar to the Sharpe Ratio but only penalizes downside volatility.
            *Formula:* $$(R_p - R_f) / \sigma_d$$
        - **Maximum Drawdown:** The largest peak-to-trough decline in the portfolio's value over a specific period.
        - **Alpha:** Measures the active return on an investment compared to a market index.
            *Formula:* $$R_p - [R_f + \beta (R_m - R_f)]$$
        - **Beta:** Indicates the volatility of an investment relative to the market.
            *Formula:* $$\beta = \frac{Cov(R_p, R_m)}{Var(R_m)}$$
        - **Information Ratio:** Measures portfolio returns beyond the returns of a benchmark, adjusted for the volatility of those returns.
            *Formula:* $$(R_p - R_b) / TE$$
        - **Gain/Loss Ratio:** The ratio of total gains to total losses in the portfolio.
        - **Modigliani–Modigliani Measure (M²):** Adjusts the portfolio return to the risk of a benchmark, allowing for comparison.
            *Formula:* $$M² = \frac{\text{Sharpe Ratio}_{\text{Portfolio}}}{\text{Sharpe Ratio}_{\text{Benchmark}}} \times \sigma_{\text{Benchmark}}$$
        - **Tracking Error:** Measures the standard deviation of the difference between portfolio returns and benchmark returns.
        - **Upside/Downside Capture Ratio:** Measures how well the portfolio captures the benchmark's positive and negative movements respectively.
        - **Risk Factor Attribution:** Decomposes portfolio returns based on exposure to different risk factors like Momentum, Value, and Size.
        - **Performance Attribution:** Breaks down portfolio performance by individual assets, showing each asset's contribution to overall returns.
        - **Recommendation Engine:** Provides suggestions for portfolio adjustments to optimize performance based on current allocations and objectives.
        """)

if current_step == "Run Backtest":
    st.title("📊 Run Backtest")
    if not st.session_state.portfolios:
        st.warning("Please add at least one portfolio to run backtest.")
    else:
        st.subheader("🔍 Select Portfolios to Compare")
        comparison_type = st.selectbox(
            "Comparison Type",
            ["Portfolio vs Benchmark", "Portfolio vs Portfolio"],
            key="comparison_type"
        )

        portfolio_names = [p['name'] for p in st.session_state.portfolios]

        # Dynamic labels based on comparison_type
        portfolio1_label = "Select Portfolio" if comparison_type == "Portfolio vs Benchmark" else "Select First Portfolio"
        portfolio2_disabled = comparison_type == "Portfolio vs Benchmark"

        portfolio1 = st.selectbox(
            portfolio1_label,
            portfolio_names,
            key="p1"
        )

        portfolio2 = st.selectbox(
            "Select Second Portfolio",
            portfolio_names,
            key="p2",
            disabled=portfolio2_disabled
        )

        st.subheader("⚙️ Visualization 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.button("Run Backtest")

    if run:
        with st.spinner("Running backtest..."):
            if comparison_type == "Portfolio vs Benchmark":
                portfolio = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)
                result = run_portfolio_vs_benchmark(portfolio)
            else:
                if portfolio1 == portfolio2:
                    st.error("Please select two different portfolios for comparison.")
                    st.stop()
                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)
                result = run_portfolio_vs_portfolio(portfolio_a, portfolio_b)

            if result.get("error"):
                st.error(result["error"])
            else:
                backtest_results = result["backtest_results"]
                display_backtest_results(backtest_results, comparison_type, portfolio1, portfolio2)

if current_step == "Optimize Portfolio":
    st.title("🔧 Optimize Portfolio")
    if st.session_state.portfolios:
        portfolio_names = [p['name'] for p in st.session_state.portfolios]
        selected_portfolio = st.selectbox("Select Portfolio to Optimize", portfolio_names)

        portfolio = next(p for p in st.session_state.portfolios if p['name'] == selected_portfolio)
        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:
                returns = price_data[available_selected].pct_change().dropna()
                st.markdown("### 🛠️ Set Optimization Constraints")
                # Interactive Sliders
                max_weight = st.slider(
                    "Maximum Weight per Asset (%)",
                    min_value=0.0,
                    max_value=100.0,
                    value=100.0,
                    step=0.1,
                    help="Set the maximum allocation percentage for any single asset."
                ) / 100  # Convert to decimal

                min_weight = st.slider(
                    "Minimum Weight per Asset (%)",
                    min_value=0.0,
                    max_value=100.0,
                    value=0.0,
                    step=0.1,
                    help="Set the minimum allocation percentage for any single asset."
                ) / 100  # Convert to decimal

                # **Added Validation Check**
                if min_weight > max_weight:
                    st.error("Minimum weight cannot exceed maximum weight.")
                    st.stop()  # Prevent further execution

                # **Added: Multi-Objective Selection**
                objective_options = [
                    "Sharpe Ratio",
                    "Minimum Variance",
                    "Maximum Return",
                    "Minimum Drawdown",
                    "Maximize Alpha",
                    "Minimize Beta"
                ]
                selected_objectives = st.multiselect(
                    "Select Optimization Objectives",
                    options=objective_options,
                    default=["Sharpe Ratio"],
                    help="Choose one or more objectives to optimize your portfolio."
                )

                target_return = st.number_input(
                    "Target Annual Return (%)",
                    min_value=0.0,
                    max_value=100.0,
                    value=10.0,
                    step=0.1,
                    help="Set the target annual return for portfolio optimization."
                ) / 100  # Convert to decimal

                submitted_optimize = st.button("Run Optimization")

                if submitted_optimize:
                    if not selected_objectives:
                        st.error("Please select at least one optimization objective.")
                    objective_map = {
                        'Sharpe Ratio': 'sharpe',
                        'Minimum Variance': 'min_variance',
                        'Maximum Return': 'max_return',
                        'Minimum Drawdown': 'min_drawdown',
                        'Maximize Alpha': 'maximize_alpha',       # Added entry
                        'Minimize Beta': 'minimize_beta'          # Added entry
                    }
                    objectives_selected = [objective_map[obj] for obj in selected_objectives]
                    with st.spinner("Optimizing portfolio..."):
                        # Download benchmark data
                        benchmark_symbol = portfolio['benchmark_symbol']
                        benchmark_data = download_data([benchmark_symbol], portfolio['start_date'], portfolio['end_date'])
                        if benchmark_data.empty:
                            st.error(f"Benchmark symbol '{benchmark_symbol}' does not have available data for the chosen period.")
                            st.stop()
                        else:
                            benchmark_returns = benchmark_data[benchmark_symbol].pct_change().dropna()
                            benchmark_returns = benchmark_returns.reindex(returns.index, method='ffill').dropna()

                        # Call optimize_portfolio with benchmark_returns
                        optimized_weights = optimize_portfolio(
                            returns,
                            benchmark_returns=benchmark_returns,  # Passed as a new argument
                            objectives=objectives_selected,
                            rf=portfolio['rf_rate'],
                            max_weight=max_weight,
                            min_weight=min_weight,
                            target_return=target_return
                        )
                        if optimized_weights is not None:
                            # **Added Allocation Sum Check**
                            total_allocation = np.sum(optimized_weights)
                            if not np.isclose(total_allocation, 1.0, atol=1e-4):
                                st.warning(f"Optimized allocations sum to {total_allocation*100:.2f}%. Adjusting proportionally.")
                                optimized_weights = optimized_weights / total_allocation

                            st.success("Optimization completed successfully!")

                            # Display Optimized Weights
                            st.subheader("📈 Optimized Weights")
                            weights_df = pd.DataFrame(optimized_weights, index=available_selected, columns=["Weight"])
                            weights_df['Weight'] = weights_df['Weight'].apply(lambda x: f"{x:.2%}")
                            st.table(weights_df)

                            # Plot Allocation Pie Chart
                            plot_allocation_pie(optimized_weights, available_selected, title="Optimized Portfolio Allocation")

                            # Display Performance Metrics
                            st.subheader("📈 Optimized Portfolio Performance Metrics")
                            optimized_return = np.dot(optimized_weights, returns.mean()) * 252
                            optimized_volatility = np.sqrt(np.dot(optimized_weights.T, np.dot(returns.cov() * 252, optimized_weights)))
                            optimized_sharpe = (optimized_return - portfolio['rf_rate']) / optimized_volatility if optimized_volatility > 1e-6 else np.nan

                            optimized_metrics = {
                                'Expected Annual Return (%)': f"{optimized_return * 100:.2f}%",
                                'Annualized Volatility (%)': f"{optimized_volatility * 100:.2f}%",
                                'Sharpe Ratio': f"{optimized_sharpe:.2f}"
                            }

                            optimized_metrics_df = pd.DataFrame(list(optimized_metrics.items()), columns=['Metric', 'Value'])
                            st.table(optimized_metrics_df)

                            # Store optimized weights and metrics in session state
                            st.session_state.backtest_results.update({
                                'optimized_weights': optimized_weights,
                                'optimized_return': optimized_return,
                                'optimized_volatility': optimized_volatility,
                                'optimized_sharpe': optimized_sharpe
                            })
                        else:
                            st.error("Optimization did not return any weights.")
    else:
        st.warning("Please add at least one portfolio to optimize.")

if current_step == "Monte Carlo Simulations":
    st.title("📈 Monte Carlo Simulations")
    if st.session_state.portfolios:
        portfolio_names = [p['name'] for p in st.session_state.portfolios]
        selected_portfolio = st.selectbox("Select Portfolio for Monte Carlo Simulations", portfolio_names)
        if selected_portfolio:
            portfolio = next(p for p in st.session_state.portfolios if p['name'] == selected_portfolio)
            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.")
                else:
                    returns = price_data[available_selected].pct_change().dropna()

                    # Organize simulation settings and results into tabs
                    simulation_tabs = st.tabs(["🔧 Settings", "📊 Results"])

                    with simulation_tabs[0]:
                        st.subheader("🔍 Select Simulation Parameters")
                        with st.form("mc_simulation_form"):
                            # Simulation Settings Organized in Columns
                            sim_col1, sim_col2 = st.columns(2)

                            with sim_col1:
                                num_simulations = st.number_input("Number of Simulations", min_value=1, max_value=10000, value=1000)
                                periods = st.number_input("Number of Periods", min_value=1, max_value=252, value=252)

                            with sim_col2:
                                return_distribution = st.selectbox(
                                    "Return Distribution",
                                    options=["normal", "log-normal"],
                                    index=0,
                                    help="Select the return distribution to use in simulations."
                                )
                                enable_mean_reversion = st.checkbox("Enable Mean Reversion")
                                if enable_mean_reversion:
                                    mean_reversion_speed = st.slider("Mean Reversion Speed", min_value=0.0, max_value=1.0, value=0.1)
                                    long_term_mean_input = st.number_input("Long-term Mean Return (%)", value=0.0) / 100
                                    long_term_mean = np.full(len(available_selected), long_term_mean_input / 252)
                                else:
                                    mean_reversion_speed = 0.0
                                    long_term_mean = None

                            # Additional Simulation Settings
                            st.subheader("⚙️ Additional Settings")
                            time_varying_vol = st.checkbox("Enable Time-Varying Volatility")
                            if time_varying_vol:
                                vol_change_rate = st.number_input("Volatility Change Rate per Period", value=0.0) / np.sqrt(252)
                            else:
                                vol_change_rate = 0.0

                            # Stress Testing Settings
                            st.subheader("⚠️ Stress Testing")
                            enable_stress_testing = st.checkbox("Enable Stress Testing")
                            stress_shocks = {}
                            if enable_stress_testing:
                                selected_stress_assets = st.multiselect(
                                    "Select Assets to Apply Stress Shocks",
                                    options=available_selected,
                                    help="Choose assets to apply stress shocks."
                                )
                                for ticker in selected_stress_assets:
                                    shock = st.number_input(
                                        f"Shock to {ticker} (%)",
                                        min_value=-100.0, max_value=100.0,
                                        value=0.0,
                                        step=0.1,
                                        key=f"shock_{ticker}"
                                    ) / 100  # Convert to decimal
                                    stress_shocks[ticker] = shock

                            submitted_simulation = st.form_submit_button("Run Simulations")

                        if submitted_simulation:
                            with st.spinner("Running Monte Carlo simulations..."):
                                simulated_returns = monte_carlo_simulation(
                                    returns, num_simulations=num_simulations, periods=periods,
                                    mean_returns=returns.mean(), cov_matrix=returns.cov(),
                                    mean_reversion=enable_mean_reversion,
                                    mean_reversion_speed=mean_reversion_speed,
                                    long_term_mean=long_term_mean,
                                    time_varying_vol=time_varying_vol,
                                    vol_change_rate=vol_change_rate,
                                    stress_shocks=stress_shocks,
                                    stress_period=1,  # Example value
                                    return_distribution=return_distribution
                                )
                                st.session_state.simulated_returns = simulated_returns
                                st.success("Simulations completed successfully!")

                    with simulation_tabs[1]:
                        if 'simulated_returns' in st.session_state:
                            simulated_returns = st.session_state.simulated_returns
                            st.subheader("📊 Simulation Results")

                            # Display Basic Statistics
                            st.write("### Descriptive Statistics")
                            st.write(pd.Series(simulated_returns).describe())

                            # Histogram of Simulated Returns
                            st.write("### Returns Distribution")
                            fig_hist = px.histogram(
                                simulated_returns,
                                nbins=50,
                                title='Simulated Returns Distribution',
                                labels={'value': 'Simulated Returns', 'count': 'Frequency'},
                                template='plotly_dark'
                            )
                            st.plotly_chart(fig_hist, use_container_width=True)

                            # Cumulative Distribution Function (CDF)
                            st.write("### Cumulative Distribution Function (CDF)")
                            fig_cdf = px.ecdf(
                                simulated_returns,
                                title='Cumulative Distribution of Simulated Returns',
                                labels={'value': 'Simulated Returns', 'cumcount': 'CDF'},
                                template='plotly_dark'
                            )
                            st.plotly_chart(fig_cdf, use_container_width=True)

                            # Box Plot for Simulated Returns
                            st.write("### Box Plot of Simulated Returns")
                            fig_box = px.box(
                                pd.DataFrame(simulated_returns, columns=['Returns']),
                                y='Returns',
                                title='Box Plot of Simulated Returns',
                                template='plotly_dark'
                            )
                            st.plotly_chart(fig_box, use_container_width=True)

                            # Summary Statistics Table
                            st.write("### Summary Statistics")
                            summary_stats = pd.DataFrame({
                                'Statistic': ['Mean', 'Median', 'Standard Deviation', 'Minimum', 'Maximum'],
                                'Value': [
                                    f"{np.mean(simulated_returns):.4f}",
                                    f"{np.median(simulated_returns):.4f}",
                                    f"{np.std(simulated_returns):.4f}",
                                    f"{np.min(simulated_returns):.4f}",
                                    f"{np.max(simulated_returns):.4f}"
                                ]
                            })
                            st.table(summary_stats)
                            st.markdown("""
                            **Interpretation:**
                            - **Descriptive Statistics:** Provides an overview of the distribution of simulated portfolio returns.
                            - **Returns Distribution Histogram:** Visualizes the frequency of different return outcomes.
                            - **Cumulative Distribution Function (CDF):** Shows the probability that a return is less than or equal to a particular value.
                            - **Box Plot:** Highlights the median, quartiles, and potential outliers in the return distribution.
                            - **Summary Statistics Table:** Summarizes key metrics from the simulation runs.
                            """)
                        else:
                            st.info("Run simulations to view results.")
    else:
        st.warning("Please add at least one portfolio to run Monte Carlo simulations.")

# Main Risk Analysis Section
if current_step == "Risk Analysis":
    st.title("⚠️ Risk Analysis")
    if st.session_state.portfolios:
        portfolio_names = [p['name'] for p in st.session_state.portfolios]
        selected_portfolio = st.selectbox("Select Portfolio for Risk Analysis", portfolio_names)
        if selected_portfolio:
            portfolio = next(p for p in st.session_state.portfolios if p['name'] == selected_portfolio)
            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']).difference(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.")
                else:
                    returns = price_data[available_selected].pct_change().dropna()

                    # Organize risk analysis settings and outputs into tabs
                    risk_tabs = st.tabs(["📉 Risk Metrics", "📈 Visualizations", "🛠️ Sensitivity Analysis"])

                    with risk_tabs[0]:
                        st.subheader("🔍 Risk Metrics")
                        
                        st.subheader("Risk Analysis Parameters")
                        confidence_level = st.slider("Confidence Level", min_value=0.01, max_value=0.99, value=0.95)

                        # Enhanced Stress Testing Inputs with Asset Selection
                        st.subheader("⚠️ Enhanced Stress Testing")
                        enable_stress_testing = st.checkbox("Enable Enhanced Stress Testing")
                        stress_shocks = {}

                        if enable_stress_testing:
                            selected_stress_assets = st.multiselect(
                                "Select Assets to Stress Test",
                                options=available_selected,
                                help="Choose multiple assets to apply simultaneous stress shocks."
                            )
                            for ticker in selected_stress_assets:
                                shock = st.number_input(
                                    f"Shock to {ticker} (%)",
                                    min_value=-100.0, max_value=100.0,
                                    value=0.0,
                                    step=0.1,
                                    key=f"enhanced_shock_{ticker}"
                                ) / 100  # Convert to decimal
                                stress_shocks[ticker] = shock
                            if not selected_stress_assets:
                                st.warning("No assets selected for stress testing.")

                        # Sensitivity Analysis Inputs
                        st.subheader("🔎 Sensitivity Analysis")
                        enable_sensitivity_analysis = st.checkbox("Enable Sensitivity Analysis")
                        sensitivity_adjustments = {}
                        if enable_sensitivity_analysis:
                            interest_rate_change = st.number_input("Change in Interest Rates (bps)", value=0.0) / 10000
                            inflation_rate_change = st.number_input("Change in Inflation Rates (bps)", value=0.0) / 10000
                            for ticker in available_selected:
                                interest_sensitivity = st.number_input(f"Interest Rate Sensitivity for {ticker}", value=1.0, key=f"interest_sens_{ticker}")
                                inflation_sensitivity = st.number_input(f"Inflation Rate Sensitivity for {ticker}", value=1.0, key=f"inflation_sens_{ticker}")
                                adjustment = (interest_sensitivity * interest_rate_change +
                                              inflation_sensitivity * inflation_rate_change)
                                sensitivity_adjustments[ticker] = adjustment

                        # Cross-Asset Sensitivity Analysis Inputs
                        st.subheader("🔄 Cross-Asset Sensitivity Analysis")
                        enable_cross_asset_sensitivity_analysis = st.checkbox("Enable Cross-Asset Sensitivity Analysis")
                        cross_asset_sensitivity_adjustments = {}

                        if enable_cross_asset_sensitivity_analysis:
                            st.markdown("### Currency Sensitivity")
                            currency_rate_change = st.number_input(
                                "Change in Currency Exchange Rate (%)",
                                min_value=-50.0, max_value=50.0,
                                value=0.0,
                                step=0.1,
                                help="Enter the percentage change in currency exchange rates."
                            ) / 100  # Convert to decimal

                            st.markdown("### Commodity Price Sensitivity")
                            commodity_price_change = st.number_input(
                                "Change in Commodity Prices (%)",
                                min_value=-100.0, max_value=100.0,
                                value=0.0,
                                step=0.1,
                                help="Enter the percentage change in commodity prices."
                            ) / 100  # Convert to decimal

                            for ticker in available_selected:
                                sensitivity = st.number_input(
                                    f"Sensitivity for {ticker} to Currency and Commodity Changes (%)",
                                    min_value=-10.0, max_value=10.0,
                                    value=0.0,
                                    step=0.1,
                                    key=f"sensitivity_{ticker}"
                                ) / 100  # Convert to decimal
                                # Combine currency and commodity changes
                                adjustment = sensitivity * (currency_rate_change + commodity_price_change)
                                cross_asset_sensitivity_adjustments[ticker] = adjustment

                        # Submit Button
                        if st.button("Calculate Risk Metrics"):
                            # Risk metrics calculation logic...
                            with st.spinner("Calculating risk metrics..."):
                                # Calculate portfolio returns as weighted average
                                weights = np.array(portfolio['allocations']) / 100
                                portfolio_returns = returns.copy()

                                # Apply stress shocks if enabled
                                if enable_stress_testing:
                                    for ticker in stress_shocks:
                                        portfolio_returns[ticker] += stress_shocks[ticker]

                                # Apply sensitivity adjustments if enabled
                                if enable_sensitivity_analysis:
                                    for ticker in sensitivity_adjustments:
                                        portfolio_returns[ticker] += sensitivity_adjustments[ticker]

                                # Apply cross-asset sensitivity adjustments if enabled
                                if enable_cross_asset_sensitivity_analysis:
                                    for ticker in cross_asset_sensitivity_adjustments:
                                        portfolio_returns[ticker] += cross_asset_sensitivity_adjustments[ticker]

                                # Ensure no extreme negative returns after adjustments
                                portfolio_returns = portfolio_returns.clip(lower=-1.0)

                                # Calculate weighted portfolio returns
                                portfolio_returns = portfolio_returns.dot(weights)
                                st.session_state.portfolio_returns = portfolio_returns
                                portfolio_skewness = skew(portfolio_returns)
                                portfolio_kurtosis = kurtosis(portfolio_returns)

                                # Calculate VaR and CVaR
                                var = calculate_var(portfolio_returns, confidence_level)
                                cvar = calculate_cvar(portfolio_returns, confidence_level)
                                st.session_state.var = var
                                st.session_state.cvar = cvar

                                # Risk Metrics Summary Table
                                st.markdown("### 📈 Risk Metrics Summary")
                                cum_returns = (1 + portfolio_returns).cumprod()
                                risk_metrics = {
                                    'Value at Risk (VaR)': f"{var:.2f}",
                                    'Conditional Value at Risk (CVaR)': f"{cvar:.2f}",
                                    'Skewness': f"{portfolio_skewness:.2f}",
                                    'Kurtosis': f"{portfolio_kurtosis:.2f}",
                                    'Annualized Volatility (%)': f"{portfolio_returns.std() * 100 * np.sqrt(252):.2f}%",
                                    'Maximum Drawdown (%)': f"{drawdown(cum_returns) * 100:.2f}%"
                                }
                                risk_df = pd.DataFrame(list(risk_metrics.items()), columns=['Metric', 'Value'])
                                st.table(risk_df)
                                st.markdown("""
                                **Tooltip:**
                                - **VaR:** Measures the worst expected loss under normal market conditions over a specific time period at a given confidence level.
                                - **CVaR:** Provides the average loss exceeding the VaR, offering insight into tail risk.
                                - **Annualized Volatility:** Indicates the degree of variation in portfolio returns, representing risk.
                                - **Maximum Drawdown:** Shows the largest peak-to-trough decline, indicating potential risk exposure.
                                """)
                                # Real-Time Risk Warning
                                high_var_threshold = -0.10  # Example: VaR worse than -10%
                                if var <= high_var_threshold:
                                    st.warning(f"⚠️ High VaR! Your portfolio has a {confidence_level*100:.0f}% chance of losing over {abs(var)*100:.2f}% in the next year.")
                                else:
                                    st.success(f"✅ VaR is within acceptable limits: {var*100:.2f}%")

                    with risk_tabs[1]:
                        st.subheader("📊 Visualizations")

                        var = st.session_state.get('var', None)
                        cvar = st.session_state.get('cvar', None)
                        portfolio_returns_local = st.session_state.get('portfolio_returns', None)

                        if var is not None and cvar is not None and portfolio_returns_local is not None:

                            if portfolio_returns_local.empty:
                                st.error("Portfolio returns data is unavailable. Please ensure calculations are correct.")
                                st.stop()

                            # Value at Risk (VaR) Plot (Assuming you want to show cumulative returns over time)
                            st.subheader("Value at Risk (VaR) Plot")
                            fig_var = px.line(
                                portfolio_returns_local.cumsum(),
                                title='Value at Risk (VaR) Over Time',
                                labels={'value': 'Cumulative Returns', 'index': 'Date'}
                            )
                            fig_var.add_hline(y=var, line_dash="dash", line_color="red", annotation_text=f"VaR: {var:.2f}", annotation_position="bottom right")
                            st.plotly_chart(fig_var, use_container_width=True)

                            # Conditional Value at Risk (CVaR) Plot
                            st.subheader("Conditional Value at Risk (CVaR) Plot")
                            fig_cvar = px.line(portfolio_returns_local.cumsum(), title='Conditional Value at Risk (CVaR) Over Time', labels={'value': 'Cumulative Returns', 'index': 'Date'})
                            fig_cvar.add_hline(y=cvar, line_dash="dash", line_color="blue", annotation_text=f"CVaR: {cvar:.2f}", annotation_position="bottom right")
                            st.plotly_chart(fig_cvar, use_container_width=True)

                            # VaR and CVaR Distribution Plot
                            st.subheader("📊 VaR and CVaR Distribution")
                            fig_var_cvar_dist = px.histogram(
                                portfolio_returns_local,
                                nbins=50,
                                title='Returns Distribution with VaR and CVaR',
                                labels={'value': 'Returns', 'count': 'Frequency'}
                            )
                            fig_var_cvar_dist.add_vline(x=var, line_dash="dash", line_color="red", annotation_text=f"VaR: {var:.2f}", annotation_position="top left")
                            fig_var_cvar_dist.add_vline(x=cvar, line_dash="dash", line_color="blue", annotation_text=f"CVaR: {cvar:.2f}", annotation_position="top left")
                            st.plotly_chart(fig_var_cvar_dist, use_container_width=True)
                            st.markdown("""
                            **Interpretation:**
                            - **VaR (Value at Risk):** Indicates the maximum expected loss over a specified period at a given confidence level.
                            - **CVaR (Conditional Value at Risk):** Represents the average loss exceeding the VaR, providing insights into tail risk.
                            - **Annualized Volatility:** Measures the variability of portfolio returns, indicating risk.
                            - **Maximum Drawdown:** Shows the largest peak-to-trough decline, reflecting potential risk exposure.
                            """)

                            st.markdown("""
                            **VaR and CVaR Plots:**
                            - **VaR Plot:** Highlights the point below which a certain percentage of losses fall.
                            - **CVaR Plot:** Shows the average loss in scenarios where losses exceed the VaR.
                            """)
                        else:
                            st.info("Run the Risk Metrics calculation to display VaR and CVaR plots.")

                    with risk_tabs[2]:
                        st.subheader("🛠️ Sensitivity Analysis")
                        # Only plot the Adjusted Returns Distribution if stress testing or sensitivity analysis is enabled
                        if enable_stress_testing or enable_sensitivity_analysis or enable_cross_asset_sensitivity_analysis:
                            st.subheader("📊 Adjusted Returns Distribution")
                            fig_adjusted_returns = px.histogram(portfolio_returns, nbins=50, title='Adjusted Portfolio Returns Distribution',
                                                                labels={'value': 'Returns', 'count': 'Frequency'})
                            st.plotly_chart(fig_adjusted_returns, use_container_width=True)
                            st.markdown("**Interpretation:** This histogram shows how the portfolio returns distribution changes after applying stress shocks and sensitivity adjustments.")

                        # Risk vs Return Scatter Plot
                        st.subheader("📊 Risk vs Return Scatter Plot")
                        risk = returns.std() * np.sqrt(252)
                        ret = returns.mean() * 252

                        if isinstance(risk, (float, int)):
                            scatter_df = pd.DataFrame({
                                'Risk': [risk],
                                'Return': [ret]
                            })
                        else:
                            scatter_df = pd.DataFrame({
                                'Risk': risk,
                                'Return': ret
                            })

                        if isinstance(returns, pd.DataFrame) and returns.shape[1] > 1:
                            scatter_df['Asset'] = available_selected

                        fig_risk_return = px.scatter(scatter_df, x='Risk', y='Return',
                                                     title='Risk vs Return Scatter Plot',
                                                     labels={'Risk': 'Risk (Std Dev)', 'Return': 'Return'},
                                                     hover_data=['Asset'] if isinstance(returns, pd.DataFrame) and returns.shape[1] > 1 else [])
                        st.plotly_chart(fig_risk_return, use_container_width=True)

    else:
        st.warning("Please add at least one portfolio to run risk analysis.")