# Helper Functions

@handle_exceptions
def calculate_metrics(
    returns: pd.Series, 
    cum_returns: pd.Series, 
    rf: float = 0.02, 
    benchmark_returns: pd.Series = None
) -> dict:
    """
    Calculate portfolio performance metrics with consistent data types.
    
    Args:
        returns (pd.Series): Portfolio returns
        cum_returns (pd.Series): Cumulative returns
        rf (float): Risk-free rate
        benchmark_returns (pd.Series, optional): Benchmark returns for comparison
    
    Returns:
        dict: Dictionary of metrics with consistent data types
    """
    metrics = {}
    
    # Initialize with default values
    initial_balance = 10000.0
    metrics['Start Balance'] = initial_balance
    
    def safe_calculation(calculation, default="N/A"):
        """Helper function to safely perform calculations"""
        try:
            result = calculation()
            if isinstance(result, (int, float)):
                if not np.isinf(result) and not np.isnan(result):
                    return result
            return default
        except Exception:
            return default

    if not cum_returns.empty and not returns.empty:
        # Basic portfolio metrics
        end_balance = safe_calculation(
            lambda: initial_balance * cum_returns.iloc[-1]
        )
        years = (cum_returns.index[-1] - cum_returns.index[0]).days / 365.25

        metrics.update({
            'End Balance': end_balance,
            'Annualized Return (CAGR)': safe_calculation(
                lambda: (end_balance / initial_balance) ** (1 / years) - 1 if years > 0 else "N/A"
            ),
            'Best Year': safe_calculation(
                lambda: round(returns.resample('Y').apply(lambda x: (1 + x).prod() - 1).max() * 100, 2)  # Percentage
            ),
            'Worst Year': safe_calculation(
                lambda: round(returns.resample('Y').apply(lambda x: (1 + x).prod() - 1).min() * 100, 2)  # Percentage
            ),
            'Standard Deviation (Annualized)': safe_calculation(
                lambda: round(returns.std() * np.sqrt(252) * 100, 2)  # Percentage
            ),
            'Maximum Drawdown': safe_calculation(
                lambda: round(drawdown(cum_returns) * 100, 2)  # Percentage
            ),
            'Sharpe Ratio': safe_calculation(
                lambda: round(calculate_sharpe_ratio(returns, rf), 2)
            ),
            'Sortino Ratio': safe_calculation(
                lambda: round(calculate_sortino_ratio(returns, rf), 2)
            ),
            'Beta': safe_calculation(
                lambda: round(calculate_beta(returns, benchmark_returns), 2) if benchmark_returns is not None else "N/A"
            ),
            'Alpha (annualized)': safe_calculation(
                lambda: round(calculate_alpha(returns, benchmark_returns, rf) * 100, 2) if benchmark_returns is not None else "N/A"  # Percentage
            ),
            'R2': safe_calculation(
                lambda: round(calculate_r_squared(returns, benchmark_returns), 2) if benchmark_returns is not None else "N/A"
            ),
            'Calmar Ratio': safe_calculation(
                lambda: round(calculate_calmar_ratio(returns, cum_returns), 2) if calculate_calmar_ratio(returns, cum_returns) != "N/A" else "N/A"
            ),
            'Information Ratio': safe_calculation(
                lambda: round(calculate_information_ratio(returns, benchmark_returns), 2) if benchmark_returns is not None else "N/A"
            ),
            'Tracking Error': safe_calculation(
                lambda: round(calculate_tracking_error(returns, benchmark_returns) * 100, 2) if benchmark_returns is not None else "N/A"  # Percentage
            ),
            'Upside Capture Ratio': safe_calculation(
                lambda: round(calculate_capture_ratio(returns, benchmark_returns, upside=True) * 100, 2) if benchmark_returns is not None else "N/A"  # Percentage
            ),
            'Downside Capture Ratio': safe_calculation(
                lambda: round(calculate_capture_ratio(returns, benchmark_returns, upside=False) * 100, 2) if benchmark_returns is not None else "N/A"  # Percentage
            )
        })
    else:
        # Set default values for empty data
        default_metrics = {
            'End Balance': "N/A",
            'Annualized Return (CAGR)': "N/A",
            'Best Year': "N/A",
            'Worst Year': "N/A",
            'Standard Deviation (Annualized)': "N/A",
            'Maximum Drawdown': "N/A",
            'Sharpe Ratio': "N/A",
            'Sortino Ratio': "N/A",
            'Beta': "N/A",
            'Alpha (annualized)': "N/A",
            'R2': "N/A",
            'Calmar Ratio': "N/A",
            'Information Ratio': "N/A",
            'Tracking Error': "N/A",
            'Upside Capture Ratio': "N/A",
            'Downside Capture Ratio': "N/A"
        }
        metrics.update(default_metrics)

    # Ensure all metrics are present even if benchmark_returns is None
    expected_metrics = [
        'Start Balance', 'End Balance', 'Annualized Return (CAGR)', 'Best Year',
        'Worst Year', 'Standard Deviation (Annualized)', 'Maximum Drawdown',
        'Sharpe Ratio', 'Sortino Ratio', 'Beta', 'Alpha (annualized)', 'R2',
        'Calmar Ratio', 'Information Ratio', 'Tracking Error',
        'Upside Capture Ratio', 'Downside Capture Ratio'
    ]

    for metric in expected_metrics:
        if metric not in metrics:
            # Assign "N/A" for metrics that are percentage-based or related to performance
            if any(key in metric for key in [
                'Percentage', 'Return', 'Drawdown', 'Capture Ratio', 'Best Year', 'Worst Year'
            ]):
                metrics[metric] = "N/A"
            else:
                metrics[metric] = "N/A"

    return metrics

@handle_exceptions
def calculate_sharpe_ratio(returns: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Sharpe Ratio for a given set of returns.

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

    Returns:
    - float: Sharpe Ratio.
    """
    excess_return = returns.mean() * 252 - rf
    std_dev = returns.std() * np.sqrt(252)
    return excess_return / std_dev if std_dev != 0 else np.nan

@handle_exceptions
def calculate_sortino_ratio(returns: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Sortino Ratio for a given set of returns.

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

    Returns:
    - float: Sortino Ratio.
    """
    excess_return = returns.mean() * 252 - rf
    downside_std = returns[returns < 0].std() * np.sqrt(252)
    return excess_return / downside_std if downside_std != 0 else np.nan

@handle_exceptions
def calculate_beta(returns: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the Beta of the portfolio relative to the benchmark.

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

    Returns:
    - float: Beta value.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance_matrix = np.cov(returns, benchmark_returns)
    covariance = covariance_matrix[0, 1]
    benchmark_variance = covariance_matrix[1, 1]
    return covariance / benchmark_variance if benchmark_variance != 0 else np.nan

@handle_exceptions
def calculate_alpha(returns: pd.Series, benchmark_returns: pd.Series, rf: float = 0.02) -> float:
    """
    Calculate the Alpha of the portfolio relative to the benchmark.

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

    Returns:
    - float: Alpha value.
    """
    beta = calculate_beta(returns, benchmark_returns)
    portfolio_return = returns.mean() * 252
    benchmark_return = benchmark_returns.mean() * 252
    return portfolio_return - (rf + beta * (benchmark_return - rf)) if not np.isnan(beta) else np.nan

@handle_exceptions
def calculate_r_squared(returns: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the R-squared of the portfolio relative to the benchmark.

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

    Returns:
    - float: R-squared value.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    covariance = np.cov(returns, benchmark_returns)
    var_port = covariance[0,0]
    var_bench = covariance[1,1]
    cov = covariance[0,1]
    return (cov ** 2) / (var_port * var_bench) if var_port !=0 and var_bench !=0 else np.nan

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

@handle_exceptions
def calculate_tracking_error(returns: pd.Series, benchmark_returns: pd.Series) -> float:
    """
    Calculate the Tracking Error of the portfolio relative to the benchmark.
    
    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.
    
    Returns:
    - float: Tracking Error.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    return np.std((returns - benchmark_returns)) * np.sqrt(252)

@handle_exceptions
def calculate_capture_ratio(returns: pd.Series, benchmark_returns: pd.Series, upside: bool = True) -> float:
    """
    Calculate the Upside or Downside Capture Ratio of the portfolio relative to the benchmark.
    
    Parameters:
    - returns (pd.Series): Daily returns of the portfolio.
    - benchmark_returns (pd.Series): Daily returns of the benchmark.
    - upside (bool): If True, calculate Upside Capture Ratio; else, Downside.
    
    Returns:
    - float: Capture Ratio.
    """
    if returns.empty or benchmark_returns.empty:
        return np.nan
    if upside:
        benchmark_positive = benchmark_returns > 0
        if benchmark_positive.sum() == 0:
            return np.nan
        return (returns[benchmark_positive].mean() / benchmark_returns[benchmark_positive].mean()) * 100
    else:
        benchmark_negative = benchmark_returns < 0
        if benchmark_negative.sum() == 0:
            return np.nan
        return (returns[benchmark_negative].mean() / benchmark_returns[benchmark_negative].mean()) * 100

# Backtest Function

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

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

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

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

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

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

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

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

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

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

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

problem:
wrong calculation of the metrics or many N/A:
Annualized Return (CAGR) 0.26% 0.17%
Sharpe Ratio 0.87 0.66
Sortino Ratio 1.16 0.85
Maximum Drawdown -43.38% -36.40%
Beta N/A N/A
Alpha (annualized) N/A N/A
Information Ratio N/A N/A
R2 N/A N/A
Calmar Ratio -2.00 -1.81
Tracking Error N/A N/A
Upside Capture Ratio N/A N/A
Downside Capture Ratio N/A N/A
Best Year 77.40% 43.64%
Worst Year -38.55% -33.10%
if the segments code need update, please return the full updated segments code, no need import or other stuff, only the full version of the updated segment