import streamlit as st
import pandas as pd, numpy as np, yfinance as yf, plotly.graph_objects as go, plotly.express as px
import statsmodels.api as sm
from datetime import datetime, timedelta
from pandas_datareader import data as pdr
import warnings, requests, json
from scipy.stats import skew, kurtosis, norm
from joblib import Parallel, delayed
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
from functools import lru_cache
from concurrent.futures import ProcessPoolExecutor
from scipy.optimize import minimize
warnings.filterwarnings('ignore')

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

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

def format_asset_option(ticker, company_name):
    return f"{ticker} - {company_name}"
    
warnings.simplefilter('ignore', InsecureRequestWarning)
requests.packages.urllib3.disable_warnings()

def calculate_risk_factor_attribution(returns, factors, annualization_factor=252):
    try:
        returns = returns.rename("Portfolio_Returns").to_frame()
        returns /= 100 if returns['Portfolio_Returns'].max() > 1 else 1
        factors /= 100 if factors.max().max() > 1 else 1
        if 'RF' in factors.columns:
            returns['Portfolio_Returns'] -= factors['RF']
            factors = factors.drop(columns=['RF'])
        aligned = pd.merge(returns, factors, left_index=True, right_index=True, how='inner').dropna()
        if aligned.empty:
            raise ValueError("No overlapping dates after merging.")
        X, y = sm.add_constant(aligned[factors.columns]), aligned['Portfolio_Returns']
        model = sm.OLS(y, X).fit()
        coeffs = model.params.drop('const', errors='ignore')
        contributions = coeffs * X.mean().drop('const', errors='ignore')
        total_return = returns['Portfolio_Returns'].mean()
        if np.isclose(total_return, 0):
            st.warning("Total return is zero.")
            attribution = pd.DataFrame({'Factor': contributions.index, 'Contribution (%)': 0.0})
        else:
            attribution = pd.DataFrame({'Factor': contributions.index, 'Contribution (%)': (contributions / total_return * 100).round(2)})
        attribution = attribution[attribution['Factor'] != 'const']
        factor_names = {'Mkt-RF': 'Market Risk Premium', 'SMB': 'Small Minus Big', 'HML': 'High Minus Low'}
        attribution['Factor'] = attribution['Factor'].replace(factor_names)
        comments = []
        for _, row in attribution.iterrows():
            fname, contrib = row['Factor'], row['Contribution (%)']
            if fname == 'Market Risk Premium':
                if contrib > 75:
                    comments.append(f"{fname} = {contrib}%: Extremely high sensitivity to market movements.")
                elif contrib > 50:
                    comments.append(f"{fname} = {contrib}%: High sensitivity to overall market movements.")
                elif contrib > 30:
                    comments.append(f"{fname} = {contrib}%: Notable sensitivity to market risk.")
                elif contrib > 10:
                    comments.append(f"{fname} = {contrib}%: Moderate exposure to market risk.")
                else:
                    comments.append(f"{fname} = {contrib}%: Low exposure to market risk.")
            elif fname == 'Small Minus Big':
                if contrib > 5:
                    comments.append(f"{fname} = {contrib}%: Strong tilt towards small-cap stocks.")
                elif contrib > 0:
                    comments.append(f"{fname} = {contrib}%: Mild preference for small-cap stocks.")
                elif contrib > -5:
                    comments.append(f"{fname} = {contrib}%: Slight tilt towards larger-cap stocks.")
                else:
                    comments.append(f"{fname} = {contrib}%: Clear preference for large-cap stocks.")
            elif fname == 'High Minus Low':
                if contrib > 5:
                    comments.append(f"{fname} = {contrib}%: Strong tilt toward value stocks.")
                elif contrib > 0:
                    comments.append(f"{fname} = {contrib}%: Slight positive tilt toward value stocks.")
                elif contrib > -5:
                    comments.append(f"{fname} = {contrib}%: Minor negative tilt toward growth stocks.")
                else:
                    comments.append(f"{fname} = {contrib}%: Clear preference for growth stocks.")
        attribution['Comment'] = comments
        return attribution, None
    except Exception as e:
        return pd.DataFrame(), f"Error: {e}"

def fetch_fama_french_factors(start_date, end_date):
    try:
        ff_data = pdr.get_data_famafrench('F-F_Research_Data_Factors_daily', start=start_date, end=end_date)
        if not ff_data: return pd.DataFrame()
        factors = ff_data[0].rename(columns=lambda x: x.strip()) / 100
        factors = factors.fillna(method='ffill').fillna(method='bfill')
        required = {'Mkt-RF', 'SMB', 'HML', 'RF'}
        if not required.issubset(factors.columns):
            return pd.DataFrame()
        return factors
    except:
        return pd.DataFrame()

def calculate_final_score(primary, comparison):
    metrics = ['Annualized Return (CAGR)', 'Sharpe Ratio', 'Sortino Ratio', 'Treynor Ratio', 'Calmar Ratio',
               'Alpha (annualized)', 'Information Ratio', 'Modigliani–Modigliani Measure',
               'Upside Capture Ratio', 'Gain/Loss Ratio']
    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, total = 0, 0
    for m in metrics:
        p, c = primary.get(m, "N/A"), comparison.get(m, "N/A")
        if p != "N/A" and c != "N/A":
            pv = float(p.strip('%')) if isinstance(p, str) and '%' in p else float(p)
            cv = float(c.strip('%')) if isinstance(c, str) and '%' in c else float(c)
            w = weights.get(m, 1)
            score += w * (pv / cv if cv !=0 else 0)
            total += w
    return (score / total) * 100 if total else 0

def plot_growth_comparison(cum, bench):
    try:
        common = cum.index.intersection(bench.index)
        cum, bench = cum.loc[common], bench.loc[common]
        if cum.empty or bench.empty: return
        df = pd.DataFrame({'Date': cum.index, 'Portfolio': cum.values, 'Benchmark': bench.values})
        fig = px.line(df, x='Date', y=['Portfolio', 'Benchmark'], title='Growth Comparison', 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: pass

def plot_drawdown_comparison(dd_port, dd_bench):
    try:
        df = pd.DataFrame({'Portfolio Drawdown': dd_port, 'Benchmark Drawdown': dd_bench})
        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: pass

def plot_cagr_over_time(cum, frames=['Weekly', 'Monthly', 'Quarterly', 'Annually']):
    try:
        freq_map = {'Weekly': 'W', 'Monthly': 'M', 'Quarterly': 'Q', 'Annually': 'Y'}
        fig = go.Figure()
        for tf in frames:
            freq = freq_map.get(tf)
            if not freq: continue
            rolled = cum.resample(freq).last()
            years = (rolled.index[-1] - rolled.index[0]).days / 365.25
            if years <= 0: 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: pass

def backtest(weights, prices, freq='M', fee=0.0, debug=False):
    try:
        returns = prices.pct_change().dropna()
        if returns.empty: return pd.Series(dtype=float), pd.Series(dtype=float)
        portfolio = returns.dot(weights)
        rebalance = returns.resample(freq).last().index
        portfolio.loc[rebalance] -= fee / 100
        cum = (1 + portfolio).cumprod()
        return portfolio, cum
    except: return pd.Series(dtype=float), pd.Series(dtype=float)

def calculate_simple_metrics(returns, cum, start=10000):
    metrics = {'Start Balance': f"${start:,.2f}"}
    if not cum.empty:
        end = start * cum.iloc[-1]
        metrics['End Balance'] = f"${end:,.2f}"
        years = (cum.index[-1] - cum.index[0]).days / 365.25
        cagr = (end / start) ** (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"
        metrics['Best Year'] = f"{(returns.resample('Y').apply(lambda x: (1 + x).prod() - 1).max() * 100):.2f}%" if not returns.empty else "N/A"
        metrics['Worst Year'] = f"{(returns.resample('Y').apply(lambda x: (1 + x).prod() - 1).min() * 100):.2f}%" if not returns.empty 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) * 100:.2f}%" if not cum.empty else np.nan
        metrics['Sharpe Ratio'] = f"{calculate_sharpe_ratio(returns, 0.02):.2f}" if not cum.empty else np.nan
        metrics['Sortino Ratio'] = f"{calculate_sortino_ratio(returns, 0.02):.2f}" if not cum.empty else np.nan
        metrics['Gain/Loss Ratio'] = f"{calculate_gain_loss_ratio(returns):.2f}" if not cum.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"
    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
    return metrics

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

def calculate_sortino_ratio(returns, rf=0.02):
    er, downside = returns.mean() * 252 - rf, returns[returns < 0].std() * np.sqrt(252)
    return er / downside if downside != 0 else np.nan

def calculate_beta(returns, benchmark):
    if returns.empty or benchmark.empty: return np.nan
    cov = np.cov(returns, benchmark)[0,1]
    var = np.var(benchmark)
    return cov / var if var !=0 else np.nan

def calculate_alpha(returns, benchmark, rf=0.02):
    beta = calculate_beta(returns, benchmark)
    pr, br = returns.mean() * 252, benchmark.mean() * 252
    return pr - (rf + beta * (br - rf)) if not np.isnan(beta) else np.nan

def calculate_r_squared(returns, benchmark):
    if returns.empty or benchmark.empty: return np.nan
    cov = np.cov(returns, benchmark)
    return (cov[0,1]**2) / (cov[0,0] * cov[1,1]) if (cov[0,0] * cov[1,1]) !=0 else np.nan

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

def generate_recommendations(allocations, selected):
    recs = []
    max_a, min_a, total = max(allocations, default=0), min(allocations, default=0), sum(allocations)
    if max_a > 40.0:
        idx = allocations.index(max_a)
        recs.append(f"🔹 Reduce allocation to **{selected[idx]}** ({max_a:.2f}%).")
    if min_a < 5.0:
        idx = allocations.index(min_a)
        recs.append(f"🔸 Increase allocation to **{selected[idx]}** to at least **5.00%**.")
    if total < 100.0:
        recs.append("🔹 Allocate remaining funds to reach **100%**.")
    return recs

def get_tickers():
    try:
        sp500 = pd.read_html(requests.get('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies', verify=False).text)[0][['Symbol', 'Security']].rename(columns={'Symbol': 'Ticker', 'Security': 'Company Name'})
        sp500['Ticker'] = sp500['Ticker'].str.replace('.', '-', regex=False)
        nasdaq100_tables = pd.read_html(requests.get('https://en.wikipedia.org/wiki/NASDAQ-100', verify=False).text)
        nasdaq100 = pd.concat([table[['Ticker', 'Company']].rename(columns={'Ticker': 'Ticker', 'Company': 'Company Name'}) for table in nasdaq100_tables if 'Ticker' in table.columns and 'Company' in table.columns], ignore_index=True)
        nasdaq100['Ticker'] = nasdaq100['Ticker'].str.replace('.', '-', regex=False)
        combined = pd.concat([sp500, nasdaq100], ignore_index=True).drop_duplicates('Ticker').sort_values('Ticker').reset_index(drop=True)
        return combined
    except:
        return pd.DataFrame(columns=['Ticker', 'Company Name'])

@st.cache_data(show_spinner=False)
def download_data(tickers, start, end, retries=3, backoff=0.3):
    try:
        session = requests.Session()
        session.mount('http://', HTTPAdapter(max_retries=Retry(total=retries, backoff_factor=backoff, status_forcelist=(500,502,504))))
        session.mount('https://', HTTPAdapter(max_retries=Retry(total=retries, backoff_factor=backoff, status_forcelist=(500,502,504))))
        data = yf.download(tickers, start=start, end=end, progress=False, session=session)['Adj Close']
        data = data.to_frame() if isinstance(data, pd.Series) else data
        if data.empty: return pd.DataFrame()
        data = data.fillna(method='ffill').fillna(method='bfill')
        return data
    except:
        return pd.DataFrame()

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

def calculate_calmar_ratio(returns, cum):
    ar = returns.mean() * 252
    mdd = drawdown(cum)
    return ar / abs(mdd) if mdd !=0 else np.nan

def calculate_modigliani_miller(returns, benchmark, rf=0.02):
    sharpe = calculate_sharpe_ratio(returns, rf)
    alpha = calculate_alpha(returns, benchmark, rf)
    return alpha / sharpe if sharpe !=0 and not np.isnan(alpha) else np.nan

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

def get_drawdown_details(cum):
    drawdowns = []
    peak, peak_date = cum.iloc[0], cum.index[0]
    trough, trough_date = peak, peak_date
    for date, value in cum.items():
        if value > peak:
            if trough < peak:
                recovery = cum[cum >= peak].loc[trough_date:]
                if not recovery.empty:
                    recovery_date = recovery.index[0]
                    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_date - trough_date).days // 30} months",
                                      'Underwater Period': f"{(recovery_date - peak_date).days // 30} months",
                                      'Drawdown': f"{((trough / peak) - 1) * 100:.2f}%"})
            peak, peak_date = value, date
            trough, trough_date = value, date
        elif value < trough:
            trough, trough_date = value, 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}%"})
    drawdowns = sorted(drawdowns, key=lambda x: float(x['Drawdown'].strip('%')), reverse=False)[:10]
    for idx, dd in enumerate(drawdowns, 1):
        dd['Rank'] = idx
    return drawdowns

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

def drawdown(cum):
    peak = cum.expanding(min_periods=1).max()
    dd = (cum / peak) - 1
    return dd.min()

def optimize_portfolio(returns, benchmark, objectives=['sharpe'], rf=0.02, max_w=1.0, min_w=0.0, target=0.0):
    def sharpe(w): 
        r, s = np.dot(w, returns.mean())*252, np.sqrt(np.dot(w.T, np.dot(returns.cov()*252, w)))
        return -r/s if s >1e-6 else 0
    def min_var(w): 
        return np.dot(w.T, np.dot(returns.cov()*252, w))
    def max_ret(w): 
        return -np.dot(w, returns.mean())*252
    def min_dd(w): 
        r = returns.dot(w)
        return calculate_cvar(r, 0.95)
    def max_alpha(w): 
        pr, br = np.dot(w, returns.mean())*252, np.dot(w, benchmark.mean())*252
        beta_val = calculate_beta(returns.dot(w), benchmark)
        alpha_val = pr - (rf + beta_val * (br - rf))
        return -alpha_val
    def min_beta(w): 
        return calculate_beta(returns.dot(w), benchmark)
    obj_map = {'sharpe': sharpe, 'min_variance': min_var, 'max_return': max_ret,
               'min_drawdown': min_dd, 'maximize_alpha': max_alpha, 'minimize_beta': min_beta}
    objs = [obj_map[obj] for obj in objectives if obj in obj_map]
    def composite(w): 
        return sum(fn(w) for fn in objs)
    constraints = [{'type': 'eq', 'fun': lambda x: np.sum(x) -1}]
    if target:
        constraints.append({'type': 'ineq', 'fun': lambda x: np.dot(x, returns.mean())*252 - target})
    bounds = tuple((min_w, max_w) for _ in range(returns.shape[1]))
    initial = np.array([1.0 / returns.shape[1]]*returns.shape[1])
    res = minimize(composite, initial, method='SLSQP', bounds=bounds, constraints=constraints)
    return res.x if res.success else None

def plot_allocation_pie(weights, assets, title='Allocation', hover_info=None):
    df = pd.DataFrame({'Asset': assets, 'Weight': weights})
    hover = ['Weight'] if hover_info == "percent+name" or hover_info == "percent" else []
    fig = px.pie(df, names='Asset', values='Weight', title=title, color='Asset',
                 color_discrete_sequence=px.colors.qualitative.Set3, hover_data=hover)
    st.plotly_chart(fig, use_container_width=True)

def get_common_benchmarks(selected):
    sp500 = {"AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA"}
    nasdaq = {"AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "NVDA", "AMD"}
    s = set(selected)
    if s.issubset(sp500): 
        return {"S&P 500": "^GSPC", "Dow Jones": "^DJI", "Russell 2000": "^RUT", "Custom": "CUSTOM"}
    elif s.issubset(nasdaq): 
        return {"NASDAQ": "^IXIC", "QQQ": "QQQ", "Custom": "CUSTOM"}
    else: 
        return {"S&P 500": "^GSPC", "NASDAQ": "^IXIC", "Dow Jones": "^DJI", "Russell 2000": "^RUT", "Custom": "CUSTOM"}

def monte_carlo_simulation(returns, sims=1000, periods=252, mean=None, cov=None,
                           mean_rev=False, speed=0.1, long_mean=None,
                           var_vol=False, vol_rate=0.0,
                           shocks=None, shock_p=1,
                           dist='normal'):
    if mean is None: mean = returns.mean()
    if cov is None: cov = returns.cov()
    n = returns.shape[1]
    assets = returns.columns.tolist()
    def simulate():
        w = np.random.random(n)
        w /= w.sum()
        prices = [1]
        curr_mean, curr_vol = mean.copy(), np.sqrt(np.diag(cov))
        for t in range(periods):
            if mean_rev: curr_mean += speed * (long_mean - curr_mean)
            if var_vol: curr_vol += vol_rate
            if dist == 'log-normal':
                r = np.random.lognormal(mean=np.log(1 + curr_mean), sigma=curr_vol) -1
            else:
                r = np.random.multivariate_normal(curr_mean, np.outer(curr_vol, curr_vol) * np.corrcoef(returns.T))
            if shocks and t == shock_p:
                for i, tk in enumerate(assets):
                    r[i] += shocks.get(tk, 0)
            portfolio_r = np.dot(r, w)
            prices.append(prices[-1]*(1 + portfolio_r))
        return prices[-1] -1
    results = Parallel(n_jobs=-1)(delayed(simulate)() for _ in range(sims))
    return np.array(results)

def plot_efficient_frontier(returns, portfolios=1000, rf=0.0):
    try:
        n = returns.shape[1]
        def gen_pf(): 
            w = np.random.random(n)
            w /= w.sum()
            r = np.dot(w, returns.mean())*252
            s = np.sqrt(np.dot(w.T, np.dot(returns.cov()*252, w)))
            sharpe = (r - rf) / s if s >0 else 0
            return s, r, sharpe
        results = Parallel(n_jobs=-1)(delayed(gen_pf)() for _ in range(portfolios))
        df = pd.DataFrame(results, columns=['Std Dev', 'Return', 'Sharpe Ratio']).dropna()
        fig = px.scatter(df, x='Std Dev', y='Return', color='Sharpe Ratio',
                        color_continuous_scale='Viridis', title='Efficient Frontier',
                        labels={'Std Dev': 'Volatility', 'Return': 'Return'}, template='plotly_dark')
        max_sharpe = df.loc[df['Sharpe Ratio'].idxmax()]
        fig.add_trace(go.Scatter(x=[max_sharpe['Std Dev']], y=[max_sharpe['Return']], mode='markers+text',
                                 marker=dict(color='red', size=14, symbol='star'), name='Max Sharpe',
                                 text=["Optimized"], textposition="top center"))
        st.plotly_chart(fig, use_container_width=True)
    except: pass

def plot_correlation_heatmap(returns):
    try:
        if returns.shape[1] <2: st.warning("Need at least two assets.")
        else:
            corr = returns.corr()
            fig = px.imshow(corr, title='Correlation Heatmap', color_continuous_scale='Portland',
                            labels={'color': 'Correlation'}, template='plotly_dark')
            fig.update_xaxes(side="top")
            st.plotly_chart(fig, use_container_width=True)
    except: pass

def calculate_var(returns, cl=0.95):
    return np.percentile(returns, 100*(1 - cl))

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

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

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

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

@st.cache_data(show_spinner=False)
def calculate_portfolio_metrics(returns, cum, rf, benchmark=None):
    metrics = calculate_simple_metrics(returns, cum, rf_rate=rf)
    if benchmark is not None and not benchmark.empty:
        corr = returns.corr(benchmark)
        beta_val = calculate_beta(returns, benchmark)
        alpha_val = calculate_alpha(returns, benchmark, rf)
        metrics.update({
            'Benchmark Correlation': f"{corr:.2f}" if not np.isnan(corr) else np.nan,
            'Beta': f"{beta_val:.2f}" if not np.isnan(beta_val) else np.nan,
            'Alpha (annualized)': f"{alpha_val*100:.2f}%" if not np.isnan(alpha_val) else np.nan,
            'R2': f"{calculate_r_squared(returns, benchmark)*100:.2f}%" if not np.isnan(calculate_r_squared(returns, benchmark)) else np.nan,
            'Treynor Ratio': f"{calculate_treynor_ratio(returns, benchmark, rf):.2f}" if not np.isnan(calculate_treynor_ratio(returns, benchmark, rf)) else np.nan,
            'Calmar Ratio': f"{calculate_calmar_ratio(returns, cum):.2f}" if not np.isnan(calculate_calmar_ratio(returns, cum)) else np.nan,
            'Modigliani–Modigliani Measure': f"{calculate_modigliani_miller(returns, benchmark, rf)*100:.2f}%" if not np.isnan(calculate_modigliani_miller(returns, benchmark, rf)) else np.nan,
            'Information Ratio': f"{calculate_information_ratio(returns, benchmark):.2f}" if not np.isnan(calculate_information_ratio(returns, benchmark)) else np.nan,
            'Tracking Error': f"{calculate_tracking_error(returns, benchmark)*100:.2f}%" if not np.isnan(calculate_tracking_error(returns, benchmark)) else np.nan,
            'Active Return': f"{calculate_alpha(returns, benchmark, rf)*100:.2f}%" if not np.isnan(calculate_alpha(returns, benchmark, rf)) else np.nan,
            'Upside Capture Ratio': f"{calculate_cvar(returns, 0.95)*100:.2f}%" if not np.isnan(calculate_cvar(returns, 0.95)) else np.nan,
            'Downside Capture Ratio': f"{calculate_cvar(returns, 0.95)*100:.2f}%" if not np.isnan(calculate_cvar(returns, 0.95)) 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

def main():
    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'}
    st.set_page_config(page_title="🎯 Portfolio Optimizer", layout="wide")
    st.title("🎯 Portfolio Optimizer")
    loading_placeholder = st.empty()
    loading_placeholder.info("Loading...")
    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:
                names = [p['name'] for p in st.session_state.portfolios]
                selected = st.selectbox("Select to Edit/Delete/Duplicate", names)
                if selected:
                    portfolio = next(p for p in st.session_state.portfolios if p['name'] == selected)
                    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]
                            st.success("Deleted.")
                            if st.session_state.edit_portfolio == selected: st.session_state.edit_portfolio = None
                    with col_edit:
                        if st.button("✏️ Edit Portfolio"):
                            st.session_state.edit_portfolio = selected
                    with col_dup:
                        if st.button("📄 Duplicate Portfolio"):
                            new_name = st.text_input("New Name", f"{selected}_Copy")
                            if st.button("Confirm Duplicate"):
                                if not new_name: st.error("Name required.")
                                elif new_name in names: st.error("Name exists.")
                                else:
                                    dup = next(p for p in st.session_state.portfolios if p['name'] == selected).copy()
                                    dup['name'] = new_name
                                    st.session_state.portfolios.append(dup)
                                    st.success("Duplicated.")
            else:
                st.write("No portfolios.")
            if st.session_state.edit_portfolio:
                st.markdown("---")
                st.subheader("✏️ Edit Portfolio")
                portfolio_to_edit = next((p for p in st.session_state.portfolios if p['name'] == st.session_state.edit_portfolio), None)
                if portfolio_to_edit:
                    with st.form("edit_portfolio_form"):
                        name_edit = st.text_input("Name", portfolio_to_edit['name'])
                        start_edit = st.date_input("Start Date:", portfolio_to_edit['start_date'].date())
                        end_edit = st.date_input("End Date:", portfolio_to_edit['end_date'].date())
                        rf_edit = st.number_input("Risk-Free Rate (%)", 0.0, 10.0, portfolio_to_edit['rf_rate']*100) / 100
                        fee_edit = st.number_input("Broker Fee (%)", 0.0, 10.0, portfolio_to_edit['broker_fee']*100, step=0.000001) / 100
                        bench_edit = st.text_input("Benchmark Symbol", portfolio_to_edit['benchmark_symbol'])
                        rebalance_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))
                        st.markdown("### 🛠️ Set Allocations")
                        default_sel = [format_asset_option(t, get_company_name(tickers, t)) for t in portfolio_to_edit['selected']]
                        options = tickers.apply(lambda r: format_asset_option(r['Ticker'], r['Company Name']), axis=1).tolist()
                        selected_edit = st.multiselect("Select Assets:", options, default=default_sel)
                        selected_tickers = [opt.split(' - ')[0] for opt in selected_edit if ' - ' in opt]
                        allocations = []
                        if selected_tickers:
                            for t in selected_tickers:
                                alloc = st.number_input(f"{t} Allocation (%)", 0.0, 100.0, portfolio_to_edit['allocations'][portfolio_to_edit['selected'].index(t)] if t in portfolio_to_edit['selected'] else 0.0, step=0.000001, format="%.6f", key=f"alloc_edit_{t}")
                                allocations.append(alloc)
                            total = sum(allocations)
                            st.markdown(f"**Total Allocation:** {total:.6f}%")
                            if not np.isclose(total, 100.0, atol=1e-4):
                                st.warning("Allocations must sum to 100%. Adjusting.")
                                allocations = [a / total * 100 for a in allocations] if total >0 else [0]*len(allocations)
                        submitted_edit = st.form_submit_button("Save Changes")
                        if submitted_edit:
                            if not name_edit: st.error("Name required.")
                            elif name_edit != portfolio_to_edit['name'] and name_edit in names: st.error("Name exists.")
                            elif not selected_edit: st.error("Select assets.")
                            elif not np.isclose(sum(allocations), 100.0, atol=1e-4):
                                st.warning("Adjusting allocations.")
                                allocations = [a / total * 100 for a in allocations]
                            portfolio_to_edit.update({'name': name_edit, 'start_date': pd.to_datetime(start_edit), 'end_date': pd.to_datetime(end_edit),
                                                      'rf_rate': rf_edit, 'broker_fee': fee_edit, 'benchmark_symbol': bench_edit,
                                                      'rebalance_freq': frequency_mapping.get(rebalance_edit, 'M'),
                                                      'selected': selected_tickers, 'allocations': allocations})
                            st.success("Updated!")
                            st.session_state.edit_portfolio = None
        with col2:
            st.subheader("➕ Add New Portfolio")
            with st.form("add_portfolio_form"):
                st.markdown("### 🔧 Configure Details")
                name = st.text_input("Name")
                start = st.date_input("Start Date:", datetime.today() - timedelta(days=365*15))
                end = st.date_input("End Date:", datetime.today())
                rf = st.number_input("Risk-Free Rate (%)", 0.0, 10.0, st.session_state.default_config['rf_rate']*100) / 100
                fee = st.number_input("Broker Fee (%)", 0.0, 10.0, st.session_state.default_config['broker_fee']*100, step=0.000001) / 100
                bench = st.text_input("Benchmark Symbol", st.session_state.default_config['benchmark_symbol'])
                rebalance = st.selectbox("Rebalance Frequency", ["Daily", "Weekly", "Monthly", "Quarterly", "Yearly"], index=2)
                st.markdown("### 🛠️ Set Allocations")
                options = tickers.apply(lambda r: format_asset_option(r['Ticker'], r['Company Name']), axis=1).tolist()
                selected = st.multiselect("Select Assets:", options)
                selected_tickers = [opt.split(' - ')[0] for opt in selected]
                allocations = []
                if selected_tickers:
                    for t in selected_tickers:
                        alloc = st.number_input(f"{t} Allocation (%)", 0.0, 100.0, 0.0, step=0.000001, format="%.6f", key=f"alloc_new_{t}")
                        allocations.append(alloc)
                    total = sum(allocations)
                    st.markdown(f"**Total Allocation:** {total:.6f}%")
                submitted = st.form_submit_button("Add Portfolio")
                if submitted:
                    if not name: st.error("Name required.")
                    elif name in [p['name'] for p in st.session_state.portfolios]: st.error("Name exists.")
                    elif not selected: st.error("Select assets.")
                    elif not np.isclose(sum(allocations), 100.0, atol=1e-4): st.error("Allocations must sum to 100%.")
                    else:
                        st.session_state.portfolios.append({'name': name, 'start_date': pd.to_datetime(start),
                                                           'end_date': pd.to_datetime(end), 'rf_rate': rf,
                                                           'broker_fee': fee, 'benchmark_symbol': bench,
                                                           'rebalance_freq': frequency_mapping.get(rebalance, 'M'),
                                                           'selected': selected_tickers, 'allocations': allocations})
                        st.success("Added!")
                        st.json({t: f"{a}%" for t,a in zip(selected_tickers, allocations)})
        with st.expander("🔍 Debug Information"):
            st.write(st.session_state.portfolios)
        with st.expander("📖 Documentation"):
            st.markdown("""
            ### Documentation

            **Portfolio Configuration:**
            - **Portfolio Name:** Unique name.
            - **Risk-Free Rate:** e.g., 2%.
            - **Sharpe Ratio:** (Rp - Rf)/σp
            - **VaR:** Maximum loss at confidence level.
            - **CVaR:** Average loss beyond VaR.
            - **Sortino Ratio:** (Rp - Rf)/σd
            - **Maximum Drawdown:** Largest peak-trough decline.
            - **Alpha:** Rp - [Rf + β(Rm - Rf)]
            - **Beta:** Cov(Rp, Rm)/Var(Rm)
            - **Information Ratio:** (Rp - Rb)/TE
            - **Gain/Loss Ratio:** Total gains / total losses.
            - **Modigliani–Modigliani Measure (M²):** α / Sharpe Ratio
            - **Tracking Error:** Std dev of (Rp - Rm)
            - **Upside/Downside Capture Ratios:** Capture of benchmark's movements.
            - **Risk Factor Attribution:** Decomposition by factors.
            - **Performance Attribution:** Contribution by assets.
            - **Recommendation Engine:** Suggestions based on allocations.
            """)

    elif step == "Run Backtest":
        st.title("📊 Run Backtest")
        if st.session_state.portfolios:
            with st.form("compare_form"):
                st.subheader("🔍 Select Portfolios to Compare")
                cmp_type = st.selectbox("Comparison Type", ["Portfolio vs Benchmark", "Portfolio vs Portfolio"])
                names = [p['name'] for p in st.session_state.portfolios]
                if cmp_type == "Portfolio vs Benchmark": 
                    p1 = st.selectbox("Select Portfolio", names)
                else: 
                    p1 = st.selectbox("First Portfolio", names, key="p1")
                    p2 = st.selectbox("Second Portfolio", names, key="p2")
                st.subheader("⚙️ Visualization Settings")
                time_frames = st.multiselect("CAGR Time Frames", ['Weekly', 'Monthly', 'Quarterly', 'Annually'], default=['Annually'])
                rolling_periods = st.multiselect("Rolling Periods (Days)", [30,90,180,252], default=[252])
                run = st.form_submit_button("Run Backtest")
            if run:
                with st.spinner("Backtesting..."):
                    if cmp_type == "Portfolio vs Benchmark":
                        p = next(p for p in st.session_state.portfolios if p['name'] == p1)
                        data = download_data(p['selected'], p['start_date'], p['end_date'])
                        if data.empty: 
                            st.error("No data.")
                        else:
                            avail = [t for t in p['selected'] if t in data.columns]
                            miss = list(set(p['selected']) - set(avail))
                            if miss: 
                                st.warning(f"Excluded: {', '.join(miss)}")
                            if not avail: 
                                st.error("No valid tickers.")
                            else:
                                alloc = [p['allocations'][p['selected'].index(t)] for t in avail]
                                if not np.isclose(sum(alloc),100.0, atol=1e-4): 
                                    alloc = [a / sum(alloc)*100 for a in alloc]
                                w = np.array(alloc) / 100
                                r, cum = backtest(w, data[avail], p['rebalance_freq'], p['broker_fee'])
                                bench = p['benchmark_symbol']
                                bench_data = download_data([bench], p['start_date'], p['end_date'])
                                if bench not in bench_data.columns:
                                    st.error(f"Benchmark {bench} data unavailable.")
                                else:
                                    bench_r = bench_data[bench].pct_change().dropna().reindex(r.index, method='ffill').dropna()
                                    if bench_r.empty: 
                                        st.error("No overlapping dates.")
                                    else:
                                        r, cum = r.loc[bench_r.index], cum.loc[bench_r.index]
                                        returns = r.rename("Portfolio_Returns")
                                        bench_r = bench_r.rename("Benchmark")
                                        cum_bench = (1 + bench_r).cumprod()
                                        metrics = calculate_portfolio_metrics(returns, cum, p['rf_rate'], bench_r)
                                        with st.container():
                                            st.markdown("### 🔑 Key Metrics")
                                            key = {
                                                'Annualized Return (CAGR)': metrics.get('Annualized Return (CAGR)', "N/A"),
                                                'Sharpe Ratio': metrics.get('Sharpe Ratio', "N/A"),
                                                'Maximum Drawdown': metrics.get('Maximum Drawdown', "N/A")
                                            }
                                            cols = st.columns(len(key))
                                            for col, (k, v) in zip(cols, key.items()):
                                                with col: 
                                                    st.metric(label=k, value=v)
                                        tabs = st.tabs(["Overview", "Performance Stats", "Advanced Metrics", "Drawdowns", "Visualizations"])
                                        with tabs[0]:
                                            st.header("📈 Overview")
                                            st.write(f"**Name:** {p['name']}, **Benchmark:** {bench}")
                                        with tabs[1]:
                                            st.header("📊 Performance Statistics")
                                            df_stats = pd.DataFrame({'Metric': list(metrics.keys()), 'Portfolio': list(metrics.values()), 'Benchmark': ["N/A"]*len(metrics)})
                                            st.table(df_stats)
                                        with tabs[2]:
                                            st.header("📋 Advanced Metrics")
                                            st.table(pd.DataFrame({'Metric': list(metrics.keys()), 'Value': list(metrics.values())}).dropna())
                                        with tabs[3]:
                                            st.header("📉 Drawdowns")
                                            dd = get_drawdown_details(cum)
                                            if dd: 
                                                st.table(pd.DataFrame(dd))
                                            else: 
                                                st.write("No drawdowns.")
                                        with tabs[4]:
                                            st.header("📊 Visualizations")
                                            plot_growth_comparison(cum, cum_bench)
                                            dd_p, dd_b = (cum / cum.expanding().max() -1)*100, (cum_bench / cum_bench.expanding().max() -1)*100
                                            plot_drawdown_comparison(dd_p, dd_b)
                                            if time_frames: 
                                                plot_cagr_over_time(cum, time_frames)
                                            plot_correlation_heatmap(data[avail].pct_change().dropna())
                                            plot_allocation_pie(w, avail)
                                            if rolling_periods: 
                                                plot_rolling_metrics(returns, windows=rolling_periods)
                                            recs = generate_recommendations(alloc, avail)
                                            for rec in recs: 
                                                st.write(rec)
                                            score = calculate_final_score(metrics, metrics)
                                            st.write(f"**Final Score:** {score:.2f} / 100")
                                        st.session_state.backtest_results = {'returns': returns, 'cum_returns': cum, 'weights_a': w, 'price_data': data[avail],
                                                                          'metrics': metrics, 'benchmark_metrics': metrics, 'cum_bench': cum_bench}
                                        st.success("Backtest completed!")
                    else:
                        if p1 == p2: 
                            st.error("Different portfolios needed.")
                        else:
                            p_a, p_b = next(p for p in st.session_state.portfolios if p['name'] == p1), next(p for p in st.session_state.portfolios if p['name'] == p2)
                            overlap_start, overlap_end = max(p_a['start_date'], p_b['start_date']), min(p_a['end_date'], p_b['end_date'])
                            if overlap_start >= overlap_end: 
                                st.error("No overlap.")
                            else:
                                data_a = download_data(p_a['selected'], overlap_start, overlap_end)
                                data_b = download_data(p_b['selected'], overlap_start, overlap_end)
                                if data_a.empty or data_b.empty: 
                                    st.error("No data.")
                                else:
                                    avail_a, avail_b = [t for t in p_a['selected'] if t in data_a.columns], [t for t in p_b['selected'] if t in data_b.columns]
                                    miss_a, miss_b = list(set(p_a['selected']) - set(avail_a)), list(set(p_b['selected']) - set(avail_b))
                                    if miss_a: 
                                        st.warning(f"Excluded from {p_a['name']}: {', '.join(miss_a)}")
                                    if miss_b: 
                                        st.warning(f"Excluded from {p_b['name']}: {', '.join(miss_b)}")
                                    if not avail_a or not avail_b: 
                                        st.error("No valid tickers.")
                                    else:
                                        alloc_a = [p_a['allocations'][p_a['selected'].index(t)] for t in avail_a]
                                        alloc_b = [p_b['allocations'][p_b['selected'].index(t)] for t in avail_b]
                                        if not np.isclose(sum(alloc_a),100.0, atol=1e-4): 
                                            alloc_a = [a / sum(alloc_a) * 100 for a in alloc_a]
                                        if not np.isclose(sum(alloc_b),100.0, atol=1e-4): 
                                            alloc_b = [a / sum(alloc_b) * 100 for a in alloc_b]
                                        w_a, w_b = np.array(alloc_a)/100, np.array(alloc_b)/100
                                        r_a, cum_a = backtest(w_a, data_a[avail_a], p_a['rebalance_freq'], p_a['broker_fee'])
                                        r_b, cum_b = backtest(w_b, data_b[avail_b], p_b['rebalance_freq'], p_b['broker_fee'])
                                        if cum_a.empty or cum_b.empty: 
                                            st.error("Empty returns.")
                                        else:
                                            common_idx = cum_a.index.intersection(cum_b.index)
                                            r_a, cum_a, r_b, cum_b = r_a.loc[common_idx], cum_a.loc[common_idx], r_b.loc[common_idx], cum_b.loc[common_idx]
                                            metrics_a = calculate_portfolio_metrics(r_a, cum_a, p_a['rf_rate'], None)
                                            metrics_b = calculate_portfolio_metrics(r_b, cum_b, p_b['rf_rate'], None)
                                            st.markdown("### 🔑 Key Metrics")
                                            key_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_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_a))
                                            for col, (k, v) in zip(cols, key_a.items()):
                                                with col: 
                                                    st.metric(label=f"{p_a['name']} {k}", value=v)
                                            for col, (k, v) in zip(cols, key_b.items()):
                                                with col: 
                                                    st.metric(label=f"{p_b['name']} {k}", value=v)
                                            tabs = st.tabs(["Overview", "Performance Stats", "Advanced Metrics", "Drawdowns", "Visualizations"])
                                            with tabs[0]:
                                                st.header("📈 Overview")
                                                st.write(f"**Portfolio A:** {p_a['name']}, **Portfolio B:** {p_b['name']}")
                                            with tabs[1]:
                                                st.header("📊 Performance Statistics")
                                                st.table(pd.DataFrame({'Metric': list(metrics_a.keys()), p_a['name']: list(metrics_a.values()), p_b['name']: list(metrics_b.values())}).set_index('Metric'))
                                            with tabs[2]:
                                                st.header("📉 Advanced Metrics")
                                                st.table(pd.DataFrame({'Metric': list(metrics_a.keys()), p_a['name']: list(metrics_a.values()), p_b['name']: list(metrics_b.values())}).set_index('Metric'))
                                            with tabs[3]:
                                                st.header("📉 Drawdowns")
                                                da = get_drawdown_details(cum_a)
                                                db = get_drawdown_details(cum_b)
                                                if da: 
                                                    st.table(pd.DataFrame(da))
                                                else: 
                                                    st.write("No drawdowns.")
                                                if db: 
                                                    st.table(pd.DataFrame(db))
                                                else: 
                                                    st.write("No drawdowns.")
                                            with tabs[4]:
                                                st.header("📊 Visualizations")
                                                plot_growth_comparison(cum_a, cum_b)
                                                dd_a, dd_b = (cum_a / cum_a.expanding().max() -1)*100, (cum_b / cum_b.expanding().max() -1)*100
                                                plot_drawdown_comparison(dd_a, dd_b)
                                                plot_correlation_heatmap(data_a[avail_a].pct_change().dropna())
                                                plot_correlation_heatmap(data_b[avail_b].pct_change().dropna())
                                                plot_allocation_pie(w_a, avail_a, title=f"{p_a['name']} Allocation")
                                                plot_allocation_pie(w_b, avail_b, title=f"{p_b['name']} Allocation")
                                                recs = generate_recommendations(alloc_a, avail_a) + generate_recommendations(alloc_b, avail_b)
                                                for rec in recs: 
                                                    st.write(rec)
                                                score_a, score_b = calculate_final_score(metrics_a, metrics_b), calculate_final_score(metrics_b, metrics_a)
                                                st.write(f"**{p_a['name']}:** {score_a:.2f} / 100")
                                                st.write(f"**{p_b['name']}:** {score_b:.2f} / 100")
                                            st.session_state.backtest_results = {'returns_a': r_a, 'cum_a': cum_a, 'weights_a': w_a, 'data_a': data_a[avail_a],
                                                                              'metrics_a': metrics_a, 'returns_b': r_b, 'cum_b': cum_b,
                                                                              'weights_b': w_b, 'data_b': data_b[avail_b], 'metrics_b': metrics_b}
                                        st.success("Backtest completed!")
        else:
            st.warning("Add portfolios first.")
    
    elif step == "Optimize Portfolio":
        st.title("🔧 Optimize Portfolio")
        if st.session_state.portfolios:
            names = [p['name'] for p in st.session_state.portfolios]
            sel = st.selectbox("Select Portfolio", names)
            p = next(p for p in st.session_state.portfolios if p['name'] == sel)
            data = download_data(p['selected'], p['start_date'], p['end_date'])
            if data.empty: 
                st.error("No data.")
            else:
                avail = [t for t in p['selected'] if t in data.columns]
                miss = list(set(p['selected']) - set(avail))
                if miss: 
                    st.warning(f"Excluded: {', '.join(miss)}")
                if not avail: 
                    st.error("No valid tickers.")
                else:
                    returns = data[avail].pct_change().dropna()
                    with st.form("optimization_form"):
                        st.subheader("🔧 Optimization Constraints")
                        max_w = st.slider("Max Weight per Asset (%)", 0.0, 100.0, 100.0) / 100
                        min_w = st.slider("Min Weight per Asset (%)", 0.0, 100.0, 0.0) / 100
                        if min_w > max_w: 
                            st.error("Min weight > max weight.")
                            st.stop()
                        objectives = st.multiselect("Objectives", ["Sharpe Ratio", "Minimum Variance", "Maximum Return", "Minimum Drawdown", "Maximize Alpha", "Minimize Beta"], default=["Sharpe Ratio"])
                        target = st.number_input("Target Annual Return (%)", 0.0, 100.0, 10.0) / 100
                        submitted = st.form_submit_button("Run Optimization")
                    if submitted:
                        objs = [obj.lower().replace(' ', '_') for obj in objectives]
                        bench = download_data([p['benchmark_symbol']], p['start_date'], p['end_date'])
                        if bench.empty: 
                            st.error("Benchmark data unavailable.")
                        else:
                            bench_r = bench[p['benchmark_symbol']].pct_change().dropna().reindex(returns.index, method='ffill').dropna()
                            if bench_r.empty: 
                                st.error("No overlapping benchmark data.")
                            else:
                                weights = optimize_portfolio(returns, bench_r, objectives=objs, rf=p['rf_rate'], max_w=max_w, min_w=min_w, target=target)
                                if weights is not None:
                                    total = weights.sum()
                                    if not np.isclose(total,1.0, atol=1e-4): 
                                        st.warning("Adjusting allocations.")
                                        weights = weights / total
                                    st.success("Optimized!")
                                    st.table(pd.DataFrame({'Asset': avail, 'Weight': weights * 100}).round(2))
                                    plot_allocation_pie(weights, avail, title="Optimized Allocation")
                                    r_opt, s_opt = np.dot(weights, returns.mean())*252, np.sqrt(np.dot(weights.T, np.dot(returns.cov()*252, weights)))
                                    sharpe_opt = (r_opt - p['rf_rate']) / s_opt if s_opt >1e-6 else np.nan
                                    st.markdown("### 📈 Optimized Metrics")
                                    st.markdown(f"- **Expected Annual Return (CAGR):** {r_opt*100:.2f}%")
                                    st.markdown(f"- **Annualized Volatility (%):** {s_opt*100:.2f}%")
                                    st.markdown(f"- **Sharpe Ratio:** {sharpe_opt:.2f}")
                                    st.session_state.backtest_results.update({'optimized_weights': weights, 'optimized_return': r_opt, 'optimized_volatility': s_opt, 'optimized_sharpe': sharpe_opt})
                        else:
                            st.error("Optimization failed.")
        else:
            st.warning("Add portfolios first.")

    elif step == "Monte Carlo Simulations":
        st.title("📈 Monte Carlo Simulations")
        if st.session_state.portfolios:
            names = [p['name'] for p in st.session_state.portfolios]
            sel = st.selectbox("Select Portfolio", names)
            p = next(p for p in st.session_state.portfolios if p['name'] == sel)
            data = download_data(p['selected'], p['start_date'], p['end_date'])
            if data.empty: 
                st.error("No data.")
            else:
                avail = [t for t in p['selected'] if t in data.columns]
                miss = list(set(p['selected']) - set(avail))
                if miss: 
                    st.warning(f"Excluded: {', '.join(miss)}")
                if not avail: 
                    st.error("No valid tickers.")
                else:
                    returns = data[avail].pct_change().dropna()
                    with st.tabs(["🔧 Settings", "📊 Results"]):
                        with st.tabs[0]:
                            with st.form("mc_simulation_form"):
                                sims = st.number_input("Simulations", 1, 10000, 1000)
                                periods = st.number_input("Periods", 1, 252, 252)
                                dist = st.selectbox("Distribution", ["normal", "log-normal"])
                                mr = st.checkbox("Mean Reversion")
                                speed = st.slider("Reversion Speed", 0.0,1.0,0.1) /100 if mr else 0.0
                                long_mean = st.number_input("Long-term Mean Return (%)", 0.0) / 100 if mr else None
                                var_vol = st.checkbox("Time-Varying Volatility")
                                vol_rate = st.number_input("Volatility Change Rate", 0.0) / np.sqrt(252) if var_vol else 0.0
                                stress = st.checkbox("Enable Stress Testing")
                                shocks = {}
                                if stress:
                                    shocks = {t: st.number_input(f"Shock to {t} (%)", -100.0,100.0,0.0)/100 for t in avail}
                                submitted = st.form_submit_button("Run Simulations")
                        with st.tabs[1]:
                            if 'simulated_returns' in st.session_state:
                                sims_r = st.session_state.simulated_returns
                                st.markdown("### 📊 Simulation Results")
                                st.write(pd.Series(sims_r).describe())
                                fig = px.histogram(
                                    sims_r, nbins=50, title='Returns Distribution with VaR and CVaR',
                                    labels={'value': 'Returns', 'count': 'Frequency'}, template='plotly_dark'
                                )
                                var, cvar = calculate_var(sims_r), calculate_cvar(sims_r)
                                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)
                                fig_cdf = px.ecdf(
                                    sims_r, title='Cumulative Distribution of Simulated Returns',
                                    labels={'x': 'Returns', 'y': 'CDF'}, template='plotly_dark'
                                )
                                st.plotly_chart(fig_cdf, use_container_width=True)
                                fig_box = px.box(
                                    pd.DataFrame(sims_r, columns=['Returns']),
                                    y='Returns', title='Box Plot of Simulated Returns',
                                    template='plotly_dark'
                                )
                                st.plotly_chart(fig_box, use_container_width=True)
                                summary_stats = pd.DataFrame({
                                    'Statistic': ['Mean', 'Median', 'Standard Deviation', 'Minimum', 'Maximum'],
                                    'Value': [
                                        f"{np.mean(sims_r):.4f}",
                                        f"{np.median(sims_r):.4f}",
                                        f"{np.std(sims_r):.4f}",
                                        f"{np.min(sims_r):.4f}",
                                        f"{np.max(sims_r):.4f}"
                                    ]
                                })
                                st.table(summary_stats)
                                st.markdown("""
                                **Interpretation:**
                                - **Descriptive Statistics:** Overview of the distribution of simulated portfolio returns.
                                - **Returns Distribution Histogram:** Frequency of different return outcomes.
                                - **Cumulative Distribution Function (CDF):** Probability that a return is less than or equal to a particular value.
                                - **Box Plot:** Highlights median, quartiles, and potential outliers.
                                - **Summary Statistics Table:** Key metrics from simulations.
                                """)
                            else:
                                st.info("Run simulations first.")
                        if st.form_submit_button("Run Simulations"):
                            with st.spinner("Simulating..."):
                                shocks = shocks if 'shocks' in locals() else None
                                sims_r = monte_carlo_simulation(
                                    returns, sims, periods, mean=returns.mean(), cov=returns.cov(),
                                    mean_rev=mr, speed=speed, long_mean=long_mean,
                                    var_vol=var_vol, vol_rate=vol_rate,
                                    shocks=shocks, shock_p=1, dist=dist
                                )
                                st.session_state.simulated_returns = sims_r
                                st.success("Simulations completed!")
        else:
            st.warning("Add portfolios first.")
    
    elif step == "Risk Analysis":
        st.title("⚠️ Risk Analysis")
        if st.session_state.portfolios:
            names = [p['name'] for p in st.session_state.portfolios]
            sel = st.selectbox("Select Portfolio", names)
            p = next(p for p in st.session_state.portfolios if p['name'] == sel)
            data = download_data(p['selected'], p['start_date'], p['end_date'])
            if data.empty: 
                st.error("No data.")
            else:
                avail = [t for t in p['selected'] if t in data.columns]
                miss = list(set(p['selected']) - set(avail))
                if miss: 
                    st.warning(f"Excluded: {', '.join(miss)}")
                if not avail: 
                    st.error("No valid tickers.")
                else:
                    returns = data[avail].pct_change().dropna()
                    with st.tabs(["📉 Risk Metrics", "📈 Visualizations", "🛠️ Sensitivity Analysis"]):
                        with st.tabs[0]:
                            with st.form("risk_analysis_form"):
                                cl = st.slider("Confidence Level", 0.01,0.99,0.95)
                                stress = st.checkbox("Enable Stress Testing")
                                shocks = {t: st.number_input(f"Shock to {t} (%)", -100.0,100.0,0.0)/100 for t in avail} if stress else {}
                                sens = st.checkbox("Enable Sensitivity Analysis")
                                sens_adj = {t: st.number_input(f"Sensitivity {t}", -10.0,10.0,1.0)/100 for t in avail} if sens else {}
                                csens = st.checkbox("Enable Cross-Asset Sensitivity Analysis")
                                csens_adj = {t: st.number_input(f"Cross Sensitivity {t}", -10.0,10.0,0.0)/100 for t in avail} if csens else {}
                                submitted = st.form_submit_button("Calculate Risk Metrics")
                            if submitted:
                                with st.spinner("Calculating..."):
                                    adj_returns = returns.copy()
                                    for t, s in shocks.items(): adj_returns[t] += s
                                    for t, a in sens_adj.items(): adj_returns[t] += a
                                    for t, c in csens_adj.items(): adj_returns[t] += c
                                    portfolio_r = adj_returns.dot(np.array(p['allocations'])/100)
                                    var = calculate_var(portfolio_r, cl)
                                    cvar = calculate_cvar(portfolio_r, cl)
                                    skewness = skew(portfolio_r)
                                    kurt = kurtosis(portfolio_r)
                                    metrics = {
                                        'VaR': f"{var:.2f}", 
                                        'CVaR': f"{cvar:.2f}",
                                        'Skewness': f"{skewness:.2f}", 
                                        'Kurtosis': f"{kurt:.2f}",
                                        'Annualized Volatility (%)': f"{portfolio_r.std()*100*np.sqrt(252):.2f}%",
                                        'Maximum Drawdown (%)': f"{drawdown((1 + portfolio_r).cumprod())*100:.2f}%"
                                    }
                                    st.markdown("### 📈 Risk Metrics Summary")
                                    st.table(pd.DataFrame(list(metrics.items()), columns=['Metric', 'Value']))
                                    if var <= -10.0: 
                                        st.warning(f"⚠️ High VaR: {var*100:.2f}%")
                                    else: 
                                        st.success(f"✅ VaR within limits: {var*100:.2f}%")
                        with st.tabs[1]:
                            st.subheader("📊 Visualizations")
                            portfolio_r = st.session_state.get('portfolio_returns', None)
                            var, cvar = st.session_state.get('var', None), st.session_state.get('cvar', None)
                            if var is not None and cvar is not None and portfolio_r is not None:
                                plot_var_cvar_distribution(portfolio_r, var, cvar)
                                plot_var_cvar_over_time(portfolio_r, var, cvar)
                                plot_risk_return_attribution(portfolio_r, np.array(p['allocations'])/100)
                            else:
                                st.info("Calculate risk metrics to view visualizations.")
                        with st.tabs[2]:
                            st.subheader("🛠️ Sensitivity Analysis")
                            if 'portfolio_returns' in st.session_state:
                                portfolio_r = st.session_state.portfolio_returns
                                fig = px.histogram(portfolio_r, nbins=50, title='Adjusted Returns Distribution', template='plotly_dark')
                                st.plotly_chart(fig, use_container_width=True)
                                fig_rr = px.scatter(x=portfolio_r.std()*np.sqrt(252), y=portfolio_r.mean()*252, title='Risk vs Return', labels={'x': 'Risk (Std Dev)', 'y': 'Return'}, template='plotly_dark')
                                st.plotly_chart(fig_rr, use_container_width=True)
                            else:
                                st.info("Run risk metrics first.")
    main()

File "/www/wwwroot/portfolio_optimizer/portfolio_optimizer.py", line 556
                          rebalance_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))
                                                                                                                                                                                                                                                                                              ^
SyntaxError: closing parenthesis ')' does not match opening parenthesis '['

fix it and full fixed code, no need explanation