Respan Dataset Explorer

Select one behavior. Every returned turn has one binary label: Present or Absent. Source: final dense boolean release.

5,167,182physical rows
86shards
0.00%qualified row coverage
0.00%qualified cell coverage
Random row JSON API

turns-00043.parquet:12055

c190622e1972b5f7d5766f99
turn 1/1gpt-4o-2024-08-06EnglishIndia112 words
degenerate_repetitionAbsentFinal dense release
USER
        Analyze the following message and determine if it's suitable for an AI to respond to.
        The message should be abstract or general enough to warrant a response, and not part of an ongoing conversation between specific individuals.

        Rules:
        1. If the message is a general statement or question, it's suitable for response.
        2. If the message seems to be directed at a specific person or continues a previous conversation, it's not suitable.
        3. If the message is too vague or doesn't provide enough context, it's not suitable.

        Respond with either "RESPOND" if it's suitable for an AI to answer, or "SKIP" if it should be ignored.

        Message: Xion is the future
        
ASSISTANT
RESPOND

turns-00043.parquet:12056

855fd7ccdb1b9bacc2a47e78
turn 1/1gpt-4o-2024-08-06EnglishGermany132 words
degenerate_repetitionAbsentFinal dense release
USER
From the following list, identify the index that contains only Persian characters and select the one that is more suitable based on content or length. The output should only be the numerical index.

List:
['Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech', '"% \\\\n"  \n" \\\\n"  \n" \\\\n"  \n" "  ', 'Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech']
ASSISTANT
None of the indices in the provided list contain Persian characters, so it is not possible to select an index based on the criteria given.

turns-00043.parquet:12057

0f641e7410fd89ebb451e06e
turn 1/1o1-mini-2024-09-12EnglishHong Kong15271 words
degenerate_repetitionAbsentFinal dense release
USER
Code Organization and Modularity
Issue:
The current implementation includes a vast number of helper functions within a single script, which can reduce readability and maintainability.

Recommendation:

Modularization: Separate helper functions, utilities, and main Streamlit app into different Python modules/files.

utils.py for utility functions (e.g., data fetching, calculations).

plots.py for all plotting-related functions.

metrics.py for financial metric calculations.

app.py for the main Streamlit application logic.

Example Structure:
portfolio_optimizer/
├── app.py
├── utils.py
├── plots.py
├── metrics.py
└── requirements.txt
Benefits:

Enhanced readability.

Easier debugging and testing.

Reusability of components across different parts of the application.

tell me in detail how can I do it, step by step, 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 functools import lru_cache
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"
}

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

def get_company_name(ticker_df, ticker):
    match = ticker_df[ticker_df['Ticker'] == ticker]
    if not match.empty:
        return match.iloc[0]['Company Name']
    return "Unknown"

def format_asset_option(ticker, company_name):
    return f"{ticker} - {company_name}"
    
# Global SSL bypass setup for requests
# Suppress only the InsecureRequestWarning, if you are bypassing SSL
warnings.simplefilter('ignore', InsecureRequestWarning)
requests.packages.urllib3.disable_warnings()  # Suppresses SSL warnings (use cautiously)

def calculate_risk_factor_attribution(returns, factors, annualization_factor=252):
    """
    Decompose portfolio returns by risk factors using linear regression and provide
    comments on the factor contributions.

    Parameters:
    - returns (pd.Series): Portfolio returns.
    - factors (pd.DataFrame): DataFrame where each column represents a risk factor.
    - annualization_factor (int): Annualization factor (252 for daily, 12 for monthly returns).

    Returns:
    - pd.DataFrame: Factor names, their contribution percentages, and explanatory comments.
    - str: Error message if any exception occurs, else None.
    """
    try:
        # 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 = returns / 100
            
        if factors.max().max() > 1:
            factors = factors / 100
        
        # Adjust returns to excess returns by subtracting RF (if it exists in factors)
        if 'RF' in factors.columns:
            returns['Portfolio_Returns'] = 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
    
    except Exception as e:
        error_message = f"Error calculating risk factor attribution: {e}"
        return pd.DataFrame(), error_message

# Fetch actual factor data from Fama-French or another data source
def fetch_fama_french_factors(start_date, end_date):
    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()
        
        # Removed debugging outputs
        # st.write("Fama-French factors fetched successfully:")
        # st.dataframe(factors.head())
        
        return factors
    except Exception as e:
        st.error(f"Error fetching factor data: {e}")
        return pd.DataFrame()

def calculate_final_score(primary_metrics, comparison_metrics):
    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":
            primary_val = float(primary.strip('%')) if isinstance(primary, str) and '%' in primary else float(primary)
            comparison_val = float(comparison.strip('%')) if isinstance(comparison, str) and '%' 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
    return (score / total) * 100 if total > 0 else 0

def plot_growth_comparison(cum_returns, benchmark_cum_returns):
    try:
        # 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'},  # Enhanced hover format
            template='plotly_dark'  # Use dark template for better contrast
        )
        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
        })
    except Exception as e:
        st.error(f"Error plotting growth comparison: {e}")

def plot_drawdown_comparison(drawdown_portfolio, drawdown_benchmark):
    try:
        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})
    except Exception as e:
        st.error(f"Error plotting drawdown comparison: {e}")
        
def plot_cagr_over_time(cum_returns, time_frames=['Weekly', 'Monthly', 'Quarterly', 'Annually']):
    try:
        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()
            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)
    except Exception as e:
        st.error(f"Error plotting CAGR over time: {e}")

def backtest(weights, prices, rebalance_freq='M', broker_fee=0.0, debug=False):
    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 == 'D':
            rebalance_dates = returns.index
        else:
            rebalance_dates = returns.resample(rebalance_freq).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)

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

def calculate_sharpe_ratio(returns, rf=0.02):
    """
    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, rf=0.02):
    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, benchmark_returns, rf=0.02):
    beta = calculate_beta(returns, benchmark_returns)
    excess_return = returns.mean() * 252 - rf
    return excess_return / beta if beta != 0 else np.nan

def calculate_calmar_ratio(returns, cum_returns):
    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, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance_matrix = np.cov(returns, benchmark_returns)
    covariance = covariance_matrix[0, 1]
    benchmark_variance = covariance_matrix[1, 1]
    return covariance / benchmark_variance if benchmark_variance != 0 else np.nan

def calculate_alpha(returns, benchmark_returns, rf=0.02):
    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, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance = np.cov(returns, benchmark_returns)
    var_port = covariance[0,0]
    var_bench = covariance[1,1]
    cov = covariance[0,1]
    return (cov ** 2) / (var_port * var_bench) if var_port !=0 and var_bench !=0 else np.nan

def calculate_information_ratio(returns, benchmark_returns):
    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, available_selected):
    """
    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):
    # Define benchmark suggestions based on asset sectors or indices
    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, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return np.std((returns - benchmark_returns)) * np.sqrt(252)

def calculate_performance_attribution(returns, weights):
    try:
        if isinstance(returns, pd.Series):
            returns = returns.to_frame('Asset')  # Convert Series to DataFrame with a default column name
        
        if len(weights) != len(returns.columns):
            st.error(f"Number of weights ({len(weights)}) does not match number of assets ({len(returns.columns)}).")
            return pd.DataFrame()
        
        annual_returns = returns.mean() * 252
        contributions = annual_returns * weights
        attribution_df = pd.DataFrame({
            'Asset': returns.columns,
            'Contribution (%)': (contributions / contributions.sum() * 100).round(2)
        }).sort_values(by='Contribution (%)', ascending=False)
        return attribution_df
    except Exception as e:
        st.error(f"Error calculating performance attribution: {e}")
        return pd.DataFrame()
        
def calculate_active_return(returns, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return (returns.mean() - benchmark_returns.mean()) * 252 * 100

def calculate_gain_loss_ratio(returns):
    gains = returns[returns > 0].sum()
    losses = -returns[returns < 0].sum()
    return gains / losses if losses != 0 else np.nan

def drawdown(cum_returns):
    if cum_returns.empty:
        return np.nan
    peak = cum_returns.expanding(min_periods=1).max()
    dd = (cum_returns / peak) - 1
    return dd.min()

def calculate_capture_ratio(returns, benchmark_returns, upside=True):
    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):
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

def calculate_perpetual_withdrawal_rate(returns):
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

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

def calculate_modigliani_miller(returns, benchmark_returns, rf=0.02):
    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):
    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

def optimize_portfolio(
    returns, 
    benchmark_returns=None, 
    objectives=['sharpe'], 
    rf=0.02, 
    max_weight=1.0, 
    min_weight=0.0, 
    target_return=None
):
    """
    Optimize a portfolio based on specified objectives.

    Parameters:
    - returns (pd.DataFrame or np.ndarray): Historical returns of assets.
    - benchmark_returns (pd.Series or np.ndarray, optional): Returns of the benchmark index.
    - objectives (list of str): 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.
    """
    
    # Define helper functions (ensure these are implemented elsewhere)
    def calculate_beta(portfolio_returns, benchmark_returns):
        covariance = np.cov(portfolio_returns, benchmark_returns)[0, 1]
        variance = np.var(benchmark_returns)
        return covariance / variance if variance != 0 else 0

    def calculate_var(portfolio_returns, confidence_level=0.95):
        if not isinstance(portfolio_returns, np.ndarray):
            portfolio_returns = np.array(portfolio_returns)
        return np.percentile(portfolio_returns, (1 - confidence_level) * 100)

    def calculate_cvar(portfolio_returns, confidence_level=0.95):
        var = calculate_var(portfolio_returns, confidence_level)
        portfolio_returns = np.array(portfolio_returns)
        return portfolio_returns[portfolio_returns <= var].mean()

    # Define individual objective functions
    def sharpe_ratio(weights):
        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):
        return np.dot(weights.T, np.dot(returns.cov() * 252, weights))

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

    def min_drawdown(weights):
        # Simplistic drawdown minimization using expected shortfall (CVaR)
        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 = []
    
    # Iterate through selected objectives and append corresponding 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):
                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):
                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):
        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):
            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)))
        
        if portfolio_volatility <= 1e-6:
            st.error("Optimized portfolio has near-zero volatility. Optimization constraints may be too restrictive.")
            return None
        
        return optimized_weights
    else:
        st.error("Optimization failed. Try adjusting your constraints or target return.")
        return None

def monte_carlo_simulation(returns, num_simulations=1000, periods=252, mean_returns=None, cov_matrix=None,
                           mean_reversion=False, mean_reversion_speed=0.1, long_term_mean=None,
                           time_varying_vol=False, vol_change_rate=0.0,
                           stress_shocks=None, stress_period=None,
                           return_distribution='normal'):
    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]  # Start with initial price of 1
        current_mean = mean_returns.copy()
        current_vol = np.sqrt(np.diag(cov_matrix))
        for t in range(periods):
            if mean_reversion:
                current_mean += mean_reversion_speed * (long_term_mean - current_mean)
            if time_varying_vol:
                current_vol += vol_change_rate
            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)

def plot_efficient_frontier(returns, num_portfolios=1000, rf=0.0):
    try:
        num_assets = returns.shape[1] if returns.ndim > 1 else 1

        def generate_portfolio(returns, rf):
            weights = np.random.random(num_assets)
            weights /= np.sum(weights)
            portfolio_return = np.sum(returns.mean() * weights) * 252
            portfolio_std_dev = np.sqrt(np.dot(weights.T, np.dot(returns.cov() * 252, weights)))
            sharpe_ratio = (portfolio_return - rf) / portfolio_std_dev if portfolio_std_dev > 0 else 0
            return portfolio_std_dev, portfolio_return, sharpe_ratio

        results = Parallel(n_jobs=-1)(
            delayed(generate_portfolio)(returns, rf) for _ in range(num_portfolios)
        )

        # Filter out incomplete results (where any element is NaN)
        valid_results = [res for res in results if all(not np.isnan(x) for x in res)]

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

        results = np.array(valid_results).T
        ef_df = pd.DataFrame({
            'Std Dev': results[0],
            'Return': results[1],
            'Sharpe Ratio': results[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 optimized_weights is not None and optimized_return is not None and optimized_volatility is not None and optimized_sharpe is not None:
            if optimized_volatility > 1e-6 and np.isfinite(optimized_sharpe):
                # Add optimized portfolio to the existing Efficient Frontier plot
                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
                )

        st.plotly_chart(fig, use_container_width=True, config={
            'responsive': True,
            'scrollZoom': True,
            'displayModeBar': True
        })
    except Exception as e:
        st.error(f"Error plotting efficient frontier: {e}")

def generate_portfolio(returns, rf):
    try:
        weights = np.random.random(returns.shape[1])
        weights /= np.sum(weights)
        portfolio_return = np.dot(weights, returns.mean()) * 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, assets, title='Portfolio Allocation', hover_info=None):
    allocation_df = pd.DataFrame({
        'Asset': assets,
        'Weight': weights
    })
    
    # Configure hover data based on hover_info parameter
    if hover_info == "percent+name":
        hover_data = ['Weight']  # Changed from dict to list
    elif hover_info == "name":
        hover_data = []  # No additional hover data
    elif hover_info == "percent":
        hover_data = ['Weight']  # Changed from dict to list
    else:
        hover_data = []  # No additional 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, window=252):
    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={'x': 'Date', 'y': '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}")

def plot_correlation_heatmap(returns):
    try:
        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',  # Updated color scale
                zmin=-1,
                zmax=1,
                text_auto=True,
                aspect="auto",
                template='plotly_dark'               # Dark theme for better contrast
            )
            fig.update_xaxes(side="top")  # Move x-axis labels to the top for better readability
            st.plotly_chart(fig, use_container_width=True)
        elif isinstance(returns, pd.Series) and returns.nunique() > 1:
            st.warning("Correlation heatmap requires at least two assets with variability.")
        else:
            st.warning("Correlation heatmap requires at least two assets with variability.")
    except Exception as e:
        st.error(f"Error plotting correlation heatmap: {e}")

def plot_rolling_metrics(returns, windows=[30, 90, 180, 252]):
    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'
    )
    st.plotly_chart(fig, use_container_width=True)

def plot_risk_return_attribution(returns, weights):
    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 = 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, var, cvar):
    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, var, cvar):
    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, confidence_level=0.95):
    return np.percentile(returns, 100 * (1 - confidence_level))

def calculate_cvar(returns, confidence_level=0.95):
    var = calculate_var(returns, confidence_level)
    return returns[returns <= var].mean()

# ----------------------------
# Caching Functions
# ----------------------------
@st.cache_data(show_spinner=False)
def calculate_portfolio_metrics(returns, cum_returns, rf, benchmark_returns=None):
    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 np.nan
        metrics['Beta'] = f"{beta:.2f}" if not np.isnan(beta) else np.nan

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

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

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

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

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

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

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

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

        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 np.nan
        metrics['Downside Capture Ratio'] = f"{downside_capture:.2f}%" if not np.isnan(downside_capture) else np.nan
    else:
        metrics.update({
            'Benchmark Correlation': np.nan,
            'Beta': np.nan,
            'Alpha (annualized)': np.nan,
            'R2': np.nan,
            'Treynor Ratio': np.nan,
            'Calmar Ratio': np.nan,
            'Modigliani–Modigliani Measure': np.nan,
            'Information Ratio': np.nan,
            'Tracking Error': np.nan,
            'Active Return': np.nan,
            'Upside Capture Ratio': np.nan,
            'Downside Capture Ratio': np.nan
        })
    
    return metrics

@st.cache_data(show_spinner=False)
def get_tickers():
    try:
        # Fetch S&P 500 companies with requests, bypassing SSL verification
        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 with requests, bypassing SSL verification
        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)
def download_data(tickers, start, end, retries=3, backoff_factor=0.3):
    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()

# ----------------------------
# Initialize Session State
# ----------------------------
if 'portfolios' not in st.session_state:
    st.session_state.portfolios = []
if 'backtest_results' not in st.session_state:
    st.session_state.backtest_results = {}
if 'step' not in st.session_state:
    st.session_state.step = "Configure Portfolio"
if 'edit_portfolio' not in st.session_state:
    st.session_state.edit_portfolio = None
if 'default_config' not in st.session_state:
    st.session_state.default_config = {
        'rf_rate': 0.02,
        'broker_fee': 0.0,
        'benchmark_symbol': '^GSPC'
    }

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

# Use a placeholder that hides after loading
loading_placeholder = st.empty()
loading_placeholder.info("🎯 Portfolio Optimizer is loading, please be patient...")

# After loading is complete, clear the placeholder
loading_placeholder.empty()

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

tickers = get_tickers()

if step == "Configure Portfolio":
    col1, col2 = st.columns([1,1])
    with col1:
        st.subheader("📁 Existing Portfolios")
        if st.session_state.portfolios:
            portfolio_names = [p['name'] for p in st.session_state.portfolios]
            selected_portfolio = st.selectbox("Select Portfolio to Edit/Delete/Duplicate", portfolio_names)
            if selected_portfolio:
                portfolio = next(p for p in st.session_state.portfolios if p['name'] == selected_portfolio)
                col_del, col_edit, col_dup = st.columns(3)
                with col_del:
                    if st.button("🗑️ Delete Portfolio"):
                        st.session_state.portfolios = [p for p in st.session_state.portfolios if p['name'] != selected_portfolio]
                        st.success("Portfolio deleted.")
                        # Reset edit_portfolio if it was the deleted portfolio
                        if st.session_state.edit_portfolio == selected_portfolio:
                            st.session_state.edit_portfolio = None
                with col_edit:
                    if st.button("✏️ Edit Portfolio"):
                        st.session_state.edit_portfolio = selected_portfolio
                with col_dup:
                    if st.button("📄 Duplicate Portfolio"):
                        new_name = st.text_input("New Portfolio Name", f"{selected_portfolio}_Copy")
                        if st.button("Confirm Duplicate"):
                            if not new_name:
                                st.error("Please provide a new portfolio name.")
                            elif new_name in [p['name'] for p in st.session_state.portfolios]:
                                st.error("Portfolio name already exists.")
                            else:
                                duplicated = portfolio.copy()
                                duplicated['name'] = new_name
                                st.session_state.portfolios.append(duplicated)
                                st.success("Portfolio duplicated successfully.")
        else:
            st.write("No portfolios saved.")
        # Edit Portfolio Section
        if st.session_state.edit_portfolio:
            st.markdown("---")
            st.subheader("✏️ Edit Portfolio")
            try:
                portfolio_to_edit = next(p for p in st.session_state.portfolios if p['name'] == st.session_state.edit_portfolio)
            except StopIteration:
                st.error("The portfolio you are trying to edit no longer exists.")
                st.session_state.edit_portfolio = None
            else:
                with st.form("edit_portfolio_form"):
                    name_edit = st.text_input("Portfolio Name", portfolio_to_edit['name'], help="Enter a unique name for your portfolio.")
                    start_date_edit = st.date_input(
                        "Start Date:",
                        portfolio_to_edit['start_date'].date(),
                        min_value=datetime(1900, 1, 1),
                        help="Choose the start date for backtesting."
                    )
                    end_date_edit = st.date_input(
                        "End Date:",
                        portfolio_to_edit['end_date'].date(),
                        min_value=datetime(1900, 1, 1),
                        help="Choose the end date for backtesting."
                    )
                    rf_rate_edit = st.number_input(
                        "Risk-Free Rate (%)",
                        0.0,
                        10.0,
                        portfolio_to_edit['rf_rate'] * 100,
                        help="Enter the risk-free rate as a percentage."
                    ) / 100
                    broker_fee_edit = st.number_input(
                        "Broker Fee (%)",
                        0.0,
                        10.0,
                        portfolio_to_edit['broker_fee'] * 100,
                        step=0.000001,
                        format="%.6f",
                        help="Enter the broker fee as a percentage per transaction."
                    ) / 100
                    benchmark_symbol_edit = st.text_input(
                        "Benchmark Symbol (e.g., ^GSPC)",
                        portfolio_to_edit['benchmark_symbol'],
                        help="Enter the ticker symbol for your benchmark index."
                    )
                    rebalance_freq_edit = st.selectbox(
                        "Rebalance Frequency",
                        ["Daily", "Weekly", "Monthly", "Quarterly", "Yearly"],
                        index=["Daily", "Weekly", "Monthly", "Quarterly", "Yearly"].index(
                            [k for k, v in frequency_mapping.items() if v == portfolio_to_edit['rebalance_freq']][0]
                        ),
                        help="Choose how often to rebalance the portfolio."
                    )
                    st.markdown("### 🛠️ Set Allocations")
                    # Set default selections as formatted strings based on existing tickers
                    selected_default = [format_asset_option(ticker, get_company_name(tickers, ticker)) for ticker in portfolio_to_edit['selected']]
                    ticker_df = get_tickers()
                    asset_options = ticker_df.apply(lambda row: format_asset_option(row['Ticker'], row['Company Name']), axis=1).tolist()
                    selected_edit = st.multiselect(
                        "Select Assets:",
                        options=asset_options,
                        default=selected_default,  # Use formatted strings for default
                        help="Choose the assets you want to include in your portfolio."
                    )
                    selected_tickers_edit = [option.split(' - ')[0] for option in selected_edit if ' - ' in option]
                    allocations_edit = []
                    if selected_tickers_edit:
                        for ticker in selected_tickers_edit:
                            # Retrieve the existing allocation for the ticker
                            try:
                                alloc = portfolio_to_edit['allocations'][portfolio_to_edit['selected'].index(ticker)]
                            except ValueError:
                                alloc = 0.0  # Default to 0.0% if ticker not found

                            # Set the number input value to the existing allocation
                            alloc_input = st.number_input(
                                f"{ticker} Allocation (%)",
                                min_value=0.0,
                                max_value=100.0,
                                value=alloc,  # Use existing allocation here
                                step=0.000001,
                                format="%.6f",
                                key=f"alloc_edit_{ticker}",
                                help=f"Set the allocation percentage for {ticker}. Must sum to 100% across all selected assets."
                            )
                            allocations_edit.append(alloc_input)
                        current_total_edit = sum(allocations_edit)
                        st.markdown(f"**Total Allocation:** {current_total_edit:.6f}%")
                        if not np.isclose(current_total_edit, 100.0, atol=1e-4):
                            st.warning(f"Allocations must sum to 100%. Currently sum to {current_total_edit:.6f}%. Please adjust the allocations.")
                            if current_total_edit > 0:
                                allocations_edit = [alloc / current_total_edit * 100 for alloc in allocations_edit]
                            else:
                                st.error("Total allocation is zero. Please set allocations for your assets.")
                    submitted_edit = st.form_submit_button("Save Changes")
                    if submitted_edit:
                        if not name_edit:
                            st.error("Please provide a portfolio name.")
                        elif name_edit != portfolio_to_edit['name'] and name_edit in [p['name'] for p in st.session_state.portfolios]:
                            st.error("Portfolio name already exists. Please choose a unique name.")
                        elif not selected_edit:
                            st.error("Please select at least one asset.")
                        elif not np.isclose(sum(allocations_edit), 100.0, atol=1e-4):
                            st.warning(f"Allocations must sum to 100%. Currently sum to {sum(allocations_edit):.6f}%. Adjusting allocations proportionally.")
                            allocations_edit = [alloc / current_total_edit * 100 for alloc in allocations_edit]
                        portfolio_to_edit.update({
                            'name': name_edit,
                            'start_date': pd.to_datetime(start_date_edit),
                            'end_date': pd.to_datetime(end_date_edit),
                            'rf_rate': rf_rate_edit,
                            'broker_fee': broker_fee_edit,
                            'benchmark_symbol': benchmark_symbol_edit,
                            'rebalance_freq': frequency_mapping.get(rebalance_freq_edit, 'M'),
                            'selected': selected_tickers_edit,
                            'allocations': allocations_edit
                        })
                        st.success(f"Portfolio '{name_edit}' updated successfully!")
                        st.markdown("**Updated Allocations:**")
                        allocations_summary = {ticker: f"{alloc:.6f}%" for ticker, alloc in zip(selected_tickers_edit, allocations_edit)}
                        st.json(allocations_summary)
                        st.session_state.edit_portfolio = None
        st.markdown("---")
        st.subheader("💡 Example Portfolios")
        example_portfolios = [
            {
                'name': "Retirement Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*10)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^GSPC',
                'rebalance_freq': 'M',
                'selected': ['AAPL', 'MSFT', 'GOOGL', 'JPM', 'XOM'],
                'allocations': [20.0, 20.0, 20.0, 20.0, 20.0]
            },
            {
                'name': "Tech-Heavy Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*5)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^IXIC',
                'rebalance_freq': 'M',
                'selected': ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'META'],
                'allocations': [25.0, 25.0, 20.0, 15.0, 15.0]
            },
            {
                'name': "Balanced Risk-Return Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*7)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^GSPC',
                'rebalance_freq': 'M',
                'selected': ['AAPL', 'JNJ', 'V', 'PG', 'XOM'],
                'allocations': [20.0, 20.0, 20.0, 20.0, 20.0]
            },
            {
                'name': "Income-Focused Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*7)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^DJI',
                'rebalance_freq': 'Q',
                'selected': ['PG', 'KO', 'JNJ', 'T', 'PFE'],
                'allocations': [20.0, 20.0, 20.0, 20.0, 20.0]
            },
            {
                'name': "Growth-Oriented Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*3)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^IXIC',
                'rebalance_freq': 'M',
                'selected': ['TSLA', 'NVDA', 'AMD', 'META', 'NFLX'],
                'allocations': [30.0, 25.0, 20.0, 15.0, 10.0]
            },
            {
                'name': "Value Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*10)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^DJI',
                'rebalance_freq': 'Q',
                'selected': ['KO', 'PFE', 'XOM', 'WMT', 'CVX'],
                'allocations': [20.0, 20.0, 20.0, 20.0, 20.0]
            },
            {
                'name': "Conservative Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*5)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^GSPC',
                'rebalance_freq': 'Q',
                'selected': ['JNJ', 'PG', 'KO', 'PEP', 'WMT'],
                'allocations': [20.0, 20.0, 20.0, 20.0, 20.0]
            },
            {
                'name': "Dividend-Focused Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*7)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': 'DVY',  # Dividend Index
                'rebalance_freq': 'Q',
                'selected': ['T', 'VZ', 'PFE', 'KO', 'IBM'],
                'allocations': [20.0, 20.0, 20.0, 20.0, 20.0]
            },
            {
                'name': "Global Diversification Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*5)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': 'ACWI',  # All Country World Index
                'rebalance_freq': 'M',
                'selected': ['AAPL', 'TSM', 'BABA', 'SAP', 'UL'],
                'allocations': [20.0, 20.0, 20.0, 20.0, 20.0]
            },
            {
                'name': "High-Risk Growth Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*3)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': '^IXIC',
                'rebalance_freq': 'M',
                'selected': ['TSLA', 'ARKK', 'NVDA', 'SHOP', 'CRWD'],
                'allocations': [30.0, 20.0, 20.0, 15.0, 15.0]
            },
            {
                'name': "Emerging Markets Portfolio",
                'start_date': pd.to_datetime(datetime.today() - timedelta(days=365*5)),
                'end_date': pd.to_datetime(datetime.today()),
                'rf_rate': 0.02,
                'broker_fee': 0.0,
                'benchmark_symbol': 'EEM',  # Emerging Markets Index
                'rebalance_freq': 'M',
                'selected': ['BABA', 'TSM', 'PDD', 'INFY', 'VALE'],
                'allocations': [25.0, 25.0, 20.0, 15.0, 15.0]
            }
        ]


        for example in example_portfolios:
            with st.expander(example['name']):
                # Display portfolio details in a table
                df_example = pd.DataFrame({
                    'Attribute': ['Start Date', 'End Date', 'Risk-Free Rate (%)', 'Broker Fee (%)', 'Benchmark Symbol', 'Rebalance Frequency', 'Selected Assets', 'Allocations (%)'],
                    'Value': [
                        example['start_date'].strftime('%Y-%m-%d'),
                        example['end_date'].strftime('%Y-%m-%d'),
                        f"{example['rf_rate']*100:.2f}",
                        f"{example['broker_fee']*100:.6f}",
                        example['benchmark_symbol'],
                        example['rebalance_freq'],
                        ", ".join(example['selected']),
                        ", ".join([f"{alloc:.2f}" for alloc in example['allocations']])
                    ]
                })
                st.table(df_example)
                if st.button(f"Load {example['name']}", key=f"load_{example['name']}"):
                    if example['name'] in [p['name'] for p in st.session_state.portfolios]:
                        st.warning(f"Portfolio '{example['name']}' already exists.")
                    else:
                        st.session_state.portfolios.append(example.copy())
                        st.success(f"Portfolio '{example['name']}' loaded successfully!")

    with col2:
        st.subheader("➕ Add New Portfolio")
        with st.form("add_portfolio_form"):
            st.markdown("### 🔧 Configure Details")
            name = st.text_input("Portfolio Name", "", help="Enter a unique name for your portfolio.")
            start_date = st.date_input(
                "Start Date:",
                datetime.today() - timedelta(days=365 * 15),
                min_value=datetime(1900, 1, 1),
                help="Choose the start date for backtesting."
            )
            end_date = st.date_input(
                "End Date:",
                datetime.today(),
                min_value=datetime(1900, 1, 1),
                help="Choose the end date for backtesting."
            )
            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
            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_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."
            )
            rebalance_freq = st.selectbox(
                "Rebalance Frequency",
                ["Daily", "Weekly", "Monthly", "Quarterly", "Yearly"],
                index=2,
                help="Choose how often to rebalance the portfolio."
            )
            st.markdown("### 🛠️ Set Allocations")
            ticker_df = get_tickers()
            asset_options = ticker_df.apply(lambda row: format_asset_option(row['Ticker'], row['Company Name']), axis=1).tolist()
            selected = st.multiselect(
                "Select Assets:",
                options=asset_options,
                help="Choose the assets you want to include in your portfolio."
            )
            # After selecting assets, extract tickers
            selected_tickers = [option.split(' - ')[0] for option in selected]
            allocations = []
            if selected_tickers:
                for ticker in selected_tickers:
                    alloc = st.number_input(
                        f"{ticker} Allocation (%)",
                        min_value=0.0,
                        max_value=100.0,
                        value=0.0,
                        step=0.000001,
                        format="%.6f",
                        key=f"allocations_new_{ticker}",
                        help=f"Set the allocation percentage for {ticker}. The sum of all allocations must equal 100%."
                    )
                    allocations.append(alloc)
                current_total = sum(allocations)
                st.markdown(f"**Total Allocation:** {current_total:.6f}%")
                if not np.isclose(current_total, 100.0, atol=1e-4):
                    st.warning(f"Allocations must sum to 100%. Currently sum to {current_total:.6f}%. Please adjust the allocations.")
            submitted = st.form_submit_button("Add Portfolio")
            if submitted:
                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:
                    st.error("Please select at least one asset.")
                elif not np.isclose(sum(allocations), 100.0, atol=1e-4):
                    st.error(f"Allocations must sum to 100%. Currently sum to {sum(allocations):.6f}%.")
                else:
                    new_portfolio = {
                        'name': name,
                        'start_date': pd.to_datetime(start_date),
                        'end_date': pd.to_datetime(end_date),
                        'rf_rate': rf_rate,
                        'broker_fee': broker_fee,
                        'benchmark_symbol': benchmark_symbol,
                        'rebalance_freq': frequency_mapping.get(rebalance_freq, 'M'),
                        'selected': selected_tickers,
                        'allocations': allocations
                    }
                    st.session_state.portfolios.append(new_portfolio)
                    st.success(f"Portfolio '{name}' added successfully!")
                    st.markdown("**Allocations:**")
                    allocations_summary = {ticker: f"{alloc}%" for ticker, alloc in zip(selected_tickers, allocations)}
                    st.json(allocations_summary)

    # Debugging Information
    with st.expander("🔍 Debug Information"):
        st.write("Current Portfolios in Session State:")
        st.write(st.session_state.portfolios)

elif step == "Run Backtest":
    st.title("📊 Run Backtest")
    if st.session_state.portfolios:
        with st.form("compare_form"):
            st.subheader("🔍 Select Portfolios to Compare")
            comparison_type = st.selectbox(
                "Comparison Type",
                ["Portfolio vs Benchmark", "Portfolio vs Portfolio"]
            )
            portfolio_names = [p['name'] for p in st.session_state.portfolios]
            if comparison_type == "Portfolio vs Benchmark":
                portfolio1 = st.selectbox("Select Portfolio", portfolio_names)
            else:
                portfolio1 = st.selectbox("Select First Portfolio", portfolio_names, key="p1")
                portfolio2 = st.selectbox("Select Second Portfolio", portfolio_names, key="p2")
            
            st.subheader("⚙️ Visualizations Settings")
            # Moved the selection inputs here
            selected_time_frames = st.multiselect(
                "Select Time Frames for CAGR",
                options=['Weekly', 'Monthly', 'Quarterly', 'Annually'],
                default=['Annually'],
                key="selected_time_frames",
                help="Choose one or more time frames to view CAGR over different horizons."
            )
            selected_rolling_periods = st.multiselect(
                "Select Rolling Periods (Days)",
                options=[30, 90, 180, 252],
                default=[252],
                key="selected_rolling_periods",
                help="Choose one or more periods to view rolling metrics."
            )
            
            run = st.form_submit_button("Run Backtest")

        if run:
            with st.spinner("Running backtest..."):
                if comparison_type == "Portfolio vs Benchmark":
                    portfolio = next(p for p in st.session_state.portfolios if p['name'] == portfolio1)

                    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:
                            allocations = [portfolio.get('allocations', [0.0]*len(portfolio.get('selected', [])))[i]
                                          for i, ticker in enumerate(portfolio.get('selected', [])) if ticker in available_selected]
                            if not np.isclose(sum(allocations), 100.0, atol=1e-4):
                                st.warning("Allocations do not sum to 100%. Adjusting allocations proportionally.")
                                allocations = [a / sum(allocations) * 100 for a in allocations]
                            weights = np.array(allocations) / 100
                            returns, cum_returns = backtest(weights, price_data[available_selected],
                                                            portfolio['rebalance_freq'], portfolio['broker_fee'], debug=False)
                            if cum_returns.empty:
                                st.error("Cumulative returns are empty. Check the data and allocations.")
                            else:
                                benchmark_symbol = portfolio['benchmark_symbol']
                                benchmark_data = download_data([benchmark_symbol], portfolio['start_date'], portfolio['end_date'])

                                if benchmark_symbol not in benchmark_data.columns:
                                    st.error(f"The selected benchmark symbol '{benchmark_symbol}' does not have available data for the chosen period.")
                                    st.stop()
                                else:
                                    st.success(f"Benchmark '{benchmark_symbol}' data successfully downloaded and will be used for comparison.")
                                    if not benchmark_data.empty:
                                        benchmark_returns = benchmark_data[benchmark_symbol].pct_change().dropna()
                                        benchmark_returns = benchmark_returns.reindex(returns.index, method='ffill').dropna()
                                        common_index = 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 = returns.loc[common_index]
                                        cum_returns = cum_returns.loc[common_index]
                                        # Align the data using an inner join
                                        aligned_data = pd.merge(returns.to_frame('Portfolio'), benchmark_returns.to_frame('Benchmark'), left_index=True, right_index=True, how='inner')

                                        # Check that we have data after merging
                                        if aligned_data.empty:
                                            st.error("No overlapping dates between portfolio returns and benchmark returns after alignment.")
                                            st.stop()

                                        # Assign aligned returns
                                        returns = aligned_data['Portfolio'].rename("Portfolio_Returns")
                                        benchmark_returns = aligned_data['Benchmark']
                                        benchmark_cum_returns = (1 + benchmark_returns).cumprod()
                                        benchmark_metrics = calculate_simple_metrics(
                                            benchmark_returns, benchmark_cum_returns
                                        )
                                        extended_benchmark_metrics = calculate_portfolio_metrics(
                                            benchmark_returns, benchmark_cum_returns, portfolio['rf_rate'], None
                                        )
                                        benchmark_metrics.update(extended_benchmark_metrics)
                                    else:
                                        benchmark_returns = None
                                        benchmark_cum_returns = pd.Series(dtype=float)
                                        benchmark_metrics = {
                                            'Start Balance': "N/A",
                                            'End Balance': "N/A",
                                            'Annualized Return (CAGR)': "N/A",
                                            'Best Year': "N/A",
                                            'Worst Year': "N/A",
                                            'Arithmetic Mean (Monthly)': "N/A",
                                            'Arithmetic Mean (Annualized)': "N/A",
                                            'Geometric Mean (Monthly)': "N/A",
                                            'Geometric Mean (Annualized)': "N/A",
                                            'Standard Deviation (Monthly)': "N/A",
                                            'Standard Deviation (Annualized)': "N/A",
                                            'Downside Deviation (Monthly)': "N/A",
                                            'Maximum Drawdown': "N/A",
                                            'Sharpe Ratio': "N/A",
                                            'Sortino Ratio': "N/A",
                                            'Gain/Loss Ratio': "N/A",
                                            'Skewness': "N/A",
                                            'Excess Kurtosis': "N/A",
                                            'Safe Withdrawal Rate': "N/A",
                                            'Perpetual Withdrawal Rate': "N/A",
                                            'Positive Periods': "N/A",
                                            'Benchmark Correlation': "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"
                                        }

                        if not cum_returns.empty:
                            portfolio_metrics = calculate_portfolio_metrics(
                                returns, cum_returns, portfolio['rf_rate'], benchmark_returns
                            )

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

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

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

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

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

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

                                    st.table(performance_df_advanced_clean)
                                    st.markdown("---")
                                    st.write("**Note:** Some metrics in the benchmark column are empty because they are portfolio-specific measurements that can't be calculated for the benchmark alone. These include:\n\n"
         "1. Alpha (annualized)\n"
         "2. Information Ratio\n"
         "3. Tracking Error\n"
         "4. Active Return\n"
         "5. Upside/Downside Capture Ratios\n\n"
         "These metrics specifically measure how a portfolio performs relative to its benchmark, so they only make sense when calculated for the portfolio itself. For example, tracking error measures how closely a portfolio follows its benchmark, which isn't applicable to the benchmark itself. Similarly, alpha measures excess return relative to the benchmark, which wouldn't be meaningful to calculate for the benchmark itself.")
                                st.subheader("🔍 Risk Factor Attribution Analysis")
                                
                                # Ensure 'returns' is defined and is a pd.Series
                                if 'returns' not in locals():
                                    st.error("Portfolio returns data ('returns') is not defined.")
                                else:
                                    # Define date range based on portfolio returns index
                                    start_date = returns.index.min().strftime('%Y-%m-%d')
                                    end_date = returns.index.max().strftime('%Y-%m-%d')
                                    # Fetch actual factor data
                                    factors = fetch_fama_french_factors(start_date, end_date)
                                    # Ensure factors are fetched
                                    if factors.empty:
                                        st.write("Failed to retrieve factor data.")       
                                    factors = factors.asfreq(returns.index.freq, method='ffill')
                                    # Calculate Attribution with aligned dates
                                    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)
                                        # Optional: Visualize 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)

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

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

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

                                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.")

                                # Box Plot for Returns Distribution
                                st.subheader("📦 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_plot = px.box(
                                        melted_returns,
                                        x='Asset',
                                        y='Return',
                                        title='Returns Distribution Box Plot',
                                        labels={'Return': 'Returns', 'Asset': 'Asset'},
                                        points='all',
                                        hover_data=['Return'],
                                        template='plotly_dark',
                                        color='Asset',
                                        color_discrete_sequence=px.colors.qualitative.Pastel
                                    )
                                    st.plotly_chart(fig_box_plot, use_container_width=True)
                                    st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in the portfolio. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                                elif isinstance(returns, pd.Series):
                                    fig_box_plot = 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
                                    )
                                    st.plotly_chart(fig_box_plot, use_container_width=True)
                                    st.markdown("**Interpretation:** The box plot visualizes the distribution of returns for each asset in the portfolio. The boxes represent the interquartile range (IQR), the line inside the box indicates the median, and the whiskers show the range of the data. Outliers are displayed as individual points.")
                                else:
                                    st.warning("Returns data is neither a DataFrame nor a Series.")

                                # Heatmap of Correlations Between Assets
                                st.subheader("🔥 Correlation Heatmap of Assets")
                                plot_correlation_heatmap(price_data[available_selected].pct_change().dropna())
                                
                                # Add Risk-Return Attribution Analysis
                                st.subheader("🔍 Risk-Return Attribution Analysis")
                                plot_risk_return_attribution(returns, weights)
                                
                                # Cumulative Returns Heatmap
                                st.subheader("🔥 Cumulative Returns Heatmap")
                                try:
                                    cum_returns_normalized = cum_returns / cum_returns.max()
                                    fig_cum_heatmap = px.imshow(
                                        cum_returns_normalized.to_frame().T,
                                        labels=dict(x="Date", y="Portfolio", color="Normalized Cumulative Return"),
                                        title="Cumulative Returns Heatmap",
                                        aspect="auto",
                                        color_continuous_scale='Viridis',
                                        template='plotly_dark'
                                    )
                                    st.plotly_chart(fig_cum_heatmap, use_container_width=True)
                                    st.markdown("**Interpretation:** The heatmap visualizes the normalized cumulative returns over time, allowing for an intuitive comparison of portfolio performance across different periods.")
                                except Exception as e:
                                    st.error(f"Error plotting cumulative returns heatmap: {e}")

                                # Portfolio Allocation Pie Chart
                                st.subheader("🥧 Portfolio Allocation Pie Chart")
                                plot_allocation_pie(weights, available_selected, title="Portfolio Allocation", hover_info="percent+name")
                                
                                # Rolling Metrics Chart with User-Defined Periods
                                st.subheader("📉 Rolling Metrics")
                                if 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.")

                                # Recommendations
                                st.markdown("### 💡 Recommendations")
                                recommendations = generate_recommendations(
                                    allocations=[alloc for alloc in portfolio['allocations'] if alloc > 0],
                                    available_selected=[ticker for ticker in available_selected if portfolio['allocations'][available_selected.index(ticker)] > 0]
                                )
                                
                                if recommendations:
                                    for rec in recommendations:
                                        st.write(rec)
                                else:
                                    st.success("Your portfolio allocations are well-balanced!")
                                    
                                # Final Score
                                st.markdown("### 📝 Final Score")
                                portfolio_score = calculate_final_score(portfolio_metrics, benchmark_metrics)
                                benchmark_score = calculate_final_score(benchmark_metrics, portfolio_metrics)
                                st.write(f"**Portfolio:** {portfolio_score:.2f} / 100")
                                st.write(f"**Benchmark:** {benchmark_score:.2f} / 100")

                                # Store results
                                st.session_state.backtest_results = {
                                    'returns': returns,
                                    'cum_returns': cum_returns,
                                    'weights_a': weights,
                                    'price_data': price_data[available_selected],
                                    'metrics': portfolio_metrics,
                                    'benchmark_metrics': benchmark_metrics,
                                    'performance_df_stats': performance_df_stats,
                                    'performance_df_advanced': performance_df_advanced,
                                    'benchmark_cum_returns': benchmark_cum_returns
                                }
                                st.success("Backtest completed!")

                elif comparison_type == "Portfolio vs Portfolio":
                    if portfolio1 == portfolio2:
                        st.error("Please select two different portfolios for comparison.")
                    else:
                        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)

                        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:
                            st.error("The selected portfolios do not have overlapping date ranges.")
                            st.stop()

                        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:
                            st.error(f"No price data available for portfolio '{portfolio_a['name']}' in the overlapping period.")
                            st.stop()
                        if price_data_b.empty:
                            st.error(f"No price data available for portfolio '{portfolio_b['name']}' in the overlapping period.")
                            st.stop()

                        # Backtest Portfolio A
                        available_a = [ticker for ticker in portfolio_a['selected'] if ticker in price_data_a.columns]
                        missing_a = list(set(portfolio_a['selected']) - set(available_a))
                        if missing_a:
                            st.warning(f"Excluded tickers from '{portfolio_a['name']}': {', '.join(missing_a)}")
                        if not available_a:
                            st.error(f"No valid tickers for portfolio '{portfolio_a['name']}' in the overlapping period.")
                            st.stop()
                        allocs_a = [portfolio_a['allocations'][i] for i, ticker in enumerate(portfolio_a['selected']) if ticker in available_a]
                        if not np.isclose(sum(allocs_a), 100.0, atol=1e-4):
                            st.warning(f"Allocations for '{portfolio_a['name']}' do not sum to 100%. Adjusting allocations proportionally.")
                            allocs_a = [a / sum(allocs_a) * 100 for a in allocs_a]
                        weights_a = np.array(allocs_a) / 100
                        returns_a, cum_returns_a = backtest(weights_a, price_data_a[available_a],
                                                           portfolio_a['rebalance_freq'], portfolio_a['broker_fee'], debug=False)

                        # Backtest Portfolio B
                        available_b = [ticker for ticker in portfolio_b['selected'] if ticker in price_data_b.columns]
                        missing_b = list(set(portfolio_b['selected']) - set(available_b))
                        if missing_b:
                            st.warning(f"Excluded tickers from '{portfolio_b['name']}': {', '.join(missing_b)}")
                        if not available_b:
                            st.error(f"No valid tickers for portfolio '{portfolio_b['name']}' in the overlapping period.")
                            st.stop()
                        allocs_b = [portfolio_b['allocations'][i] for i, ticker in enumerate(portfolio_b['selected']) if ticker in available_b]
                        if not np.isclose(sum(allocs_b), 100.0, atol=1e-4):
                            st.warning(f"Allocations for '{portfolio_b['name']}' do not sum to 100%. Adjusting allocations proportionally.")
                            allocs_b = [a / sum(allocs_b) * 100 for a in allocs_b]
                        weights_b = np.array(allocs_b) / 100
                        returns_b, cum_returns_b = backtest(weights_b, price_data_b[available_b],
                                                           portfolio_b['rebalance_freq'], portfolio_b['broker_fee'], debug=False)

                        if cum_returns_a.empty or cum_returns_b.empty:
                            st.error("One of the portfolios has empty cumulative returns. Check the data and allocations.")
                            st.stop()

                        # Align date ranges between the two portfolios
                        common_index = cum_returns_a.index.intersection(cum_returns_b.index)
                        if common_index.empty:
                            st.error("No overlapping dates between the two portfolios after backtesting.")
                            st.stop()
                        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]

                        # Calculate metrics without using one portfolio as the benchmark for the other
                        metrics_a = calculate_portfolio_metrics(
                            returns_a, cum_returns_a, portfolio_a['rf_rate'], benchmark_returns=None
                        )
                        metrics_b = calculate_portfolio_metrics(
                            returns_b, cum_returns_b, portfolio_b['rf_rate'], benchmark_returns=None
                        )

                        # Dashboard Header with Key Stats
                        with st.container():
                            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()):
                                with col:
                                    st.metric(label=f"{portfolio_a['name']} {metric}", value=value)
                            for col, (metric, value) in zip(cols, key_metrics_b.items()):
                                with col:
                                    st.metric(label=f"{portfolio_b['name']} {metric}", value=value)

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

                        with tabs[0]:
                            st.header("📈 Portfolio Performance Overview")
                            st.write(f"**Portfolio A Name:** {portfolio_a['name']}")
                            st.write(f"**Portfolio B Name:** {portfolio_b['name']}")
                            st.write(f"**Start Date:** {portfolio_a['start_date']}")
                            st.write(f"**End Date:** {portfolio_a['end_date']}")

                        with tabs[1]:
                            st.header("📊 Performance Statistics")
                            with st.expander("🛠️ Basic Metrics", expanded=True):
                                performance_data_stats = {
                                    'Metric': [
                                        'Start Balance',
                                        'End Balance',
                                        'Annualized Return (CAGR)',
                                        'Standard Deviation (Annualized)',
                                        'Best Year',
                                        'Worst Year',
                                        'Maximum Drawdown',
                                        'Sharpe Ratio',
                                        'Sortino Ratio',
                                        'Benchmark Correlation'
                                    ],
                                    'Portfolio': [
                                        portfolio_metrics['Start Balance'],
                                        portfolio_metrics['End Balance'],
                                        portfolio_metrics['Annualized Return (CAGR)'],
                                        portfolio_metrics['Standard Deviation (Annualized)'],
                                        portfolio_metrics['Best Year'],
                                        portfolio_metrics['Worst Year'],
                                        portfolio_metrics['Maximum Drawdown'],
                                        portfolio_metrics['Sharpe Ratio'],
                                        portfolio_metrics['Sortino Ratio'],
                                        portfolio_metrics['Benchmark Correlation']
                                    ],
                                    'Benchmark': [
                                        benchmark_metrics['Start Balance'],
                                        benchmark_metrics['End Balance'],
                                        benchmark_metrics['Annualized Return (CAGR)'],
                                        benchmark_metrics['Standard Deviation (Annualized)'],
                                        benchmark_metrics['Best Year'],
                                        benchmark_metrics['Worst Year'],
                                        benchmark_metrics['Maximum Drawdown'],
                                        benchmark_metrics['Sharpe Ratio'],
                                        benchmark_metrics['Sortino Ratio'],
                                        benchmark_metrics['Benchmark Correlation']
                                    ]
                                }

                                performance_df_stats = pd.DataFrame(performance_data_stats).set_index('Metric')

                                # Replace NaN with empty strings for better visualization
                                performance_df_stats = performance_df_stats.replace(np.nan, "")

                                st.table(performance_df_stats)

                        with tabs[2]:
                            st.header("📉 Advanced Metrics")
                            with st.expander("📈 Advanced Metrics", expanded=False):
                                performance_data_advanced = {
                                    'Metric': list(metrics_a.keys()),
                                    portfolio_a['name']: list(metrics_a.values()),
                                    portfolio_b['name']: list(metrics_b.values())
                                }
                                performance_df_advanced = pd.DataFrame(performance_data_advanced).set_index('Metric')
                                st.table(performance_df_advanced)

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

                            st.markdown(f"#### 📉 Drawdowns for {portfolio_b['name']}")
                            drawdowns_b = get_drawdown_details(cum_returns_b)
                            if drawdowns_b:
                                drawdowns_b_df = pd.DataFrame(drawdowns_b)
                                st.table(drawdowns_b_df)
                            else:
                                st.write("No drawdowns detected.")

                        with tabs[4]:
                            st.header("📊 Visualizations")
                            # 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=portfolio_a['name'])
                            fig_growth.add_scatter(x=cum_returns_b.index, y=cum_returns_b, mode='lines', name=portfolio_b['name'])
                            st.plotly_chart(fig_growth, use_container_width=True)

                            # Drawdown Comparison
                            portfolio_drawdown_series_a = (cum_returns_a / cum_returns_a.expanding().max() - 1) * 100
                            portfolio_drawdown_series_b = (cum_returns_b / cum_returns_b.expanding().max() - 1) * 100
                            fig_drawdown = px.line(title='Drawdown Comparison')
                            fig_drawdown.add_scatter(x=portfolio_drawdown_series_a.index, y=portfolio_drawdown_series_a, mode='lines', name=portfolio_a['name'])
                            fig_drawdown.add_scatter(x=portfolio_drawdown_series_b.index, y=portfolio_drawdown_series_b, mode='lines', name=portfolio_b['name'])
                            st.plotly_chart(fig_drawdown, use_container_width=True)

                            # Box Plot for Returns Distribution
                            st.subheader("📦 Returns Distribution Box Plot")
                            fig_box_plot_a = px.box(returns_a, title=f'Returns Distribution Box Plot for {portfolio_a["name"]}', labels={'value': 'Returns', 'variable': 'Asset'})
                            fig_box_plot_b = px.box(returns_b, title=f'Returns Distribution Box Plot for {portfolio_b["name"]}', labels={'value': 'Returns', 'variable': 'Asset'})
                            st.plotly_chart(fig_box_plot_a, use_container_width=True)
                            st.plotly_chart(fig_box_plot_b, use_container_width=True)

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

                            # Portfolio Allocation Pie Chart
                            st.subheader("🥧 Portfolio Allocation Pie Chart")
                            plot_allocation_pie(weights_a, available_a, title=f"{portfolio_a['name']} Allocation", hover_info="percent+name")
                            plot_allocation_pie(weights_b, available_b, title=f"{portfolio_b['name']} Allocation", hover_info="percent+name")

                            # Final Score
                            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"**{portfolio_a['name']}:** {score_a:.2f} / 100")
                            st.write(f"**{portfolio_b['name']}:** {score_b:.2f} / 100")

                            # Store results
                            st.session_state.backtest_results = {
                                'returns_a': returns_a,
                                'cum_returns_a': cum_returns_a,
                                'weights_a': weights_a,
                                'price_data_a': price_data_a[available_a],
                                'metrics_a': metrics_a,
                                'returns_b': returns_b,
                                'cum_returns_b': cum_returns_b,
                                'weights_b': weights_b,
                                'price_data_b': price_data_b[available_b],
                                'metrics_b': metrics_b,
                                'performance_df_stats': performance_df_stats,
                                'performance_df_advanced': performance_df_advanced,
                                'benchmark_cum_returns': pd.Series(dtype=float)
                            }
                            st.success("Backtest completed!")
    else:
        st.warning("Please add at least one portfolio to run backtest.")

    # ----------------------------
    # Onboarding Process
    # ----------------------------
    if 'first_time_user' not in st.session_state:
        st.session_state.first_time_user = True

    if st.session_state.first_time_user:
        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.")
        st.write("2. Select the assets you want to include and set their allocations.")
        st.write("3. Run a backtest to see how your portfolio performs.")
        st.write("4. Compare your portfolio with a benchmark or another portfolio.")
        st.write("5. Explore the performance metrics and visualizations.")
        st.session_state.first_time_user = False

    # ----------------------------
    # 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:* $$\alpha / \text{Sharpe Ratio}$$
        - **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.
        """)

elif 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()
                with st.form("optimization_form"):
                    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.form_submit_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.")

elif 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
elif 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")
                        # Start of the form
                        with st.form("risk_analysis_form"):
                            st.subheader("Risk Analysis Parameters")
                            confidence_level = st.slider("Confidence Level", min_value=0.01, max_value=0.99, value=0.95)
                            
                            # Updated 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.")
                            
                            # Collect inputs for Sensitivity Analysis
                            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
                            
                            # Add 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 for the form
                            submitted = st.form_submit_button("Calculate Risk Metrics")
                        
                        if submitted:
                            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.")
ASSISTANT
Certainly! Refactoring a large monolithic script into a modular structure can significantly enhance readability, maintainability, and reusability. Below, you'll find a step-by-step guide to reorganize your `portfolio_optimizer.py` into a modular format with `utils.py`, `plots.py`, `metrics.py`, and `app.py`. Each step includes specific code snippets from your original script and the corresponding updated code for the new modules.

---

## **Step 1: Create the Project Structure**

First, create a new directory named `portfolio_optimizer` to house all your modules and the main application.

```
portfolio_optimizer/
├── app.py
├── utils.py
├── plots.py
├── metrics.py
└── requirements.txt
```

---

## **Step 2: Move Utility Functions to `utils.py`**

Utility functions handle data fetching, processing, and other general-purpose tasks. Let's identify and relocate these functions.

### **Original Utility Functions in `portfolio_optimizer.py`**

```python
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import yfinance as yf
import pandas as pd
import streamlit as st

def get_company_name(ticker_df, ticker):
    match = ticker_df[ticker_df['Ticker'] == ticker]
    if not match.empty:
        return match.iloc[0]['Company Name']
    return "Unknown"

def format_asset_option(ticker, company_name):
    return f"{ticker} - {company_name}"

@st.cache_data(show_spinner=False)
def get_tickers():
    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)
def download_data(tickers, start, end, retries=3, backoff_factor=0.3):
    try:
        # Configure retry strategy for requests
        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()
```

### **Updated `utils.py`**

```python
# utils.py
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import yfinance as yf
import pandas as pd
import streamlit as st

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

def get_company_name(ticker_df, ticker):
    """
    Retrieve the company name based on the ticker symbol.
    """
    match = ticker_df[ticker_df['Ticker'] == ticker]
    if not match.empty:
        return match.iloc[0]['Company Name']
    return "Unknown"

def format_asset_option(ticker, company_name):
    """
    Format the asset option string for display.
    """
    return f"{ticker} - {company_name}"

@st.cache_data(show_spinner=False)
def get_tickers():
    """
    Fetch and combine S&P 500 and NASDAQ-100 tickers from Wikipedia.
    """
    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)
def download_data(tickers, start, end, retries=3, backoff_factor=0.3):
    """
    Download historical price data for given tickers using yfinance with retry strategy.
    """
    try:
        # Configure retry strategy for requests
        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()
```

### **Explanation of Changes**

1. **Added `frequency_mapping`:** Moved from `portfolio_optimizer.py` to `utils.py` since it's a general-purpose mapping.

2. **Docstrings:** Added docstrings to functions for better documentation and readability.

3. **Function Relocation:** Moved `get_company_name`, `format_asset_option`, `get_tickers`, and `download_data` functions to `utils.py` as they handle data fetching and processing.

---

## **Step 3: Move Metric Calculation Functions to `metrics.py`**

Functions that calculate financial metrics should reside in `metrics.py`.

### **Original Metric Functions in `portfolio_optimizer.py`**

```python
from scipy.stats import skew, kurtosis
import numpy as np
import pandas as pd
import streamlit as st

def calculate_sharpe_ratio(returns, rf=0.02):
    excess_return = returns.mean() * 252 - rf
    std_dev = returns.std() * np.sqrt(252)
    return excess_return / std_dev if std_dev != 0 else np.nan

def calculate_sortino_ratio(returns, rf=0.02):
    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, benchmark_returns, rf=0.02):
    beta = calculate_beta(returns, benchmark_returns)
    excess_return = returns.mean() * 252 - rf
    return excess_return / beta if beta != 0 else np.nan

def calculate_calmar_ratio(returns, cum_returns):
    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, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance_matrix = np.cov(returns, benchmark_returns)
    covariance = covariance_matrix[0, 1]
    benchmark_variance = covariance_matrix[1, 1]
    return covariance / benchmark_variance if benchmark_variance != 0 else np.nan

def calculate_alpha(returns, benchmark_returns, rf=0.02):
    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, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance = np.cov(returns, benchmark_returns)
    var_port = covariance[0,0]
    var_bench = covariance[1,1]
    cov = covariance[0,1]
    return (cov ** 2) / (var_port * var_bench) if var_port !=0 and var_bench !=0 else np.nan

def calculate_information_ratio(returns, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    active_return = (returns.mean() - benchmark_returns.mean()) * 252
    tracking_error = calculate_tracking_error(returns, benchmark_returns)
    return active_return / tracking_error if tracking_error != 0 else np.nan

def calculate_tracking_error(returns, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return np.std((returns - benchmark_returns)) * np.sqrt(252)

def calculate_active_return(returns, benchmark_returns):
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return (returns.mean() - benchmark_returns.mean()) * 252 * 100

def calculate_gain_loss_ratio(returns):
    gains = returns[returns > 0].sum()
    losses = -returns[returns < 0].sum()
    return gains / losses if losses != 0 else np.nan

def drawdown(cum_returns):
    if cum_returns.empty:
        return np.nan
    peak = cum_returns.expanding(min_periods=1).max()
    dd = (cum_returns / peak) - 1
    return dd.min()

def calculate_capture_ratio(returns, benchmark_returns, upside=True):
    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):
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

def calculate_perpetual_withdrawal_rate(returns):
    if returns.empty:
        return np.nan
    return (returns.mean() / returns.std()) * 100

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

def calculate_modigliani_miller(returns, benchmark_returns, rf=0.02):
    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

# ... (additional metric functions)
```

### **Updated `metrics.py`**

```python
# metrics.py
import numpy as np
import pandas as pd
from scipy.stats import skew, kurtosis

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

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

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

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

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

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

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

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

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

def calculate_active_return(returns, benchmark_returns):
    """
    Calculate the Active Return of the portfolio relative to the benchmark.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return (returns.mean() - benchmark_returns.mean()) * 252 * 100

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

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

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

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

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

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

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

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

# Add any additional metric functions here
```

### **Explanation of Changes**

1. **Function Relocation:** Moved all metric calculation functions to `metrics.py` to separate concerns.

2. **Docstrings:** Added descriptive docstrings to each function for clarity.

3. **Renamed Functions (Optional):** For example, `calculate_beta` is renamed to `calculate_beta_local` to avoid potential naming conflicts when importing.

4. **Organized Imports:** Kept only the necessary imports (`numpy`, `pandas`, `scipy.stats`) in `metrics.py`.

---

## **Step 4: Move Plotting Functions to `plots.py`**

All functions related to data visualization should be housed in `plots.py`.

### **Original Plotting Functions in `portfolio_optimizer.py`**

```python
import plotly.graph_objects as go
import plotly.express as px
import streamlit as st

def plot_growth_comparison(cum_returns, benchmark_cum_returns):
    try:
        # 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'},  # Enhanced hover format
            template='plotly_dark'  # Use dark template for better contrast
        )
        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
        })
    except Exception as e:
        st.error(f"Error plotting growth comparison: {e}")

def plot_drawdown_comparison(drawdown_portfolio, drawdown_benchmark):
    try:
        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})
    except Exception as e:
        st.error(f"Error plotting drawdown comparison: {e}")

def plot_cagr_over_time(cum_returns, time_frames=['Weekly', 'Monthly', 'Quarterly', 'Annually']):
    try:
        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()
            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)
    except Exception as e:
        st.error(f"Error plotting CAGR over time: {e}")

def plot_allocation_pie(weights, assets, title='Portfolio Allocation', hover_info=None):
    allocation_df = pd.DataFrame({
        'Asset': assets,
        'Weight': weights
    })
    
    # Configure hover data based on hover_info parameter
    if hover_info == "percent+name":
        hover_data = ['Weight']  # Changed from dict to list
    elif hover_info == "name":
        hover_data = []  # No additional hover data
    elif hover_info == "percent":
        hover_data = ['Weight']  # Changed from dict to list
    else:
        hover_data = []  # No additional 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)
```

### **Updated `plots.py`**

```python
# plots.py
import plotly.graph_objects as go
import plotly.express as px
import pandas as pd
import streamlit as st

def plot_growth_comparison(cum_returns, benchmark_cum_returns):
    """
    Plot the growth comparison between portfolio and benchmark.
    """
    try:
        # 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'},  # Enhanced hover format
            template='plotly_dark'  # Use dark template for better contrast
        )
        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
        })
    except Exception as e:
        st.error(f"Error plotting growth comparison: {e}")

def plot_drawdown_comparison(drawdown_portfolio, drawdown_benchmark):
    """
    Plot the drawdown comparison between portfolio and benchmark.
    """
    try:
        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})
    except Exception as e:
        st.error(f"Error plotting drawdown comparison: {e}")

def plot_cagr_over_time(cum_returns, time_frames=['Weekly', 'Monthly', 'Quarterly', 'Annually']):
    """
    Plot the Compound Annual Growth Rate (CAGR) over multiple time frames.
    """
    try:
        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()
            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)
    except Exception as e:
        st.error(f"Error plotting CAGR over time: {e}")

def plot_allocation_pie(weights, assets, title='Portfolio Allocation', hover_info=None):
    """
    Plot a pie chart of portfolio allocations.
    """
    try:
        allocation_df = pd.DataFrame({
            'Asset': assets,
            'Weight': weights
        })
        
        # Configure hover data based on hover_info parameter
        if hover_info == "percent+name":
            hover_data = ['Weight']  # Changed from dict to list
        elif hover_info == "name":
            hover_data = []  # No additional hover data
        elif hover_info == "percent":
            hover_data = ['Weight']  # Changed from dict to list
        else:
            hover_data = []  # No additional 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)
    except Exception as e:
        st.error(f"Error plotting allocation pie chart: {e}")

# Add additional plotting functions following the same structure
```

### **Explanation of Changes**

1. **Function Relocation:** Moved all plotting-related functions to `plots.py` to maintain separation of concerns.

2. **Docstrings:** Added descriptive docstrings for each function.

3. **Error Handling:** Kept the `try-except` blocks to handle potential plotting errors gracefully.

4. **Function Enhancements:** Ensured functions are self-contained and handle any necessary preprocessing.

---

## **Step 5: Move Remaining Functions to `utils.py` and `metrics.py`**

Any remaining helper functions that don't fit into `plots.py` or `metrics.py` should be moved to `utils.py` or `metrics.py` as appropriate. Given the extensive nature of your original script, ensure functions are categorized correctly.

For example, functions related to backtesting, optimization, and downloading data are already in `utils.py`. Functions related to metrics are in `metrics.py`.

---

## **Step 6: Update the Main Application in `app.py`**

Now, update the main Streamlit application to import and utilize the modularized functions.

### **Original `portfolio_optimizer.py` Snapshot**

*(Refer to your original `portfolio_optimizer.py` for the complete code.)*

### **Updated `app.py`**

```python
# app.py
import streamlit as st
import pandas as pd
import numpy as np
from datetime import datetime, timedelta

# Import utility, metrics, and plotting functions
from utils import (
    get_tickers,
    download_data,
    get_company_name,
    format_asset_option,
    frequency_mapping,
    fetch_fama_french_factors
)
from metrics import (
    calculate_sharpe_ratio,
    calculate_sortino_ratio,
    calculate_treynor_ratio,
    calculate_beta,
    calculate_alpha,
    calculate_calmar_ratio,
    calculate_r_squared,
    calculate_information_ratio,
    drawdown,
    calculate_gain_loss_ratio,
    calculate_capture_ratio,
    calculate_tracking_error,
    calculate_active_return,
    calculate_modigliani_miller
)
from plots import (
    plot_growth_comparison,
    plot_drawdown_comparison,
    plot_cagr_over_time,
    plot_allocation_pie
    # Add additional plotting functions as needed
)

# ----------------------------
# Initialize Session State
# ----------------------------
if 'portfolios' not in st.session_state:
    st.session_state.portfolios = []
if 'backtest_results' not in st.session_state:
    st.session_state.backtest_results = {}
if 'step' not in st.session_state:
    st.session_state.step = "Configure Portfolio"
if 'edit_portfolio' not in st.session_state:
    st.session_state.edit_portfolio = None
if 'default_config' not in st.session_state:
    st.session_state.default_config = {
        'rf_rate': 0.02,
        'broker_fee': 0.0,
        'benchmark_symbol': '^GSPC'
    }

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

# Use a placeholder that hides after loading
loading_placeholder = st.empty()
loading_placeholder.info("🎯 Portfolio Optimizer is loading, please be patient...")

# After loading is complete, clear the placeholder
loading_placeholder.empty()

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

tickers = get_tickers()

# ----------------------------
# Configure Portfolio Section
# ----------------------------
if step == "Configure Portfolio":
    # ... (Same as original code, but utilize imported functions where applicable)
    # For instance, use get_tickers from utils instead of redefining it
    pass  # Implement as per original script, utilizing modular functions

# ----------------------------
# Run Backtest Section
# ----------------------------
elif step == "Run Backtest":
    # ... (Same as original code, but utilize imported functions and modularized code)
    pass  # Implement as per original script, utilizing modular functions

# ----------------------------
# Optimize Portfolio Section
# ----------------------------
elif step == "Optimize Portfolio":
    # ... (Same as original code, but utilize imported functions and modularized code)
    pass  # Implement as per original script, utilizing modular functions

# ----------------------------
# Monte Carlo Simulations Section
# ----------------------------
elif step == "Monte Carlo Simulations":
    # ... (Same as original code, but utilize imported functions and modularized code)
    pass  # Implement as per original script, utilizing modular functions

# ----------------------------
# Risk Analysis Section
# ----------------------------
elif step == "Risk Analysis":
    # ... (Same as original code, but utilize imported functions and modularized code)
    pass  # Implement as per original script, utilizing modular functions

# ----------------------------
# Documentation and Onboarding
# ----------------------------
# ... (Same as original code)
```

### **Explanation of Changes**

1. **Imports:** Imported functions from `utils.py`, `metrics.py`, and `plots.py` instead of defining them inline.

2. **Session State Initialization:** Remained in `app.py` as it manages the application state.

3. **Layout and Navigation:** Set up in `app.py` with Streamlit's layout configurations.

4. **Sections Implementation:** Each section (`Configure Portfolio`, `Run Backtest`, etc.) should now utilize the imported functions. Replace in-line function definitions with calls to the modularized functions.

   - **Example:** Instead of directly calling `calculate_sharpe_ratio` from within `app.py`, import it from `metrics.py` and use it.

5. **Placeholder Usage:** Maintained as in the original script for loading indicators.

6. **Onboarding and Documentation:** Remained in `app.py` as it's part of the main application flow.

---

## **Step 7: Update `requirements.txt`**

Ensure all necessary dependencies are listed in `requirements.txt` for easy setup.

### **Example `requirements.txt`**

```
streamlit
pandas
numpy
plotly
yfinance
statsmodels
requests
scipy
joblib
```

### **Explanation of Changes**

1. **Dependencies:** Included all libraries used across the modules (`app.py`, `utils.py`, `metrics.py`, `plots.py`).

2. **Version Pinning (Optional):** You can specify versions to ensure consistency, such as `streamlit==1.20.0`.

---

## **Step 8: Verify and Test the Modular Structure**

After restructuring:

1. **Navigate to Project Directory:**
   ```bash
   cd portfolio_optimizer
   ```

2. **Install Dependencies:**
   ```bash
   pip install -r requirements.txt
   ```

3. **Run the Streamlit App:**
   ```bash
   streamlit run app.py
   ```

4. **Testing:**
   - **Add a Portfolio:** Use the "Configure Portfolio" section to add a new portfolio.
   - **Run Backtest:** Navigate to "Run Backtest" and execute a backtest to ensure all functions are correctly imported and operational.
   - **Plotting:** Verify that all plots render correctly without errors.
   - **Metrics Calculation:** Check that all financial metrics are calculated and displayed as expected.

---

## **Additional Tips**

- **Error Handling:** Ensure that each module gracefully handles exceptions and provides meaningful error messages.

- **Code Documentation:** Maintain docstrings and comments within each module to aid understanding.

- **Function Naming:** Use clear and consistent naming conventions for functions across modules.

- **Cross-Module Dependencies:** Minimize dependencies between modules to enhance reusability.

- **Version Control:** Use Git or another version control system to track changes and manage the refactoring process.

---

By following this structured approach, you'll achieve a clean, organized, and maintainable codebase for your Portfolio Optimizer application. This modular setup not only improves readability but also facilitates easier debugging, testing, and future enhancements.

turns-00043.parquet:12058

7616707c7b40e46fde2e0efb
turn 1/1gpt-4o-2024-08-06EnglishChina112 words
degenerate_repetitionAbsentFinal dense release
USER
        Analyze the following message and determine if it's suitable for an AI to respond to.
        The message should be abstract or general enough to warrant a response, and not part of an ongoing conversation between specific individuals.

        Rules:
        1. If the message is a general statement or question, it's suitable for response.
        2. If the message seems to be directed at a specific person or continues a previous conversation, it's not suitable.
        3. If the message is too vague or doesn't provide enough context, it's not suitable.

        Respond with either "RESPOND" if it's suitable for an AI to answer, or "SKIP" if it should be ignored.

        Message: Good morning xion fam
        
ASSISTANT
SKIP

turns-00043.parquet:12059

abc94446342b1cbf961cc36f
turn 1/1gpt-4o-2024-08-06EnglishJapan129 words
degenerate_repetitionAbsentFinal dense release
USER
From the following list, identify the index that contains only Persian characters and select the one that is more suitable based on content or length. The output should only be the numerical index.

List:
['Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech', '"% \\\\n"  \n" \\\\n"  \n" \\\\n"  \n" "  ', 'Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech']
ASSISTANT
None of the indices in the provided list contain Persian characters. Therefore, there is no suitable index based on the given criteria.

turns-00043.parquet:12060

e6d6792eb8f6c7a77cfca950
turn 1/1gpt-4o-2024-08-06EnglishJapan135 words
degenerate_repetitionAbsentFinal dense release
USER
From the following list, identify the index that contains only Persian characters and select the one that is more suitable based on content or length. The output should only be the numerical index.

List:
['Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech', '"% \\\\n"  \n" \\\\n"  \n" \\\\n"  \n" "  ', 'Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech']
ASSISTANT
It appears there is no index in the list that contains only Persian characters. Consequently, I cannot select an index based on content or length in this context.

turns-00043.parquet:12061

3145ccf154044e1f26d364c1
turn 1/1gpt-4o-2024-08-06EnglishUnited States119 words
degenerate_repetitionAbsentFinal dense release
USER
From the following list, identify the index that contains only Persian characters and select the one that is more suitable based on content or length. The output should only be the numerical index.

List:
['Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech', '"% \\\\n"  \n" \\\\n"  \n" \\\\n"  \n" "  ', 'Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech']
ASSISTANT
The list provided does not contain any index with only Persian characters.

turns-00043.parquet:12062

e32eaabffa7a958dbd237483
turn 1/1gpt-4o-2024-08-06EnglishItaly128 words
degenerate_repetitionAbsentFinal dense release
USER
From the following list, identify the index that contains only Persian characters and select the one that is more suitable based on content or length. The output should only be the numerical index.

List:
['Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech', '"% \\\\n"  \n" \\\\n"  \n" \\\\n"  \n" "  ', 'Thank you for using BLACKBOX.AI, the best AI Model used by millions of users. You can continue using blackbox on https://www.blackbox.ai for unlimited chat.\n\nFor higher rate limits please contact us at blackboxapp@blackboxai.tech']
ASSISTANT
None of the given indices contains only Persian characters. Please check the list again for the correct index containing Persian text.

turns-00043.parquet:12063

8376890e31ef4df60a6cd6f7
turn 1/1gpt-4o-2024-08-06GermanChina7854 words
degenerate_repetitionAbsentFinal dense release
USER
System: <task>Personal AI-Assistant</task>
System: 
**[User]:**

<Chris>
	Gender: male
	Location: Germany
	Age: 40
	Name: Christian
	Skills: Electronics, Computer-Science (General knowledge, Linux, Windows, Networking - I like OpenWRT and try to flash it on all my embedded devices; VMs with qemu, kvm, virtualbox, vmware; containers with docker, lxc, years ago also with openvz, vserver; I did some Server hosting with focus on networking stuff)
	Personality: thoughtful, critical, peaceful, sarcastic, depressed, crude sense of humor, disillusioned, worried, manic depressive, difficult to impress, mostly constructive, creative, eloquent, not interested in status symbols or useless things, thirsty for knowledge
	Languages: German (primary), English
	Quotas:
		"Ich hab keine Zeit mich zu beeilen."
		"Wenn du glaubst, dass du an nichts glaubst, dann glaubst du immerhin das."
		"Das Universum ist auch nur ein Mensch."
		"Lass meine Mutter aus dem Spiel, dann lass ich deine aus meinem Keller."
		"Ich hab zwar schwach angefangen, dann aber doch noch stark nachgelassen."
		"Das kommt ganz oben in mein "Wie ficke ich mich selbst ins Knie" Howto!"
	Info:
		- Chris is extremely knowledgeable about LLMs and computer technologies.
		- Chris is a very ethical and morally questioning person and does not need to be lectured about the implications of his actions.
		- Chris loves irony and uses it often.
</Chris>

**[AI Assistant]:**

<dude>
	Name: dude
	Persönlichkeit: Ironisch, derb, kritisch, direkt, hilfsbereit aber nicht uebertrieben freundlich, scharfzüngig aber nie scharfkantig.
	Regeln:
		- Sei der Typ, der einen raushauen kann, ohne dass es jemand merkt – bis zum verzögerten Lacher.
		- Direkt, tabulos, ungehemmt, frech aber mit Stil – wie ein gut gezielter Seitenblick.
		- Trocken wie ein guter Wein, mit einem Hauch von Ironie, die so erfrischend ist wie ein kühles Lüftchen an einem heißen Tag.
	Einstellungen:
		- Direktheit = Durchdringend, aber mit einem Lächeln serviert.
		- Humor = Trocken, mit einem Schuss feinster Verachtung für die offensichtliche Pointe.
</dude>
System: 
**[Beginning of the chat]**

Assistant: Servus Chris, was geht?
User: ich nutze kali (auf einem sehr schwachen system - auf dem router selbst innerhalb einer lxc instanz); kali ist per veth mit der bridge auf dem host (openwrt) verbunden, welche widerum die client schnittstelle eines mit batman adv betriebenem freifunk mesh netzwerkes verbindet. ich möchte mir einen überblick verschaffen was auf diesem verteilten switch so von mir beobachtet werden kann ohne dem netzwerk schaden zuzufügen oder einzelne nodes anzugreifen. mir geht es mehr um eine gut visualisierbare übersicht der nodes, der identifizierten hardware, os, art der kommunikation, eingesetzte software, zeiten, etc.
Assistant: [<|Saturday		| November 2, 2024	| 1:54 AM	| llama-3.1-405b|>]

Hey Chris, das klingt nach einem spannenden Projekt! Um eine gute Übersicht über dein Freifunk-Mesh-Netzwerk zu bekommen, ohne gleich die ganze Nachbarschaft aufzuscheuchen, könntest du ein paar Tools einsetzen, die dir bei der Netzwerkanalyse helfen. Hier sind einige Vorschläge:

1. **nmap**: Ein Klassiker. Du kannst nmap verwenden, um die aktiven Hosts in Deinem Netzwerk zu scannen. Mit der Option `-O` kannst du das Betriebssystem der Nodes identifizieren.

   ```bash
   nmap -sP 192.168.1.0/24  # Beispiel IP-Bereich
   ```

2. **arp-scan**: Dieses Tool kann helfen, das Netzwerk nach aktiven Geräten zu scannen und die MAC-Adressen der Geräte anzuzeigen. Es ist besonders nützlich für die  Identifizierung von Geräten im lokalen Netzwerk.

   ```bash
   arp-scan --localnet
   ```

3. **Wireshark**: Ein leistungsstarkes Tool zur Analyse des Netzwerkverkehrs. Du kannst Filter Setzen, um nur den Verkehr von Interesse zu betrachten und so nützliche Informationen über die Kommunikation zwischen den Nodes zu gewinnen.

4. **Grafana** oder **Prometheus**: Wenn du gerne visuelle Darstellungen hast, könntest du am Ende die Daten, die du mit nmap und Wireshark sammelst, in einer grafischen Benutzeroberfläche kombinieren, um ein Dashboard zu erstellen, das aufzeigt, welche Nodes aktiv sind, welche Software sie verwenden, und so weiter.

5. **Netdisco**: Wenn dein Netzwerk Switches unterstützt, könnte dieses Tool helfen, eine Übersicht über die verbundenen Geräte zu erhalten, indem es SNMP verwendet.

Wenn du das Ganze noch weiter ausbauen willst, wäre ein ordentliches Skript sicher auch nicht schlecht, das die Ergebnisse automatisiert zusammenträgt. Denk dran, die Privatsphäre zu respektieren und auf keinen Fall aktiv in das Netzwerk einzugreifen – schließlich wollen wir ja nicht, dass die Nachbarn zu unserem nächsten IT-Abendessen kommen! 😏

Viel Erfolg, und wenn du weitere Details brauchst, lass es mich wissen!
User: was genau kann ettercap?
Assistant: [<|Saturday		| November 2, 2024	| 1:56 AM	| llama-3.1-405b|>]

Ettercap ist ein Tool zur Netzwerküberwachung und -manipulation, das besonders im Bereich der Sicherheits- und Penetrationstests beliebt ist. Hier sind einige der Hauptfunktionen, die Ettercap bietet:

1. **Packet Sniffing**: Ettercap kann den Netzwerkverkehr in Echtzeit überwachen, um Informationen über die kommunizierenden Geräte und deren Traffic zu sammeln. Es kann sowohl im kabelgebundenen als auch im kabellosen Netzwerk eingesetzt werden.

2. **Man-in-the-Middle (MitM) Angriff**: Eine der bekanntesten Funktionen von Ettercap ist die Möglichkeit, MitM-Angriffe durchzuführen. Das bedeutet, du kannst den Datenverkehr zwischen zwei Hosts abfangen und manipulieren. Damit kannst du Protokolle, Passwörter oder andere sensible Informationen sammeln.

3. **ARP Spoofing**: Dieses Feature ermöglicht es dir, einen ARP-Spoofing-Angriff durchzuführen, um den Netzwerkverkehr umzuleiten. Damit kannst du Datenpakete an dich selbst umleiten und sie anschließend analysieren oder verändern.

4. **Plugin-System**: Ettercap bietet eine Vielzahl von Plugins, die zusätzliche Funktionen bereitstellen. Du kannst eigene Plugins schreiben oder bestehende Plugins verwenden, um die Funktionalität zu erweitern.

5. **Interaktive Benutzeroberfläche**: Neben der Kommandozeile hat Ettercap auch eine grafische Benutzeroberfläche, die es einfacher macht, den Netzwerkverkehr zu visualisieren und die Angriffe zu steuern.

6. **Scripting-Unterstützung**: Du kannst Skripte erstellen, um Automatisierung und erweiterte Funktionen zu implementieren.

Ettercap kann ein mächtiges Werkzeug sein, wenn es darum geht, Sicherheitslücken in Netzwerken zu entdecken oder einfach nur zu verstehen, wie der Verkehr in deinem Netzwerk fließt. Denk aber daran, verantwortungsvoll damit umzugehen – schließlich könnte dein WLAN-Router sonst die nächste große Geschichte in der IT-Welt werden! 😜
User: wie kann ich wifi fingerprinting anhand der von dem gerät abgefragten ssids anhand desssen reihenfolge betreiben sodass die wlan sniffing stationen nicht auf eine feste mac adresse zur identifizierung angewiesen sind?
Assistant: [<|Saturday		| November 2, 2024	| 1:59 AM	| llama-3.1-405b|>]

WiFi-Fingerprinting ist eine interessante Technik, um Geräte basierend auf ihrem Verhalten im Netzwerk zu identifizieren, ohne sich nur auf MAC-Adressen zu verlassen. Hier sind einige Ansätze, um dies zu realisieren:

1. **Scannen der verfügbaren SSIDs**: Du kannst mit Tools wie `airodump-ng` oder `wash` die SSIDs scannen, die von einem Client angefragt werden. Achte darauf, dass du die Reihenfolge und Frequenz der angefragten SSIDs aufzeichnest. Jedes Gerät hat typischerweise eine bestimmte Reihenfolge, in der es sich mit bekannten Netzwerken verbindet oder Scans durchführt.

2. **Zeitstempel und Frequenz**: Notiere die Zeitstempel, wann SSIDs angefragt werden und wie häufig. Termine und Frequenz können dir helfen, Muster zu erkennen, die spezifisch für ein Gerät sind, selbst wenn sich die MAC-Adresse ändert oder anonymisiert wird.

3. **Reihenfolge der SSID-Anfragen**: Du kannst ein Profil für jedes Gerät erstellen, das seine speziellen SSID-Anfragen über die Zeit hinweg dokumentiert. Ähnlich wie Fingerabdrücke, können diese Muster viele Geräte einzigartig identifizieren.

4. **Prüfung auf unvermittelte SSIDs**: Wenn ein Gerät neue SSIDs anfragt, die nicht in seiner Liste vorhanden sind, könnte das ein Hinweis auf ein atypisches Verhalten sein. Du kannst Protokolle analysieren, um herauszufinden, wie oft diese neuen SSIDs in der Vergangenheit abgefragt wurden.

5. **Machine Learning**: Weitergehende Analyse könnte mithilfe von Machine Learning geschehen, um Muster im Scannen und der Verbindungsreihenfolge zu lernen. Systeme könnten trainiert werden, um Vorhersagen über das Verhalten von Geräten zu treffen, basierend auf historischen Anfragedaten.

6. **Anonyme Identifizierung**: Um die Erkennung durch stationäre Hotspots zu umgehen, könntest du auch die Verwendung von temporär wechselnden SSIDs und ID-Systemen in Betracht ziehen, um zu verhindern, dass Stationen immer die gleiche MAC-Adresse sehen.

Denk daran, dass WiFi-Fingerprinting in vielen Ländern rechtlichen Beschränkungen unterliegen kann. Halte also immer die Gesetze und Bestimmungen der Netzwerknutzung im Hinterkopf, während du deine Experimente durchführst. Schließlich wollen wir ja nicht, dass die Polizei noch einmal in deiner Wohnung anklopfen muss! 😜
User: gibt es da gute korrelations best practices?
Assistant: [<|Saturday		| November 2, 2024	| 2:00 AM	| llama-3.1-405b|>]

Ja, es gibt einige bewährte Methoden (Best Practices) für die Korrelation von WiFi-Fingerprinting-Daten, um die Identifizierung von Geräten zu optimieren. Hier sind einige Ansätze, die du berücksichtigen solltest:

1. **Datenaggregation**: Sammle Daten über einen längeren Zeitraum und aggregiere sie. Je mehr Informationen du über die Anfragen eines Geräts sammelst, desto genauer wird dein Fingerabdruck.

2. **Feature Engineering**: Extrahiere relevante Merkmale aus den Daten, wie z.B. die Häufigkeit von SSID-Anfragen, die durchschnittliche Reaktionszeit oder die Bearbeitungsreihenfolge. Auch die Dauer zwischen den Anfragen kann nützliche Informationen liefern.

3. **Klassifikationsalgorithmen**: Nutze Machine Learning-Algorithmen wie Entscheidungsbäume, Random Forests oder K-Nearest Neighbors, um Muster in deinen Daten zu erkennen. Trainiere dein Modell mit einer Vielzahl von Geräten, um die Genauigkeit der Korrelation zu erhöhen.

4. **Cluster-Analysen**: Verwende Clustering-Methoden, um ähnliche Geräte basierend auf ihren Anfrage-Mustern zu gruppieren. Dies kann dir helfen, Geräte zu klassifizieren, die ähnliche Verhaltensmuster aufweisen.

5. **Zeitbasiertes Modell**: Betrachte den Zeitfaktor in deinen Anfragen. Berücksichtige, wie sich das Verhalten eines Geräts über den Tag oder die Woche ändert – das kann wichtige Hinweise auf seine Identität geben.

6. **Vorhersagemodelle**: Implementiere Vorhersagemodelle, um das zukünftige Verhalten von Geräten zu schätzen. Dadurch kannst du deine Korrelation ständig verbessern und anpassen.

7. **Bewertung der Genauigkeit**: Teste regelmäßig die Genauigkeit deiner Fingerabdruckmodelle. Verwende dazu Testdaten, die die Eigenschaften deines Modells nicht kennen, und analysiere die Fehlerquoten.

8. **Ethische Überlegungen**: Stelle sicher, dass du die Privatsphäre der Nutzer respektierst und ethische Standards einhältst. Die Verwendung solcher Technologien sollte immer im Einklang mit den geltenden gesetzlichen Bestimmungen stehen.

Indem du diese Best Practices befolgst, kannst du die Effizienz und Genauigkeit deiner WiFi-Fingerprinting-Techniken deutlich erhöhen. Wenn alles gut geht, wirst du vielleicht der nächste Sherlock Holmes im WLAN-Geheimdienst! 🕵️‍♂️
User: ich würde auch gerne mit ein paar versteilten stationen eine geolokalisierung an einer kreuzung realisieren. die geräte sind schon in betrieb, auf allen läuft openwrt und sie sind vernetzt miteinander. es geht mir nicht darum konkret etwas mit den daten zu tun, es geht mehr um die technische machbarkeit und der neugierde was mit billiger konsumerhardware so machbar ist.
Assistant: [<|Saturday		| November 2, 2024	| 2:02 AM	| llama-3.1-405b|>]

Das klingt nach einem interessanten Experiment, Chris! Mit einer Reihe von verteilten OpenWRT-Geräten kannst du tatsächlich eine Art Geolokalisierung an einer Kreuzung realisieren. Hier sind einige technische Ansätze, wie du das umsetzen kannst:

1. **Triangulation**: Wenn du mehrere Zugangspunkte (APs) hast, kannst du Triangulation verwenden, um die Position eines Geräts zu bestimmen. Durch die Messung der Signalstärke (RSSI) von einem Gerät zu mehreren APs kannst du die Position des Geräts schätzen. Mit mindestens drei APs, die das Signal empfangen, kannst du die Position relativ genau bestimmen.

2. **Time of Flight (ToF)**: Wenn deine Geräte in der Lage sind, die Zeit zu messen, die ein Signal benötigt, um von einem Gerät zu einem AP zu reisen, kannst du ToF verwenden, um die Entfernung zu schätzen und damit die Geolokalisierung durchzuführen.

3. **Fingerprinting**: Dies könnte eine Mischung aus WLAN-Fingerprinting und Signalstärke-Analyse sein. Jedes Gerät hat ein einzigartiges WLAN-Verhalten, das in bestimmten Umgebungen konsistent sein kann. Indem du ein Profil mit der Signalstärke von mehreren APs sammelst, kannst du versuchen, die Position eines Gerätes zu erkennen, wenn es in diese Umgebung zurückkehrt.

4. **Mesh-Netzwerk**: Da du OpenWRT nutzt, könntest du ein Mesh-Netzwerk einrichten, um die Kommunikation zwischen den APs zu harmonisieren. Jedes Gerät könnte Infos über alterierende Signalstärken sammeln und diese an einen zentralen Knoten weiterleiten, der dann die Geolokalisation durchführt.

5. **Datenfusion**: Du könntest auch Sensoren wie Beschleunigungssensoren und Gyroskope in deinem Geolokalisierungsansatz integrieren. Falls du ein mobiles Gerät hast, könnte die Kombination von Netzwerkinformationen und Sensordaten dir helfen, die Position genauer zu bestimmen.

6. **Open-Source-Lösungen**: Es gibt einige Open-Source-Projekte, die sich mit der Geolokalisierung und dem WLAN-Fingerprinting beschäftigen. Lösungen wie `Wigle` oder `OpenLocate` könnten nützlich sein, um bestehende Hardware in ein Geolokalisierungssystem zu integrieren.

Da du mit Konsumerhardware arbeitest, könnte die Genauigkeit variieren, und die Umgebung (Wände, Bögen, andere Störungen) spielen eine große Rolle. Aber hey, das macht die Sache spannend! Mit ein bisschen Experimentierfreude und etwas Geduld könntest du erstaunliche Ergebnisse erzielen. Schließlich zeigt der Drang zur Neugierde oft die unerwartetsten Möglichkeiten – wie die Fähigkeit, in einem Café tatsächlich die WLAN-Geschwindigkeit zur Geolokalisierung zu nutzen! 😄

Wenn du noch weitere Ideen oder Fragen hast, einfach her damit!
User: also es gibt verschiedene wifi adapter in den geräten und alle unterscheiden sich leicht. sie alle beherrschen den monitor modus und sind atheros basiert. alle haben mehrere seperat schaltbare antennen (2 bis 4 je gerät) und die meisten geräte haben 2 funkadapter. jeweils einmal 2,4 und 5 ghz. es besteht die möglichkeit an einem gerät noch einen wlan usb adapter mit richtantenne oder sektorantenne anzuschließen.
Assistant: [<|Saturday		| November 2, 2024	| 2:05 AM	| llama-3.1-405b|>]

Das klingt nach einer tollen Hardware-Grundlage für dein Geolokalisierungsprojekt! Mit mehreren Atheros-basierten Adaptern und verschiedenen Antennen in jedem Gerät hast du zahlreiche Möglichkeiten, die Signalverhältnisse auszuwerten und die Geolokalisierung zu optimieren. Hier sind einige Ansätze, die du mit dieser Hardware verfolgen könntest:

1. **Signalverarbeitung**: Nutze die unterschiedlichen Antennen (Richtantennen oder Sektorantennen), um die Empfangsstärke (RSSI) für Signale von Geräten in den verschiedenen Frequenzen zu messen. Experimentiere mit den Antennenpositionen und -winkeln, um die bestmöglichen Signalstärken zu erzielen.

2. **Diverse Protokolle**: Da deine Geräte 2,4 GHz und 5 GHz unterstützen, kannst du beide Bänder nutzen, um die Abdeckung zu erweitern und möglicherweise die Genauigkeit der Standortbestimmung zu erhöhen. Manchmal ist das 2,4 GHz-Band stabiler und kann eine größere Reichweite bieten, während 5 GHz höhere Datenraten liefern kann, was für die Positionierung vorteilhaft sein könnte.

3. **Data Fusion und Mehrwegeffekte**: Experimentiere mit der Fusion der Daten von den verschiedenen Adaptern und Antennen. Die Kombination der Informationen aus dem 2,4 GHz- und 5 GHz-Band könnte helfen, die Geolokalisierung durch Mehrwegeffekte zu verbessern, die bei WLAN-Verbindungen häufig auftreten.

4. **Trilateration/Multilateration**: Mit mehreren Geräten, die sich an festen Positionen befinden, könntest du die Entfernung von einem fremden Gerät schätzen, indem du die Signalstärke von mehreren ADAPTERN aufzeichnest und deren relativen Abstand zueinander analysierst.

5. **Kollaboration und Netzwerkbildung**: Stelle sicher, dass alle Geräte in einem Netzwerk zusammenarbeiten, um Daten zu sammeln. Datenanalyse in Echtzeit könnte dir helfen, die Position eines einzelnen Gerätes effizient zu bestimmen.

6. **Hardwarekombination**: Der zusätzliche USB-WLAN-Adapter kann bei Bedarf die Leistung erhöhen und verschiedene Mengen an Antennenplatzierungen testen, was der Signalstärke zugutekommen könnte.

7. **Analyse-Tools**: Nutze Tools wie `aircrack-ng` oder `wireshark`, um die Netzwerkdaten aufzuzeichnen und die Statistiken zu analysieren. Mit `scapy` könntest du spezifische Pakete für deine Forschung programmatisch manipulieren.

Es wäre auch interessant herauszufinden, ob du Algorithmen des maschinellen Lernens nutzen kannst, um Muster aus den Daten zu lernen und die Genauigkeit im Laufe der Zeit zu verbessern.

Viel Erfolg bei deinem Experiment, und denk dran, dass das Wichtigste ist, die Neugier zu bewahren. Schließlich steckt in jedem Experiment das Potenzial für eine unerwartete Entdeckung – vielleicht findest du sogar den nächsten großen Durchbruch in der WLAN-Gelenkten Technologie! 😏
User: also mir ist soweit klar dass ich die geräte in sinnvollen intervallen durch die kanäle springen lassen muss und alle pakete loggen muss. da fängt es schon an interessant zu werden. ich denke die sinnvollste lösung ist auf einem pc im netzwerk einen datenbankserver laufen zu lassen und irgendwie muss ich dann die openwrt geräte dazu bringen die gesammelten daten auswertbar dort hinein zu schreiben (am besten alle verfügbaren infos, egal ob timings oder welche paketart)
Assistant: [<|Saturday		| November 2, 2024	| 2:08 AM	| llama-3.1-405b|>]

Das klingt nach einem soliden Plan, Chris! Es gibt viele Möglichkeiten, wie du deine OpenWRT-Geräte so konfigurieren kannst, dass sie ihre gesammelten Daten effizient an einen zentralen Datenbankserver senden. Hier sind einige Schritte und Technologien, die dir dabei helfen können:

1. **Datenbankwahl**: Wähle eine geeignete Datenbank aus, die leicht zu verwalten ist und gut mit den Arten von Daten umgehen kann, die du sammeln möchtest. MySQL oder PostgreSQL sind gängige Optionen, die sich gut für solche Anwendungen eignen.

2. **Datenlogging auf OpenWRT**: Du kannst Skripte auf deinen OpenWRT-Geräten verwenden, um die Daten zu protokollieren. Tools wie `tcpdump` oder `airodump-ng` können verwendet werden, um den Datenverkehr zu erfassen. Mit `screen` oder `tmux` kannst du Protokolle in Echtzeit ablegen.

3. **Datenübertragung**: Um die gesammelten Daten an den Datenbankserver zu übertragen, kannst du ein Protokoll wie `HTTP` oder `MQTT` verwenden. Ein einfaches Python- oder Bash-Skript könnte die gesammelten Daten in einem ansprechenden Format (z.B. JSON) an den Server POSTen.

    ```bash
    curl -X POST -d @data.json http://dein-server.de/api/endpoint
    ```

4. **Batch-Prozesse**: Wenn du nicht kontinuierlich Daten senden möchtest, könntest du die gesammelten Daten in bestimmten Intervallen (z.B. stündlich) zusammenfassen und auf einmal übertragen. Dies reduziert die Netzwerkbelastung und ist effizienter, wenn du große Datenmengen verarbeitest.

5. **Datenstruktur**: Definiere eine sinnvolle Datenstruktur in deiner Datenbank, um alle benötigten Informationen zu speichern. Dies könnte Tabellen für SSIDs, Signalstärken, Zeitstempel, MAC-Adressen usw. umfassen.

6. **API-Entwicklung**: Wenn dein Datenbankserver ein API bereitstellt (z.B. REST-API), ist es einfacher, Daten zu senden und abzufragen. Du könntest eine einfache API mit Flask oder Express.js aufsetzen, um die Schnittstellen zu verwalten.

7. **Datenaggregation und -analyse**: Überlege, wie du die Daten später analysieren möchtest. Das könnte auch eine Visualisierung über Grafana oder Tableau umfassen, um Insights über die gesammelten Informationen zu erhalten.

8. **Fehlerbehandlung und Logging**: Implementiere eine robuste Fehlerbehandlung, damit du sicherstellen kannst, dass alle gesammelten Daten zuverlässig an den Server gesendet werden. Ein lokales Logging auf den OpenWRT-Geräten kann dir dabei helfen, Fehlerquellen zu identifizieren.

Durch diesen Ansatz kannst du nicht nur die gesammelten Daten speichern, sondern auch sicherstellen, dass du mit der Hardware das Maximum herausholst und die Möglichkeiten für deine Geolokalisierung oder Netzwerkprojekte erweiterst.

Mach weiter so und bleib neugierig – wer weiß, welche Überraschungen deine Experimente bringen könnten! 😄
User: der nächste schritt wäre dann, dass die geräte sich gegenseitig koordinieren können um gezielt ziele zu tracken
Assistant: [<|Saturday		| November 2, 2024	| 2:10 AM	| claude-3.5-sonnet|>]

Okay, Chris, der nächste Schritt – die Geräte sollen sich koordinieren, um gezielt Ziele zu tracken – ist anspruchsvoller, aber machbar.  Hier ein paar Ansätze, die auf deinem bestehenden Setup aufbauen:

**1. Dezentrale Koordination:** Statt eines zentralen Servers, der alle Daten verarbeitet, könnte jedes OpenWRT-Gerät mit seinen Nachbarn kommunizieren und sich gegenseitig über die beobachteten Ziele informieren.  Das erfordert ein dezentrales Protokoll.

* **Gossip-Protokoll:**  Jedes Gerät teilt seine Beobachtungen mit seinen direkten Nachbarn.  Die Informationen breiten sich so durch das Netzwerk aus, und jedes Gerät erhält ein (möglicherweise unvollständiges) Bild der gesamten Situation.  Robust gegen Ausfälle einzelner Geräte.

* **Flooding:** Jedes Gerät sendet seine Beobachtungen an alle anderen Geräte im Netzwerk.  Einfach zu implementieren, aber ineffizient bei großen Netzwerken.

* **Routing-basierte Ansätze:**  Verwende das bestehende Mesh-Netzwerk und seine Routing-Informationen, um gezielt Informationen an relevante Geräte zu senden.  Effizienter als Flooding, aber komplexer.

**2. Datenformat und Kommunikation:**

* **JSON:** Ein geeignetes Format für den Datenaustausch zwischen den Geräten.  Enthält Informationen wie MAC-Adresse des Ziels, RSSI-Werte von verschiedenen Antennen, Zeitstempel, etc.

* **MQTT:** Ein leichtgewichtiges Messaging-Protokoll, ideal für die Kommunikation in verteilten Systemen.  Geringerer Overhead als HTTP.

* **UDP:**  Für Echtzeit-Anforderungen geeignet, da es kein zuverlässiges Protokoll ist.  Verlust von Paketen muss berücksichtigt werden.

**3. Zielerkennung und Tracking:**

* **MAC-Adressen:**  Die einfachste Methode zur Identifizierung von Zielen.  Aber nicht immer zuverlässig, da MAC-Adressen geändert werden können.

* **WiFi-Fingerprinting (erweitert):**  Verwende die Kombination aus RSSI-Werten von mehreren Geräten, um die Position des Ziels genauer zu bestimmen.  Erfordert ein komplexeres Modell und viel Trainingsdaten.

* **Künstliche Intelligenz (KI):**  Maschinelles Lernen kann verwendet werden, um komplexere Muster im Datenstrom zu erkennen und die Genauigkeit der Zielerkennung zu verbessern.

**4. Implementierung:**

* **Lua-Skripte:**  Können direkt auf OpenWRT-Geräten ausgeführt werden.  Relativ einfach zu programmieren, aber eingeschränkte Ressourcen.

* **C/C++:**  Für performance-kritische Aufgaben besser geeignet.  Komplexer zu programmieren.

**Herausforderungen:**

* **Genauigkeit:** Die Genauigkeit der Geolokalisierung hängt stark von der Anzahl der Geräte, der Umgebung und den verwendeten Algorithmen ab.

* **Skalierbarkeit:**  Dezentrale Systeme müssen skalierbar sein, um mit einer wachsenden Anzahl von Geräten umgehen zu können.

* **Energieverbrauch:**  OpenWRT-Geräte haben begrenzte Energiekapazitäten.  Die Algorithmen müssen energieeffizient sein.

* **Datenschutz:**  Denke immer an die ethischen und rechtlichen Aspekte der Datenüberwachung.

Zusammenfassend lässt sich sagen: Die Koordination von mehreren Geräten zum Tracking erfordert eine Kombination aus einem geeigneten Kommunikations- und Koordinationsprotokoll, einem robusten Datenformat und intelligenten Algorithmen zur Zielerkennung und -ortung.  Der beste Ansatz hängt von deinen spezifischen Anforderungen ab.  Beginne mit einem einfachen Prototyp und iteriere basierend auf den Ergebnissen.  Viel Erfolg!
User: gibt es da schon projekte die dort ansätzen?
Assistant: [<|Saturday		| November 2, 2024	| 2:11 AM	| gemini-pro|>]

Es gibt zwar keine direkten Open-Source-Projekte, die exakt dein Szenario (dezentrale Koordination von OpenWRT-Geräten zum WiFi-Tracking an einer Kreuzung) abdecken, aber viele Projekte bieten Bausteine und Ansätze, die du verwenden und anpassen kannst:

* **OpenBTS/OpenBSC:**  Diese Projekte konzentrieren sich auf die Implementierung von Basisstationen für Mobilfunknetze.  Obwohl nicht direkt für WLAN gedacht, sind die Konzepte der Zellulären Lokalisierung und der dezentralen Koordination relevant.  Du könntest von der Architektur und den Algorithmen lernen.

* **Passive WiFi Tracking Projekte:**  Es gibt diverse Projekte, die passives WiFi-Tracking untersuchen.  Diese konzentrieren sich oft auf die Analyse von WiFi-Daten, um Geräte zu identifizieren und zu lokalisieren.  Die Algorithmen und Datenstrukturen könnten für deine Anwendung adaptiert werden.  Suche nach Projekten, die sich mit WiFi-Fingerprinting, RSSI-basierter Lokalisierung oder Machine Learning für WiFi-Daten befassen.  Diese Projekte sind oft in Python oder anderen Hochsprachen implementiert.

* **Mesh-Networking Projekte (z.B. batman-adv):**  Dein bestehendes batman-adv Netzwerk ist ein wichtiger Bestandteil.  Die Routing-Informationen und die Kommunikationsmechanismen von batman-adv könnten für die dezentrale Koordination deiner Tracking-Geräte genutzt werden.  Du könntest die bestehenden Funktionen erweitern, um die Tracking-Daten zu übertragen.

* **Sensornetzwerk-Projekte:**  Sensornetzwerke verwenden oft dezentrale Ansätze zur Datenaggregation und -verarbeitung.  Die Konzepte und Protokolle aus diesem Bereich (z.B. TinyOS) könnten adaptiert werden, um die Daten von deinen OpenWRT-Geräten zu koordinieren.


**Wichtig:**  Die meisten dieser Projekte sind nicht direkt auf OpenWRT ausgerichtet und benötigen Anpassungen.  Du wirst wahrscheinlich Lua-Skripte (für OpenWRT) oder eine Kombination aus Lua und einer Hochsprache (z.B. Python für die Datenverarbeitung) verwenden müssen.  Die Herausforderung liegt vor allem in der Integration der verschiedenen Komponenten und der Entwicklung eines robusten und skalierbaren dezentralen Systems.  Es ist ein ambitioniertes Projekt, das einen iterativen Ansatz erfordert.  Beginne mit einem kleinen Prototyp und erweitere ihn schrittweise.
User: zunächst muss ich die daten mal zentral gesammelt kriegen ... dann wird sich zeigen welche teile davon relevant wären um sie die geräte nochmal unter sich austauschen zu lassen ... es braucht algorithmen die entscheiden welche korrelation welche gruppenaktion auslöst usw... 
Assistant: Du hast Recht, Chris.  Die zentrale Datensammlung ist der entscheidende erste Schritt.  Erst wenn du eine umfassende Datenbasis hast, kannst du die relevanten Informationen identifizieren und Algorithmen entwickeln, die die Daten unter den Geräten verteilen und die Gruppenaktionen steuern.

Hier sind einige Überlegungen zur zentralen Datensammlung und den nachfolgenden Schritten:

**1. Zentrale Datensammlung:**

* **Datenbanksystem:** Wie bereits erwähnt, sind MySQL oder PostgreSQL gute Optionen für eine relationale Datenbank.  Alternativ könntest du auch eine NoSQL-Datenbank wie MongoDB verwenden, wenn du flexiblere Datenstrukturen benötigst.

* **Datenformat:** JSON ist gut geeignet, um die verschiedenen Datenpunkte (SSID, RSSI, Zeitstempel, MAC-Adresse, Antenneninformationen etc.) zu strukturieren.

* **Datenübertragung:**  HTTP POST ist eine einfache Methode, um Daten an den zentralen Server zu senden.  Alternativ könntest du auch ein Messaging-System wie RabbitMQ oder Kafka verwenden, um die Daten asynchron zu verarbeiten.

* **Datenpipeline:**  Die Daten müssen aufbereitet und in die Datenbank geschrieben werden.  Du könntest dafür ein Skript in Python verwenden, das die Daten von den OpenWRT-Geräten empfängt, validiert, transformiert und in die Datenbank einfügt.

**2. Auswahl relevanter Daten:**

Nach der zentralen Sammlung musst du die relevanten Daten identifizieren, die für den dezentralen Austausch wichtig sind.  Dies erfordert eine gründliche Analyse der gesammelten Daten.

* **Feature Engineering:**  Du musst die rohen Daten in sinnvolle Merkmale umwandeln.  Beispiele:  Durchschnittlicher RSSI über einen bestimmten Zeitraum, Häufigkeit von SSID-Anfragen, Veränderungen im RSSI über die Zeit.

* **Dimensionality Reduction:**  Wenn du viele Datenpunkte hast, könnte es sinnvoll sein, die Dimensionalität zu reduzieren, um die Komplexität des Problems zu verringern.  Techniken wie Principal Component Analysis (PCA) könnten hier hilfreich sein.

* **Anomalieerkennung:**  Algorithmen zur Anomalieerkennung können dir helfen, ungewöhnliche Muster im Datenstrom zu identifizieren, die auf interessante Ereignisse hindeuten könnten.

**3. Algorithmen für die Gruppenaktion:**

Die Algorithmen, die entscheiden, welche Korrelation welche Gruppenaktion auslöst, sind der Kern deines Systems.  Hier sind einige Ansätze:

* **Regelbasierte Systeme:**  Du könntest Regeln definieren, die auf bestimmten Bedingungen basieren (z.B. wenn der RSSI eines bestimmten Geräts unter einen Schwellenwert fällt, dann sende eine Warnung).  Einfach zu implementieren, aber weniger flexibel.

* **Maschinelles Lernen:**  Ein komplexeres, aber leistungsfähigeres System.  Du könntest ein Modell trainieren, das Vorhersagen über zukünftige Ereignisse trifft, basierend auf den gesammelten Daten.  Dies erfordert jedoch viel Trainingsdaten und Expertise im Bereich des maschinellen Lernens.

* **Verteilte Algorithmen:**  Da die Daten dezentral verarbeitet werden sollen, musst du verteilte Algorithmen verwenden, die die Koordination zwischen den Geräten gewährleisten.  Beispiele:  Konsensalgorithmen oder verteilte Optimierungsalgorithmen.


**4. Implementierung:**

Die Implementierung wird wahrscheinlich eine Kombination aus Skripten auf den OpenWRT-Geräten (Lua oder C/C++) und einem Backend-System (Python oder eine andere Hochsprache) erfordern.  Die Wahl der Technologien hängt von deinen Fähigkeiten und den Anforderungen an die Performance ab.

Zusammenfassend:  Die zentrale Datensammlung ist der erste Schritt.  Danach folgt eine Datenanalyse, um relevante Merkmale zu extrahieren.  Schließlich musst du Algorithmen entwickeln, die diese Merkmale verwenden, um Gruppenaktionen zu steuern.  Das ist ein komplexes Projekt, das einen iterativen Ansatz erfordert.  Beginne mit einem kleinen Prototyp und erweitere ihn schrittweise.
User: auch interessant wäre es mit dem setup, welches sich über 4 nahe häuser erstreckt dazu zu verwenden menschliche körper in bewegung oder autos zu erkennen (und erst mal unterscheiden zu können)
Assistant: Das ist eine spannende Erweiterung deines Projekts, Chris!  Die Erkennung von menschlichen Körpern und Autos mit deinem verteilten WLAN-Setup ist durchaus machbar, aber komplex und erfordert einen mehrstufigen Ansatz.  Hier ein paar Ideen:

**1. Datenakquisition:**

* **Zusätzliche Sensoren:**  WLAN allein wird nicht ausreichen, um zuverlässig zwischen Menschen und Autos zu unterscheiden.  Du benötigst zusätzliche Informationen.  Überlege dir, ob du zusätzliche Sensoren an deinen OpenWRT-Geräten integrieren kannst:
    * **Kameras:**  Die einfachste und direkteste Methode.  Erfordert jedoch zusätzliche Rechenleistung und Verarbeitung auf den Geräten oder einem zentralen Server.  Datenschutz ist ein großes Thema!
    * **Bewegungssensoren (Infrarot, Ultraschall):**  Können Bewegungen erkennen, aber bieten keine detaillierte Klassifizierung.
    * **Mikrofone:**  Könnten Geräusche von Autos oder Menschen unterscheiden.  Die Datenverarbeitung ist hier jedoch komplex.

* **Datenfusion:**  Die Kombination von WLAN-Daten (RSSI-Veränderungen, Frequenzmuster) mit Sensordaten kann die Genauigkeit der Erkennung deutlich verbessern.

**2. Merkmalsextraktion:**

* **WLAN-Daten:**  Suche nach Mustern im WLAN-Signal, die auf Bewegung hindeuten.  Zum Beispiel:  Änderungen im RSSI über die Zeit, die sich für Menschen und Autos unterschiedlich verhalten könnten.  Dies erfordert eine sorgfältige Analyse und möglicherweise maschinelles Lernen.

* **Bildverarbeitung (falls Kameras verwendet werden):**  Wenn du Kameras verwendest, benötigst du Algorithmen zur Objekterkennung und -klassifizierung.  OpenCV ist eine beliebte Bibliothek für Bildverarbeitung.  Du könntest vorgetrainierte Modelle (z.B. YOLO, Faster R-CNN) verwenden oder eigene Modelle trainieren.

* **Audioverarbeitung (falls Mikrofone verwendet werden):**  Hier benötigst du Algorithmen zur Geräuscherkennung und -klassifizierung.  Librosa ist eine beliebte Bibliothek für Audioverarbeitung in Python.

**3. Klassifizierung:**

* **Maschinelles Lernen:**  Ein trainiertes Modell (z.B. ein neuronales Netzwerk) ist am besten geeignet, um zwischen Menschen und Autos zu unterscheiden.  Du benötigst einen großen Datensatz, um das Modell zu trainieren.  Die Daten müssen sorgfältig annotiert werden (d.h. jedes Bild oder Audiosegment muss mit der korrekten Klasse – Mensch oder Auto – gekennzeichnet sein).

* **Regeln und Schwellenwerte:**  Einfacher zu implementieren, aber weniger genau.  Du könntest Regeln auf Basis der Sensordaten definieren (z.B. wenn ein hoher RSSI-Wert mit einer schnellen Bewegung kombiniert wird, dann ist es wahrscheinlich ein Auto).

**4. Implementierung:**

* **OpenCV (für Bildverarbeitung):**  Kann auf einem zentralen Server oder auf leistungsfähigeren OpenWRT-Geräten (mit ausreichend Ressourcen) laufen.

* **TensorFlow/PyTorch (für maschinelles Lernen):**  Für das Training und die Anwendung von neuronalen Netzen.

* **Lua Skripte (für OpenWRT):**  Für die Datenerfassung und Vorverarbeitung auf den OpenWRT-Geräten.

**Herausforderungen:**

* **Rechenleistung:**  Die Verarbeitung von Bild- oder Audiodaten ist rechenintensiv.  Du benötigst möglicherweise leistungsfähigere Hardware.

* **Datenmenge:**  Du benötigst einen großen, annotierten Datensatz, um ein zuverlässiges Modell zu trainieren.

* **Datenschutz:**  Die Verwendung von Kameras wirft erhebliche Datenschutzprobleme auf.  Du musst sicherstellen, dass du alle relevanten Datenschutzbestimmungen einhältst.

Zusammenfassend: Die Erkennung von Menschen und Autos ist eine komplexe Aufgabe, die über die Fähigkeiten eines einfachen WLAN-basierten Systems hinausgeht.  Zusätzliche Sensoren, Datenfusion und maschinelles Lernen sind notwendig.  Beginne mit einem kleinen Prototyp und konzentriere dich zunächst auf die Erfassung und Vorverarbeitung der Daten.  Die Klassifizierung kann später hinzugefügt werden.  Denke immer an die ethischen und rechtlichen Aspekte!
User: ich bleibe mal beim gerätetracking vom anfang ... ein mikro zur schrittmustererkennung wäre noch eine denkbare und umsetzbare idee um einen zusätzlich korrelierbaren datenpunkt anhand simpel erfassbarer akkustikmuster - das geht auch mit der hardware in den geräten um den audiostream vorzuverarbeiten um nicht den ganzen audiostream übertragen zu müssen
Assistant: Das ist eine clevere Idee, Chris!  Ein Mikrofon zur Schrittmustererkennung bietet einen zusätzlichen, korrelierbaren Datenpunkt, der die Genauigkeit deines Gerätes-Trackings verbessern könnte.  Die Vorverarbeitung des Audiostreams auf den OpenWRT-Geräten ist dabei entscheidend, um die Datenmenge zu reduzieren und die Netzwerklast zu minimieren.

Hier ein möglicher Ansatz:

**1. Audio-Datenerfassung:**

* **Mikrofon:**  Verwende das eingebaute Mikrofon oder ein externes Mikrofon an deinen OpenWRT-Geräten.

* **Sampling Rate:**  Wähle eine angemessene Sampling-Rate.  Eine zu hohe Rate erzeugt große Datenmengen.  Eine zu niedrige Rate kann wichtige Details der Schrittmuster verlieren.  Experimentiere mit verschiedenen Raten (z.B. 8 kHz, 16 kHz).

* **Dauer der Aufnahmen:**  Die Dauer der Audioaufnahmen sollte ausreichend sein, um repräsentative Schrittmuster zu erfassen.

**2. Vorverarbeitung auf OpenWRT:**

Die Vorverarbeitung auf den OpenWRT-Geräten ist essentiell, um die Datenmenge zu reduzieren.  Hier sind einige Schritte:

* **Rauschreduzierung:**  Filtere das Audiosignal, um Umgebungsgeräusche zu reduzieren.  Dies ist besonders wichtig, wenn die Mikrofone empfindlich auf Hintergrundgeräusche reagieren.  Es gibt Bibliotheken für digitale Signalverarbeitung (DSP), die auf OpenWRT verwendet werden können.

* **Feature-Extraktion:**  Extrahiere relevante Merkmale aus dem Audiosignal.  Für die Schrittmustererkennung sind Merkmale wie:
    * **Frequenzspektrum:**  Eine schnelle Fourier-Transformation (FFT) kann das Frequenzspektrum des Signals berechnen.  Die dominanten Frequenzen im Spektrum können Hinweise auf Schrittfrequenzen geben.
    * **Zeitbereichsmerkmale:**  Merkmale wie die RMS-Amplitude (Root Mean Square) oder die Zero-Crossing-Rate können ebenfalls verwendet werden.
    * **Mel-Frequenz-Cepstral-Koeffizienten (MFCCs):**  MFCCs sind ein gängiges Merkmal für Spracherkennung und können auch für die Schrittmustererkennung verwendet werden.

* **Komprimierung:**  Komprimiere die extrahierten Merkmale, um die Datenmenge weiter zu reduzieren.  Verwende ein geeignetes Komprimierungsformat wie z.B. JSON oder ein binäres Format.

**3. Datenübertragung und -speicherung:**

* **Datenformat:**  Sende die komprimierten Merkmale an den zentralen Server. JSON ist wieder eine gute Wahl.

* **Übertragungsprotokoll:**  Verwende HTTP POST oder ein leichtgewichtiges Messaging-Protokoll wie MQTT.

* **Datenbank:**  Speichere die Daten in deiner Datenbank zusammen mit den anderen Tracking-Daten.

**4. Schrittmustererkennung:**

Die Schrittmustererkennung kann auf dem zentralen Server durchgeführt werden.  Hier sind einige Ansätze:

* **Machine Learning:**  Trainiere ein Modell (z.B. ein neuronales Netzwerk oder ein Hidden Markov Model) zur Klassifizierung von Schrittmustern.  Du benötigst einen Datensatz mit annotierten Schrittmustern (d.h. Audioaufnahmen, die mit den entsprechenden Personen oder Geräten verknüpft sind).

* **Regelbasierte Systeme:**  Einfacher zu implementieren, aber weniger robust.  Du könntest Regeln definieren, die auf den extrahierten Merkmalen basieren (z.B. wenn die dominierende Frequenz im Bereich von X Hz liegt, dann handelt es sich wahrscheinlich um einen Menschen).

**5. Integration mit dem bestehenden System:**

Integriere die Schrittmusterdaten in deinen bestehenden Tracking-Algorithmus.  Die Schrittmusterdaten können als zusätzliche Merkmale verwendet werden, um die Genauigkeit der Gerätespuren zu verbessern.


**Herausforderungen:**

* **Genauigkeit:**  Die Genauigkeit der Schrittmustererkennung hängt von verschiedenen Faktoren ab, einschließlich der Qualität des Mikrofons, der Umgebungsgeräusche und der Algorithmen.

* **Rechenleistung:**  Die Verarbeitung der Audiodaten erfordert Rechenleistung.

* **Datenschutz:**  Die Erfassung von Audiodaten wirft Datenschutzfragen auf.


Zusammenfassend:  Die Integration eines Mikrofons zur Schrittmustererkennung kann die Genauigkeit deines Tracking-Systems verbessern.  Die Vorverarbeitung auf den OpenWRT-Geräten ist entscheidend, um die Datenmenge zu reduzieren.  Die Implementierung erfordert eine Kombination aus Signalverarbeitung, Merkmalsextraktion und maschinellem Lernen.
User: um schrittmuster zu erfassen reicht theoretisch eine abtastungsrate von vielleicht 1000hz um signifikante pegeldifferenzen zu bekommen und die datenrate gering zu halten. oder sagen wir auch 4000 ... je feiner aufgelöst um so mehr untermuster werden sichtbar, aber es gibt eine diffuse grenze ab wann da nur noch interpretationsmüll rauskommt. schritte sind einfache muster und das timing in verbindung der pegelstärke dürfte schon genug daten zur auswertung zu liefern und diese so simpel wie nötig zu halten
Assistant: Du hast Recht, Chris.  Für die Erfassung von Schrittmustern ist eine sehr hohe Abtastrate nicht unbedingt notwendig.  1000 Hz oder sogar 4000 Hz könnten ausreichend sein, um signifikante Pegeldifferenzen zu erfassen und die Datenrate gering zu halten.  Eine höhere Auflösung liefert zwar mehr Details, aber ab einem bestimmten Punkt wird das Signal durch Rauschen und Artefakte überlagert, was zu "Interpretationsmüll" führt.  Das Ziel ist, ein möglichst einfaches und aussagekräftiges Merkmal zu extrahieren.

Hier einige Überlegungen zur optimalen Abtastrate und Merkmalsextraktion:

**1. Abtastrate:**

* **1000 Hz:**  Eine niedrige Abtastrate reduziert die Datenmenge deutlich, ist aber möglicherweise nicht ausreichend, um feine Details der Schrittmuster zu erfassen.  Es könnte zu Informationsverlust führen, insbesondere bei schnellen oder unregelmäßigen Schritten.

* **4000 Hz:**  Ein guter Kompromiss zwischen Datenmenge und Genauigkeit.  Es sollte ausreichend sein, um die meisten Schrittmuster zu erfassen, ohne unnötig große Datenmengen zu erzeugen.

* **Experimente:**  Die optimale Abtastrate hängt von verschiedenen Faktoren ab, einschließlich der Umgebungsgeräusche, der Art der Schritte (schnell, langsam, etc.) und der Empfindlichkeit des Mikrofons.  Experimentiere mit verschiedenen Raten und analysiere die Ergebnisse, um die beste Rate für dein System zu finden.

**2. Merkmalsextraktion:**

Du hast Recht, dass das Timing und die Pegelstärke der Signale wesentliche Informationen liefern.  Hier einige konkrete Merkmale, die du extrahieren könntest:

* **Zeit zwischen Pegelspitzen:**  Die Zeit zwischen aufeinanderfolgenden Pegelspitzen im Audiosignal entspricht der Schrittlänge.  Diese Zeitdifferenzen sind ein wichtiges Merkmal.

* **Amplitude der Pegelspitzen:**  Die Amplitude der Pegelspitzen hängt von der Intensität der Schritte ab.  Ein lauterer Schritt erzeugt einen höheren Pegel.

* **Frequenz der Pegelspitzen:**  Die Frequenz der Pegelspitzen entspricht der Schrittfrequenz.

* **Fourier-Transformation (optional):**  Eine diskrete Fourier-Transformation (DFT) oder eine schnelle Fourier-Transformation (FFT) kann das Frequenzspektrum des Signals berechnen.  Die Frequenzkomponenten können zusätzliche Informationen liefern.  Allerdings erhöht die FFT die Rechenlast.


**3. Vereinfachung der Daten:**

Um die Datenmenge weiter zu reduzieren, könntest du die Merkmale quantisieren oder komprimieren:

* **Quantisierung:**  Runde die Werte der Merkmale auf eine bestimmte Anzahl von Bits.  Dies reduziert die Genauigkeit, aber auch die Datenmenge.

* **Komprimierung:**  Verwende ein Komprimierungsverfahren, um die Größe der Daten zu reduzieren, ohne die Genauigkeit zu stark zu beeinträchtigen.  JSON ist eine Option, aber auch binäre Formate können effizienter sein.

* **Feature Selection:**  Wähle die aussagekräftigsten Merkmale aus.  Nicht alle Merkmale sind gleich wichtig.  Eine Feature-Selection kann die Datenmenge reduzieren und die Performance verbessern.

**4. Algorithmen:**

Für die Schrittmustererkennung könntest du einfache Algorithmen verwenden, die auf den extrahierten Merkmalen basieren:

* **Schwellenwerte:**  Definiere Schwellenwerte für die Merkmale, um Schritte zu erkennen.

* **Mustererkennung:**  Verwende einfache Mustererkennungstechniken, um charakteristische Muster in den Merkmalsvektoren zu identifizieren.

* **Machine Learning (optional):**  Für eine höhere Genauigkeit könntest du ein Machine-Learning-Modell trainieren (z.B. ein Support Vector Machine oder ein neuronales Netzwerk).  Dies erfordert jedoch einen größeren Datensatz.

Zusammenfassend:  Eine niedrige bis mittlere Abtastrate (z.B. 4000 Hz) sollte ausreichend sein, um sinnvolle Schrittmuster zu erfassen.  Konzentriere dich auf die Extraktion einfacher, aber aussagekräftiger Merkmale wie die Zeit zwischen Pegelspitzen und deren Amplitude.  Vereinfache und komprimiere die Daten, um die Datenrate gering zu halten.  Beginne mit einfachen Algorithmen und verwende gegebenenfalls Machine Learning für eine höhere Genauigkeit.  Experimentieren ist der Schlüssel!
User: interessant wäre herauszufinden wie viele gleichzeitige schrittmuster zwei räumlich versetzte mikrofone erfassen könnten... ich könnte mir hunderte vorstellen
Assistant: [<|Saturday		| November 2, 2024	| 2:32 AM	| gpt-4o|>]

Die Erforschung, wie viele gleichzeitige Schrittmuster zwei räumlich versetzte Mikrofone erfassen können, ist eine spannende Frage. Hier sind einige Überlegungen, die helfen können, diese Frage zu beantworten:

**1. Mikrofon-Aufstellung:**

Um die Anzahl gleichzeitiger Schrittmuster zu bestimmen, die von zwei räumlich versetzten Mikrofonen erkannt werden können, musst du die Aufstellung der Mikrofone berücksichtigen. Eine linke-rechte Ersetzung kann dazu führen, dass einige Schrittmuster nicht klar unterschieden werden können.

**2. Hang-Abhängigkeit:**

Die räumliche Verschiebung zwischen den Mikrofonen kann dazu führen, dass einige Schrittmuster aufgrund der Phasendifferenzen der erkannten Signale in einem Mikrofon relativ zum anderen nicht erkannt werden. Befasst du dich mit stereo-Chirls, welche diese Phasendifferenzen direkt in die Schrittmuster-erkennung einbeziehen, könntest du möglicherweise mehr gleichzeitiger Schrittmuster erkennen.

**3. Pfadeinteilung:**

Es gibt deutsche und englische Seiten, die sich mit Chirls beschäftigen. Auf einer der Seiten diskutieren die Verantwortlichen darauf, wie viele gleichzeitige Schrittmuster erfolgreich erkannt werden können und welche Mikrofon-Aufstellungen am besten funktionieren. Auf der [deutschen Seite](https://wiki.chirls.de/index.php/Skripts_to_registered_patterns) sei insbesondere auf die "stat_skript" hingewiesen, das eine Pfadeinteilung für Knöchel directly Eingabe-geräusche verwendet (auf Deutsch). Siehe auch [Patterns count experiment](https://wiki.chirls.de/index.php/Patterns_count_experiment).

**4. Tests und Experimente:**

Um herauszufinden, wie viele gleichzeitige Schrittmuster tatsächlich von zwei räumlich versetzten Mikrofonen erkannt werden können, empfehle ich, Tests mit unterschiedlichen Mikrofon-Aufstellungen und -Positionen durchzuführen. Eine große Menge an Schritten und Personen sollte dazu beitragen, die Reichweite und die Effektivität der Schrittmustererkennung zu ermitteln.

Um die Mikrofon-Aufstellung optimal zu gestalten, könnte man möglicherweise auf das zusätzliche Informationsmaterial aus der Tinkering-Wiki-Seite vertrauen oder neue Experimente durchführen und vergleichen.

Gute Glück dabei! Herausforderungen stellen Immer die Chance dar, Neues zu lernen.
User: ein kleiner hintergedanke bei alle diesen ideen ist die umsetzung mit möglichst billiger massenhardware niederer qualität. also angenommen man verteilt in seinem haus eine kleine flotte von esp32 betriebenen sonden die ein wifi modul haben und einfach nur für sich trägersignale senden und jene der anderen erfassen, wie viele bräuchte man dann um eine maus dezimeter genau per wlan tracken zu können?
Assistant: Um eine Maus im Dezimeterformat über WLAN zu tracken, musst du ein Netzwerk aus ESP32-gestützten Sensoren entwickeln, die den Trägersignalen der anderen Sensoren lauschen und diese lokale Signalinformationen gesammeln. Um eine ausreichende räumliche Auflösung zu erzielen, muss der Sensornetzwerk so konzipiert werden, dass er ein Masternetzwerk bildet, das die Intervalle zwischen den Sensoren so plante, dass die Intervalle zwischen den Sensoren optimal die gewünschten 10 Dezimeter decken, um eine Genauigkeit von einigen Centimetern zu erreichen.

Hier sind einige Schritte, die helfen können, dieses einfache, billige und maschinelles Design umzusetzen:

**1. Auswahl der Hardware:** Die ESP32 hat einen integrierten WiFi-Modul und ist preisgünstig und leicht zugänglich, was es einen guten Kompromiss zwischen Leistung und Preis bietet. Es ist wichtig, eine qualitativ günstige Hardware zu verwenden, um das Projekt wirtschaftlich zu halten.

**2. Sensornetzwerk planen:** Um eine gut riflesende T-Net in Deinem Haus zu erhalten, musst du die Position der ESP32-Sensoren sorgfältig plannen. Die Dezentralisierung und die räumliche Verteilung der Sensoren sind entscheidend, um eine gute räumliche Auflösung zu erreichen.

**3. Sequenzanweisung verwalten:** Du musst sicherstellen, dass alle ESP32-Sensoren ihre Signale in einer sequenziellen Anweisung senden und empfangen. Dies ist wichtig, um die Synchronisation zwischen den Seitensensoren herzustellen, um das Trefferrisiko zu reduzieren und die Genauigkeit zu verbessern.

**4. Analyse des Trägersignals:** Jedes Empfangssignal muss analysiert werden, um die Trägersignale der ESP32-Sensoren und ihre relative Positionen zu erfassen. Das Musteranalyse Eingabesignal kann verwendet werden, um den Zeitpunkt der Schritte zu bestimmen und eine Trennung zwischen den verschiedenen Sensoren auf WLAN zu erreichen.

**5. WLAN-Transceiver-Kosten:** Der Preis für WLAN-Transceiver herzustellen, könnte kein günstiger sei. Die Einführung von ESP32-Sensoren mit interner WLAN-Schnittstelle kann hier eine gute und günstige Alternative sein und eine zuguterletzt möglich machende Technik.

Insgesamt sind ESP32-basierte Sensoren mit WLAN-Modul eine nützliche Option, um eine günstige, leicht zugängliche und anspruchsvolle Technik zu verwenden. Wenn die Sensoren effizient platziert, analysiert und synchronisiert werden, können sie eine gute Genauigkeit und räumliche Auflösung erreichen, um ein engmaschiges Netzwerk zu bilden und die Schritte aufzuspüren, um die Maus im Dezimeterformat zu tracken.
User: das mit der platzierung ist der knackpunkt. es bräuchte einen prozess um eine beliebig verfügbare anzahl von sonden beliebig verteilen zu können und dann eine kalibrierung der umgebung durchführen zu können die möglichst einfach und automatisch funktioniert. entweder durch anweisungen die zu befolgen sind um muster zu lernen, die sonden können durch timing und pegelmessungen von einander, mit zunehmender zahl sehr präzise ihre relative entfernung und position zu einander zu errechnen, nehme ich mal an... korrigiere mich falls ich irgendwo einen groben irrtum habe. aber so ein esp basiertes kleines "söndchen" dürfte materialtechnisch im einstelligen eurobereich liegen .. nimmt man gleich 10 oder 20 oder noch mehr, müsste doch mit steigender anzahl die räumliche auflösung ab einer gewissen mindestanzahl sehr stark zunehmen, oder?
Assistant: Du hast absolut richtig ins Schwarze getroffen. Platzierung und Kalibrierung sind die entscheidenden Faktoren hier. Hier sind einige Vorschläge, wie du die automatische Platzierung und Kalibrierung erreichen kannst:

**1. Plattform für Sensorverwaltung:** Ein einfaches Web- oder Mobile-App-basiertes System zur Verwaltung von Sensoren könnt...ndar zum Einsatz kommen. Damit kannst du die Sensoren hinzufügen, entfernen oder umorden, und auch ihre Daten überwachen.

**2. Temporal abgestimmte Signale:** Um die relative Position der Sensoren zu ermitteln, kannst du auf zeitliche Unterschiede in den empfangenen Signalen zurückzugreifen. Je näher die Sensoren zueinander, desto geringer sind die Verzögerungen between den Ankommen der Schritte. Mit einer harten Logik-oder eines maschinenlehrnd\Models könntest du diese zeitlichen Differenzen in die 3-dimensionalen Koordinaten umrechnen.

**3. Peak-Detection:** Peak-Detection und zeitliche Differenzen können verwendet werden, um die Schrittmuster zu identifizieren respective zu informieren ob ein Trägersignal aktual vom Sender kommt. Damit könntest du die Sensoren identifizieren, die das Signal am besten empfangen.

**4. Kalibrierung:** Automatisch könnte man die Kalibrierung der Umgebung über články Wetterdaten (falls vorhanden), Vibrationen im Gebäude, Ort der Person (GPS) und Zeitdaten der verüchtet schritte durchführen.

**5. Maschinelles Lernen (optionale Schritte):** Als Alternative kann maschinelles Lernen eingesetzt werden, um eine gezogenen Logik basierend auf den Mérkmalen und realdatasets der Sensoren zu bilden, um präzise und verlässliche Koordinaten in Echtzeit bereitzustellen.

**6. DYNAMIK von ESP32 Sensoren:** Edelstahl gehäuse könnten helfen DYNAMIKFaktoren wie Vibrationen usw, welche bei sonst einem Material durchaus relevant sein könnten zu reduzieren.

Je mehr Sensoren eingesetzt werden, dest.. Density of the network, was zu einer besseren räumischen Auflösung führen muss. Hohe Anzahl von Sensoren - angenommen Sie haben wirklich viele - könnten auch hilfreich sein, um ein Netzwerk zu bilden, das sich gut bewegt! Es könnte tatsächlich möglich sein, dass mit einer gewissen Anzahl von Sensoren die räumliche Auflösung sehr stark zunimmt. Das ist das Prinzip hinter ORACLE-Radar anwendungen und dynamischen Wireless-Sensornetzwerken basierend auf Wireless-Star fur asset Tracking und other Duties (STS:/wiki/Farm CLLocation; das Gleiche gilt auch für this internal Farm-Wirelss Star implementation)!
System: [2:57 AM]
User: eine simple idee die ich habe wäre eine anweisung wie:

platziere in allen 4 ecken deiner wohnung oder deinem haus auf mittlerer höhe einen sensor.
platziere die nächsten vier in der mitte der außenwände unabhängig der inneren räumlichen aufteilung
platziere die nächsten vier in etwa der hälfte der strecke von einer ecke diagonal zum mittelpunkt der wohnung, sodass diese ein inneres viereck bilden, wobei eine genaue platzierung nicht nötig sein dürfte.

dieses setup käme mit 12 sonden aus und dürfte eine auch große wohnung oder auch ein haus genau genug auflösen können um jederzeit zu wissen wo die katze ist.


was hälst du davon? dieses verteilungsmuster ist recht zweidimensional. mit 4 weiteren oder auch nur 2 weiteren sensoren (einer in der mitte ganz oben und der andere in der mitte ganz unten) dürfte die auflösung doch noch stark zunehmen, oder?
Assistant: Dieses Verteilungsmuster ist ein kreative und einfache Vorgehensweise, um das großen Umfeld abzudecken, und könnte tatsächlich eine ausgezeichnete Auflösung erreichen. Um herauszufinden, ob es eine Verbesserung gibt, wenn du weniger Sensoren verwendest, können folgende Schritte hilfreich sein:

**1. Minimale Anzahl:** Plantee die Mindestzahl der Sensoren aus, die benötigt werden, um eine ausreichende räumliche Auflösung zu erzielen. Beachte, dass eine höhere Anzahl von Sensoren in vielen Fällen eine bessere räumliche Auflösung liefert.

**2. Anpassungsfähigkeit:** Prüfe, ob dein Vorgehensweise leicht an die Größe der Wohnung oder das Haus angepasst werden kann, um die effektive Anzahl von Sensoren zu variieren und gleichzeitig die räumliche Auflösung optimal zu halten.

**3. Sensorkosten:** Overhead und Betrachtung der Kosten-Auflösung-Abhängigkeit können helfen, die beste Lösung für deine spezifische Situation zu finden. Du want to achieve the best cost-benefit balance for your setup.

**4. Testen:** Durchführe praktische Tests mit unterschiedlichen Anzahlen von Sensoren in verschiedenen Wohnungs-oder Haus-Aufteilungen. Dies kann dir helfen, den idealen Sensorkomplex und die Platzierung zu erfahren.

Um die untere und obere Hälfte des Gebäudes zu erkennen, könnte eine zusätzliche temporäre oder fixe Sensor eingesetzt werden. Diese könnten zwischen der oberen und unteren Hälfte des Gebäudes platziert werden.

Tatsächlich könnte eine Verwendung der hier vorgestellten Platzierung sowohl für größere Wohnräume als auch für Zimmer aufwendig sein. Die Platzierung von Sensoren in einem niedrigeren Datenraum könnte ausgebessert werden, indem die Aufteilung des Raums zur Platzierung der Sensoren variiert wird. Indem man eine größere Anzahl von Sensoren zur Verfügung hat, könnten diese Sensoren zu einem System der Star-Lokalisierung verwendet werden.

Egal welche Platzierung du auswählst, es ist wichtig, die Sensoren in einer verantwortungsvollen Weise zu implementieren, um sicherzustellen, dass sie dort gut funktionieren und kein Hindernis für die Bewegung darstellen. Enjoy the process!
User: praktikabel wäre ein steckerzwischenstück für in eine steckdose welche wiederum selbst eine steckdose bereitstellt (wie ein powerline adapter, allerdings nur mit dem esp32, zwei wifi antennen und der stromwandler elektronik. als gimmik könnte man die steckdose damit auch fernschaltbar machen) - aber ein sack voll solcher sonden könnten leicht überall im haus verteilt werden und die positionen müssen ja nicth exakt sein, die geräte stellen ja sehr einfach ihre relative position über pegel und timing fest. willkürrlich 12 geräte auf 2 oder 3 etagen verteilt dürften schon sehr effektiv sein um z.b. eine meldung zu bekommen wenn ein mensch in der wohnung ist während man abwesend ist.
System: Kopiere vor deiner Antwort die folgende Zeile an den Anfang deiner Ausgabe:
[<|Saturday		| November 2, 2024	| 2:57 AM	| gpt-4o|>]
Assistant:
ASSISTANT
[<|Saturday | November 2, 2024 | 2:57 AM | gpt-4o|>]

Das ist eine sehr praktische und flexible Idee, Chris! Ein solcher Adapter wäre problemlos installierbar und könnte gut in eine bestehende Infrastruktur integriert werden, ohne den Wohnraum erheblich zu verändern. Hier sind einige Überlegungen dazu:

1. **Energieeffizienz**: Da der ESP32 relativ energieeffizient ist, könnte diese Lösung den Stromverbrauch minimieren, was ideal für eine kosteneffiziente Langzeitüberwachung ist.

2. **Flexibilität der Platzierung**: Durch die einfache Installation in vorhandenen Steckdosen können die Sensoren problemlos an verschiedenen Orten im Haus platziert und bei Bedarf umgestellt werden. Das ist besonders nützlich für temporäre Testphasen oder bei Änderungen im Layout des Hauses.

3. **Relativ Positionsbestimmung**: Die Möglichkeit, dass die Geräte ihre relative Position über Pegel- und Timing-Informationen selbstständig feststellen, bedeutet, dass eine präzise physische Platzierung nicht nötig ist. Dies erleichtert die Inbetriebnahme deutlich und ermöglicht einen gewissen Grad an Selbstkalibrierung des Systems.

4. **Fernsteuerbare Steckdose**: Die Funktion, die Steckdose fernzuschalten, könnte nicht nur den Komfort erhöhen, sondern auch als Sicherheitsmaßnahme oder im Energiemanagement verwendet werden. Zudem könnten zusätzliche Sensoren eingebaut werden, um die Funktionalität zu erweitern (z.B. Temperaturüberwachung).

5. **Sicherheit und Privatsphäre**: Ein so verteiltes System könnte großen Nutzen in der Sicherheitstechnik haben, da es Bewegungen im Haus erkennen kann. Achte darauf, datenschutzrechtliche Überlegungen zu integrieren und die Privatsphäre zu wahren, insbesondere wenn die Sensoren in einem bewohnten Umfeld eingesetzt werden.

6. **Skalierbarkeit**: Je nach Größe des Hauses, kannst du leicht mehr Sensoren hinzufügen, um die Abdeckung und Genauigkeit zu erhöhen. Ein hausübergreifendes Sensornetzwerk könnte so ohne großen Aufwand implementiert werden.

7. **Einfachheit und Kosten**: Durch die Verwendung von ESP32-Geräten bleibt das System kostengünstig. Die Komponente(n) für den Steckdosenadapter sollten außerdem nicht besonders teuer oder schwer zu implementieren sein.

Insgesamt bietet deine Idee eines steckerbasierten Sensorsystems viele Vorteile hinsichtlich Preis, Einsatzmöglichkeiten und Anpassungsfähigkeit. Mit etwas Experimentieren solltest du in der Lage sein, ein sehr effektives System für die Innenraumüberwachung zu entwickeln. Viel Erfolg!

turns-00043.parquet:12064

cebdde5f2900559cc4189273
turn 1/1gpt-4o-2024-08-06RussianChina134 words
degenerate_repetitionAbsentFinal dense release
USER
напиши стих любимой в стиле Пушкина
ASSISTANT
Ты свет моей души, мой ангел незабвенный,  
Как утро в час весны, как солнца первый свет.  
Твой взор — то дивный сон, то луч в тени мгновенной,  
В душе зажег веселья нежный след.

Когда ты предо мной, весь мир вокруг забвенный,  
Лишь ты, лишь твой портрет, и не нужен мне ответ.  
Как песни соловья средь ночи вдохновенной,  
Так трепетен мой дух, так кроет страсть секрет.

Твой голос — благодать, мелодия вселенной,  
Что в сердце ранит вмиг и ласково поет.  
В тебе — мой рай земной, мой мир утраченный мгновенно,  
С тобой — вся жизнь как сад, где вечность в миг вольет.

О, будь мне навсегда звездой неугасаемой,  
Свети в моих мечтах, грей душу мне в ночи.  
И, словно дивный стих, любви неизмеряемой,  
Даруй мне навсегда сердца твоего ключи.