# app.py # ============================================ # Simulatore Monte Carlo di un PAC e di Azioni/ETF/Cripto (dati Yahoo) - single file Streamlit app # Autori: Giovanni Vignola e GPT-5 Plus # # Requisiti: # pip install streamlit yfinance numpy pandas matplotlib # Avvio: # streamlit run app.py # # Note importanti: # - Questo strumento è SOLO a scopo didattico, non è consulenza finanziaria. # - Utilizziamo rendimenti LOG-normali: i log-rendimenti sono gaussiani (Normali), non i rendimenti semplici. # - La tassazione è semplificata e applicata a fine periodo sui soli guadagni (non gestisce compensazioni/minusvalenze). # - I dati Yahoo possono essere ritardati; l'aggiornamento dipende dal fuso orario del mercato di riferimento. # === ENV HOTFIX per Hugging Face Spaces (da mettere PRIMA di importare streamlit/matplotlib) === import os, tempfile _tmp = tempfile.gettempdir() def _first_writable(paths): for p in paths: if not p: continue try: os.makedirs(p, exist_ok=True) # test scrittura testfile = os.path.join(p, ".write_test") with open(testfile, "w") as f: f.write("ok") os.remove(testfile) return p except Exception: continue return None # 1) Directory di config Streamlit (dove Streamlit prova a scrivere ~/.streamlit/*) # Ordine di tentativi: variabile già impostata -> $HF_HOME/.streamlit -> $HOME/.streamlit -> /tmp/.streamlit if not os.environ.get("STREAMLIT_CONFIG_DIR"): candidates = [ os.environ.get("STREAMLIT_CONFIG_DIR"), os.path.join(os.environ.get("HF_HOME",""), ".streamlit"), os.path.join(os.environ.get("HOME",""), ".streamlit"), os.path.join(_tmp, ".streamlit"), ] cfg = _first_writable(candidates) if cfg is None: # fallback finale cfg = os.path.join(_tmp, ".streamlit") os.makedirs(cfg, exist_ok=True) os.environ["STREAMLIT_CONFIG_DIR"] = cfg # 2) MPLCONFIGDIR per Matplotlib if not os.environ.get("MPLCONFIGDIR"): mplcfg = _first_writable([ os.path.join(os.environ.get("XDG_CONFIG_HOME",""), "matplotlib"), os.path.join(os.environ.get("HOME",""), ".config", "matplotlib"), os.path.join(_tmp, "mplconfig"), ]) or os.path.join(_tmp, "mplconfig") os.makedirs(mplcfg, exist_ok=True) os.environ["MPLCONFIGDIR"] = mplcfg # 3) XDG dirs (evita scritture in /) os.environ.setdefault("XDG_CONFIG_HOME", os.path.join(_tmp, "xdgconfig")) os.environ.setdefault("XDG_CACHE_HOME", os.path.join(_tmp, "xdgcache")) # 4) opzionale: niente usage stats (non influisce sul path ma evita altre scritture) os.environ.setdefault("STREAMLIT_BROWSER_GATHER_USAGE_STATS", "false") import math from typing import Tuple, Optional, List, Dict import numpy as np import pandas as pd import matplotlib.pyplot as plt import streamlit as st # === PRESETS === # Preset tickers (indici, ETF, cripto) PRESETS = { "—": "", "Indici (non investibili)": { "S&P 500 (Index)": "^GSPC", "NASDAQ-100 (Index)": "^NDX", "Nasdaq Composite (Index)": "^IXIC", "Dow Jones (Index)": "^DJI", "EURO STOXX 50 (Index)": "^STOXX50E", }, "ETF (US)": { "SPDR S&P 500 ETF (SPY)": "SPY", "iShares Core S&P 500 (IVV)": "IVV", "Vanguard S&P 500 (VOO)": "VOO", }, "ETF (UCITS EU)": { "Vanguard S&P 500 UCITS (VUSA.L)": "VUSA.L", "iShares Core MSCI World (IWDA.AS)": "IWDA.AS", "iShares Core MSCI EM IMI (EIMI.L)": "EIMI.L", "iShares Core MSCI World (SWDA.L)": "SWDA.L", }, "Cripto": { "Bitcoin (BTC-USD)": "BTC-USD", "Ethereum (ETH-USD)": "ETH-USD", "Solana (SOL-USD)": "SOL-USD" } } def preset_selector(label, key_out): grp = st.selectbox(label + " – gruppo", list(PRESETS.keys()), index=0, key=label+"_grp") if grp == "—": return name = st.selectbox(label + " – preset", list(PRESETS[grp].keys()), key=label+"_name") ticker_val = PRESETS[grp][name] st.write(f"Ticker selezionato: **{ticker_val}**") if st.button("Usa questo ticker", key=label+"_apply"): st.session_state[key_out] = ticker_val # yfinance è opzionale: gestiamo un errore amichevole se non presente try: import yfinance as yf YF_OK = True except Exception: YF_OK = False # ----------------------------- # Stili UI ad alto contrasto # ----------------------------- def inject_css(): st.markdown( """ """, unsafe_allow_html=True ) # ----------------------------- # Helpers Yahoo Finance # ----------------------------- def get_ticker_fast_price(ticker: "yf.Ticker") -> Optional[float]: """Prova ad ottenere un 'last price' intraday/near-RT da yfinance in modo robusto.""" try: fi = getattr(ticker, "fast_info", None) if fi: # fast_info può essere dict-like o oggetto for key in ("last_price", "lastPrice", "regularMarketPrice"): try: val = fi[key] if isinstance(fi, dict) else getattr(fi, key, None) except Exception: val = None if val is not None and not math.isnan(val): return float(val) except Exception: pass # fallback su prezzo più recente disponibile try: h = ticker.history(period="5d", interval="1m") if isinstance(h, pd.DataFrame) and not h.empty: return float(h["Close"].dropna().iloc[-1]) except Exception: pass try: h = ticker.history(period="1d") if isinstance(h, pd.DataFrame) and not h.empty: return float(h["Close"].dropna().iloc[-1]) except Exception: pass return None def load_yahoo_monthly(ticker_str: str, years: int = 10) -> Tuple[pd.DataFrame, Optional[float], Optional[str]]: """Scarica dati giornalieri (auto-adjust) e crea mensili (ultimo close per mese) + log-ret mensili. Ritorna: (df_mensile, last_price, currency)""" if not YF_OK: raise RuntimeError("yfinance non è installato. Esegui: pip install yfinance") ticker = yf.Ticker(ticker_str) df = ticker.history(period=f"{max(years,1)}y", interval="1d", auto_adjust=True) if df is None or df.empty: raise ValueError("Nessun dato disponibile per il ticker specificato.") df = df.rename(columns=str.title) # Close, Open, etc. # >>> FIX: rendiamo l'indice timezone-naive per evitare errori di confronto <<< if df.index.tz is not None: df.index = df.index.tz_localize(None) # pandas future-proof: usare "ME" (month-end) invece di "M" df_m = df.resample("ME").agg({"Close": "last"}) df_m["LogRet_M"] = np.log(df_m["Close"] / df_m["Close"].shift(1)) df_m = df_m.dropna() last_price = get_ticker_fast_price(ticker) currency = None try: info = getattr(ticker, "fast_info", None) or {} currency = info.get("currency") if isinstance(info, dict) else getattr(info, "currency", None) currency = currency or (info.get("Currency") if isinstance(info, dict) else getattr(info, "Currency", None)) except Exception: pass return df_m, last_price, currency def load_multiple_monthly(tickers: List[str], years: int = 10) -> Tuple[pd.DataFrame, pd.DataFrame, Dict[str, Optional[str]]]: """Carica più ticker in formato mensile. Ritorna: (Close_df, LogRet_df, currency_map) con indici timezone-naive.""" closes = {} rets = {} ccys = {} for t in tickers: try: df_m, _, ccy = load_yahoo_monthly(t, years) closes[t.upper()] = df_m["Close"] rets[t.upper()] = df_m["LogRet_M"] ccys[t.upper()] = ccy except Exception: continue if not closes: raise ValueError("Nessun dato scaricato.") close_df = pd.DataFrame(closes).dropna() ret_df = pd.DataFrame(rets).loc[close_df.index].dropna() return close_df, ret_df, ccys # ----------------------------- # Monte Carlo core # ----------------------------- def simulate_pac_montecarlo( months: int, n_sims: int, mu_log_annual: float, sigma_log_annual: float, initial: float, monthly: float, inflation_rate: float, tax_rate: float, seed: Optional[int] = None, contrib_at_end_of_month: bool = True, ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, float]: """ Simula l'evoluzione di un investimento con versamento iniziale + PAC mensile. Log-rendimenti ~ N(mu_m, sigma_m). Restituisce: - paths: matrice (n_sims, months+1) con i valori del portafoglio (lordi) nel tempo - final_net: vettore (n_sims,) valori finali netti (dopo imposta semplificata) - max_drawdowns: vettore (n_sims,) massimi drawdown realizzati (0..1) - invested_total: totale versato (nominale) """ if seed is not None: np.random.seed(seed) # Convertiamo annuale -> mensile mu_m = mu_log_annual / 12.0 sigma_m = sigma_log_annual / math.sqrt(12.0) # Generiamo log-rendimenti shocks = np.random.normal(loc=mu_m, scale=sigma_m, size=(n_sims, months)) factors = np.exp(shocks) # (1+r_m) # Sequenza dei contributi (aggiustati per inflazione una volta l'anno) contrib_schedule = np.zeros(months, dtype=float) for t in range(months): year_idx = (t // 12) contrib_schedule[t] = monthly * ((1.0 + inflation_rate) ** year_idx) invested_total = initial + float(contrib_schedule.sum()) # Evoluzione portafoglio paths = np.zeros((n_sims, months + 1), dtype=float) values = np.full(shape=(n_sims,), fill_value=initial, dtype=float) paths[:, 0] = values max_peaks = values.copy() max_dd = np.zeros(n_sims, dtype=float) for t in range(months): values *= factors[:, t] # rendimento mese if contrib_at_end_of_month and contrib_schedule[t] > 0.0: values += contrib_schedule[t] paths[:, t + 1] = values # drawdown update max_peaks = np.maximum(max_peaks, values) dd = 1.0 - (values / np.maximum(max_peaks, 1e-12)) max_dd = np.maximum(max_dd, dd) # Tassazione semplificata a fine periodo sui guadagni (se positivi) gains = values - invested_total tax = np.where(gains > 0, gains * tax_rate, 0.0) final_net = values - tax return paths, final_net, max_dd, invested_total def percentile_band(arr: np.ndarray, lower=5, upper=95) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: med = np.percentile(arr, 50, axis=0) lo = np.percentile(arr, lower, axis=0) hi = np.percentile(arr, upper, axis=0) return med, lo, hi # ----------------------------- # Backtest PAC storico (singolo ticker) # ----------------------------- def backtest_pac_storico( df_m: pd.DataFrame, start_date: Optional[pd.Timestamp], initial: float, monthly: float, inflation_rate: float, ) -> pd.DataFrame: """ Simula un PAC storico acquistando a fine mese al prezzo di chiusura mensile. Restituisce DataFrame con colonne: Close, Contrib, ContribCum, Shares, Value, Invested, PnL, Return. """ if df_m is None or df_m.empty: raise ValueError("Serie storica non disponibile.") # Assicuriamoci che l'indice sia timezone-naive if df_m.index.tz is not None: df_m = df_m.copy() df_m.index = df_m.index.tz_localize(None) # filtro finestra con start_date timezone-naive if start_date is not None: start_date = pd.Timestamp(start_date).tz_localize(None) df = df_m[df_m.index >= start_date].copy() else: df = df_m.copy() if df.empty: raise ValueError("Finestra selezionata vuota.") # inizializzazioni df["Contrib"] = 0.0 df["ContribCum"] = 0.0 df["Shares"] = 0.0 df["Value"] = 0.0 df["Invested"] = 0.0 df["PnL"] = 0.0 df["Return"] = 0.0 shares = 0.0 invested = 0.0 # versamento iniziale al primo mese first_price = float(df["Close"].iloc[0]) shares += initial / first_price if first_price > 0 else 0.0 invested += initial df.iloc[0, df.columns.get_loc("Contrib")] = initial for i, (date, row) in enumerate(df.iterrows()): price = float(row["Close"]) if i > 0: years_since_start = (date.year - df.index[0].year) adj_monthly = monthly * ((1.0 + inflation_rate) ** years_since_start) if price > 0 and adj_monthly > 0: add_sh = adj_monthly / price shares += add_sh invested += adj_monthly df.iloc[i, df.columns.get_loc("Contrib")] = adj_monthly value = shares * price df.iloc[i, df.columns.get_loc("Shares")] = shares df.iloc[i, df.columns.get_loc("Value")] = value df.iloc[i, df.columns.get_loc("Invested")] = invested df.iloc[i, df.columns.get_loc("ContribCum")] = df["Contrib"].iloc[: i + 1].sum() df.iloc[i, df.columns.get_loc("PnL")] = value - invested df.iloc[i, df.columns.get_loc("Return")] = (value / invested - 1.0) if invested > 0 else 0.0 return df # ----------------------------- # Backtest PAC storico (multi-titolo) # ----------------------------- def backtest_pac_portafoglio( close_df: pd.DataFrame, weights: Dict[str, float], start_date: Optional[pd.Timestamp], initial: float, monthly: float, inflation_rate: float, ) -> pd.DataFrame: """ Backtest di un PAC su più titoli con acquisti a fine mese, ripartizione dei versamenti secondo i pesi. Ritorna DataFrame con colonne: Value, Invested, PnL, Return e colonne Shares_ per titoli. """ if close_df is None or close_df.empty: raise ValueError("Serie storica non disponibile.") df = close_df.copy() if df.index.tz is not None: df.index = df.index.tz_localize(None) if start_date is not None: start_date = pd.Timestamp(start_date).tz_localize(None) df = df[df.index >= start_date] tickers = [t for t in weights.keys() if t in df.columns] if not tickers: raise ValueError("Nessun ticker dei pesi è presente nella serie caricata.") w = np.array([weights[t] for t in tickers], dtype=float) w = w / w.sum() # inizializza for t in tickers: df[f"Shares_{t}"] = 0.0 df["Invested"] = 0.0 df["Value"] = 0.0 df["PnL"] = 0.0 df["Return"] = 0.0 shares = {t: 0.0 for t in tickers} invested = 0.0 # versamento iniziale first_row = df.iloc[0] for j, t in enumerate(tickers): px = float(first_row[t]) alloc = initial * w[j] if px > 0: shares[t] += alloc / px invested += initial for t in tickers: df.iloc[0, df.columns.get_loc(f"Shares_{t}")] = shares[t] # loop mensile for i, (date, row) in enumerate(df.iterrows()): if i > 0: years_since_start = (date.year - df.index[0].year) adj_monthly = monthly * ((1.0 + inflation_rate) ** years_since_start) for j, t in enumerate(tickers): px = float(row[t]) alloc = adj_monthly * w[j] if px > 0 and alloc > 0: shares[t] += alloc / px invested += adj_monthly for t in tickers: df.iloc[i, df.columns.get_loc(f"Shares_{t}")] = shares[t] # valore portafoglio corrente value = 0.0 for t in tickers: value += shares[t] * float(row[t]) df.iloc[i, df.columns.get_loc("Value")] = value df.iloc[i, df.columns.get_loc("Invested")] = invested df.iloc[i, df.columns.get_loc("PnL")] = value - invested df.iloc[i, df.columns.get_loc("Return")] = (value / invested - 1.0) if invested > 0 else 0.0 return df # ----------------------------- # Charting helpers (matplotlib ONLY) # ----------------------------- def plot_paths_with_band(paths: np.ndarray, months: int, invested_total: float, sample_paths: int = 50): fig, ax = plt.subplots(figsize=(9, 4.8)) p50, p10, p90 = percentile_band(paths[:, 1:], 10, 90) x = np.arange(1, months + 1) ax.fill_between(x, p10, p90, alpha=0.25, label="Banda 10–90°p") ax.plot(x, p50, linewidth=2.2, label="Mediana") ns = min(sample_paths, paths.shape[0]) idx = np.random.choice(paths.shape[0], ns, replace=False) for i in idx: ax.plot(x, paths[i, 1:], linewidth=0.8, alpha=0.25) ax.axhline(invested_total, linestyle="--", linewidth=1.2, label="Capitale versato") ax.set_xlabel("Mesi") ax.set_ylabel("Valore portafoglio (€)") ax.set_title("Evoluzione simulata del capitale") ax.legend() fig.tight_layout() return fig def plot_hist_final(vals: np.ndarray, invested_total: float): fig, ax = plt.subplots(figsize=(9, 4.8)) ax.hist(vals, bins=60, alpha=0.85) med = np.median(vals) q_lo, q_hi = np.percentile(vals, [2.5, 97.5]) ax.axvline(med, linestyle="-", linewidth=2.0, label=f"Mediana: {med:,.0f}€") ax.axvline(invested_total, linestyle="--", linewidth=1.5, label="Capitale versato") ax.axvline(q_lo, linestyle=":", linewidth=1.2, label=f"2.5°p: {q_lo:,.0f}€") ax.axvline(q_hi, linestyle=":", linewidth=1.2, label=f"97.5°p: {q_hi:,.0f}€") ax.set_xlabel("Valore finale netto (€)") ax.set_ylabel("Frequenza") ax.set_title("Distribuzione del capitale finale (netto)") ax.legend() fig.tight_layout() return fig def plot_hist_drawdown(dd: np.ndarray): fig, ax = plt.subplots(figsize=(9, 4.8)) ax.hist(dd * 100.0, bins=50, alpha=0.85) med = np.median(dd) * 100.0 q_lo, q_hi = np.percentile(dd, [2.5, 97.5]) ax.axvline(med, linestyle="-", linewidth=2.0, label=f"Mediana: {med:.1f}%") ax.axvline(q_lo * 100.0, linestyle=":", linewidth=1.2, label=f"2.5°p: {q_lo*100:.1f}%") ax.axvline(q_hi * 100.0, linestyle=":", linewidth=1.2, label=f"97.5°p: {q_hi*100:.1f}%") ax.set_xlabel("Massimo Drawdown (%)") ax.set_ylabel("Frequenza") ax.set_title("Distribuzione dei massimi drawdown (lordi)") ax.legend() fig.tight_layout() return fig def plot_backtest(df_bt: pd.DataFrame, title: str): fig, ax = plt.subplots(figsize=(9, 4.6)) ax.plot(df_bt.index, df_bt["Value"], linewidth=2.0, label="Valore PAC") ax.plot(df_bt.index, df_bt["Invested"], linestyle="--", linewidth=1.2, label="Capitale versato") ax.set_title(title) ax.set_ylabel("Valore (€)") ax.legend() fig.tight_layout() return fig def plot_bar_last_month(chg_map: Dict[str, float]): tickers = list(chg_map.keys()) vals = [chg_map[t] for t in tickers] x = np.arange(len(tickers)) fig, ax = plt.subplots(figsize=(9, 4.6)) ax.bar(x, vals) ax.set_xticks(x) ax.set_xticklabels(tickers) ax.set_ylabel("Rendimento ultimo mese (%)") ax.set_title("Variazione % ultimo mese per ticker") fig.tight_layout() return fig # ----------------------------- # App # ----------------------------- def main(): st.set_page_config(page_title="Simulatore Monte Carlo PAC & Azioni/ETF/Cripto (mensile)", page_icon="📈", layout="wide") inject_css() st.title("📈 Simulatore Monte Carlo di un PAC e di Azioni/ETF/Cripto (mensile)") st.caption("Dati storici/near real-time da Yahoo Finance • I risultati sono a fini didattici.") # Tabs principali (aggiunta tab Cripto) tab_sim, tab_backtest, tab_multi, tab_pf, tab_crypto = st.tabs([ "🎲 Simulazione Monte Carlo", "⏱️ Backtest PAC Storico", "📊 Overview Multi-Ticker", "📦 Portafoglio PAC multi-titolo", "💱 Cripto" ]) # =============== TAB SIMULAZIONE =============== with tab_sim: left, right = st.columns([0.9, 1.1], gap="large") with left: st.subheader("🔧 Parametri di simulazione") with st.container(): data_mode = st.radio( "Fonte parametri di rendimento", options=["Manuale (log-rendimenti annualizzati)", "Storico Yahoo (stima automatica)"], index=1, help="Nel caso Manuale, inserisci media e deviazione standard dei LOG-rendimenti annuali.\n" "Nel caso Storico, stimiamo i parametri dai log-rendimenti mensili del ticker scelto." ) preset_selector("Ticker (Simulazione)", "sim_ticker") ticker_str = st.text_input( "Ticker Yahoo (es. AAPL, MSFT, ^GSPC, VUSA.L, BTC-USD)", value=st.session_state.get("sim_ticker", "AAPL"), key="sim_ticker" ) est_years = st.slider("Anni storici per la stima", min_value=3, max_value=20, value=10, key="sim_years") get_data = st.button("📥 Carica dati dal ticker", key="sim_getdata") if data_mode == "Manuale (log-rendimenti annualizzati)": mu_a = st.number_input("Media annua dei log-rendimenti (es. 0.07 = 7%)", value=0.07, step=0.005, format="%.4f") sigma_a = st.number_input("Deviazione standard annua dei log-rendimenti (es. 0.20 = 20%)", value=0.20, step=0.01, format="%.4f") else: mu_a = None sigma_a = None st.markdown("---") st.subheader("💶 Piano di versamenti") initial = st.number_input("Versamento iniziale (€)", value=1000.0, step=100.0, min_value=0.0, key="sim_initial") monthly = st.number_input("Versamento mensile (€)", value=300.0, step=50.0, min_value=0.0, key="sim_monthly") years = st.slider("Orizzonte d'investimento (anni)", min_value=1, max_value=40, value=15, key="sim_horizon") inflation = st.number_input("Adeguamento annuo % della quota mensile (inflazione attesa)", value=0.0, step=0.25, format="%.2f", key="sim_infl") inflation_rate = inflation / 100.0 st.markdown("---") st.subheader("⚙️ Opzioni Monte Carlo") n_sims = st.slider("Numero di simulazioni", min_value=500, max_value=20000, value=5000, step=500, key="sim_nsims") tax_choice = st.selectbox("Tassazione su plusvalenza a fine periodo", options=["0%", "12.5%", "26%"], index=2, key="sim_tax") tax_rate = {"0%": 0.0, "12.5%": 0.125, "26%": 0.26}[tax_choice] seed = st.number_input("Seed casuale (opzionale, per replicare i risultati)", value=0, step=1, key="sim_seed") seed = int(seed) if seed != 0 else None run = st.button("🚀 Esegui simulazione", use_container_width=True, key="sim_run") # Stato dati Yahoo df_m = None last_price = None currency = None mu_est_m, sigma_est_m = None, None if data_mode == "Storico Yahoo (stima automatica)": if get_data: with st.spinner("Scarico e preparo i dati..."): try: df_m, last_price, currency = load_yahoo_monthly(ticker_str, est_years) mu_est_m = float(df_m["LogRet_M"].mean()) sigma_est_m = float(df_m["LogRet_M"].std(ddof=1)) except Exception as e: st.error(f"Errore nel caricamento dati: {e}") elif YF_OK and ticker_str.strip(): try: df_m, last_price, currency = load_yahoo_monthly(ticker_str, est_years) mu_est_m = float(df_m["LogRet_M"].mean()) sigma_est_m = float(df_m["LogRet_M"].std(ddof=1)) except Exception: pass with right: st.subheader("📊 Dati del titolo (Yahoo) e parametri") with st.container(): if YF_OK and df_m is not None and not df_m.empty: col1, col2, col3, col4 = st.columns(4) last_close = float(df_m["Close"].iloc[-1]) prev_close = float(df_m["Close"].iloc[-2]) if len(df_m) > 1 else last_close m_ret = (last_close / prev_close - 1.0) * 100.0 with col1: st.markdown('
', unsafe_allow_html=True) st.metric("Ultimo close mensile", f"{last_close:,.2f} {currency or ''}", delta=f"{m_ret:+.2f}% vs mese prec.") st.markdown('
', unsafe_allow_html=True) with col2: if last_price is not None: delta_vs_last_close = (last_price / last_close - 1.0) * 100.0 st.markdown('
', unsafe_allow_html=True) st.metric("Prezzo attuale (near RT)", f"{last_price:,.2f} {currency or ''}", delta=f"{delta_vs_last_close:+.2f}% vs ultimo close") st.markdown('
', unsafe_allow_html=True) else: st.markdown('
', unsafe_allow_html=True) st.metric("Prezzo attuale", "n/d", delta="") st.markdown('
', unsafe_allow_html=True) with col3: if mu_est_m is not None and sigma_est_m is not None: st.markdown('
', unsafe_allow_html=True) st.write("Media log-ret. mensile stimata") st.write(f"**{mu_est_m*100:.2f}%**") st.write("Dev. std log-ret. mensile stimata") st.write(f"**{sigma_est_m*100:.2f}%**") st.markdown('
', unsafe_allow_html=True) with col4: st.markdown('
', unsafe_allow_html=True) st.write("Campione storico (mesi)") st.write(f"**{len(df_m)}**") st.write("Finestra (anni)") st.write(f"**{est_years}**") st.markdown('
', unsafe_allow_html=True) fig_price, axp = plt.subplots(figsize=(9, 3.2)) axp.plot(df_m.index, df_m["Close"], linewidth=2.0) axp.set_title(f"Andamento mensile di {ticker_str.upper()} (ultimi {est_years} anni)") axp.set_ylabel(f"Prezzo ({currency or ''})") axp.grid(alpha=0.2) st.pyplot(fig_price, use_container_width=True) elif not YF_OK: st.warning("Il pacchetto 'yfinance' non è installato. Esegui `pip install yfinance` per usare i dati Yahoo.") else: st.info("Inserisci un ticker valido e premi **Carica dati** per ottenere i parametri storici.") st.markdown("---") # Determinazione parametri finali per la simulazione months = years * 12 if data_mode == "Manuale (log-rendimenti annualizzati)": mu_log_annual = float(mu_a) sigma_log_annual = float(sigma_a) else: if mu_est_m is not None and sigma_est_m is not None: mu_log_annual = mu_est_m * 12.0 sigma_log_annual = sigma_est_m * math.sqrt(12.0) else: mu_log_annual = 0.07 sigma_log_annual = 0.20 st.subheader("🎲 Simulazione Monte Carlo") st.write(f"Parametri usati (annuali, log): **μ = {mu_log_annual:.4f}**, **σ = {sigma_log_annual:.4f}**") if run: with st.spinner("Eseguo la simulazione..."): paths, final_net, max_dd, invested_total = simulate_pac_montecarlo( months=months, n_sims=n_sims, mu_log_annual=mu_log_annual, sigma_log_annual=sigma_log_annual, initial=initial, monthly=monthly, inflation_rate=inflation_rate, tax_rate=tax_rate, seed=seed, contrib_at_end_of_month=True, ) # Risultati testuali med_final = float(np.median(final_net)) lo_final, hi_final = np.percentile(final_net, [2.5, 97.5]) med_dd = float(np.median(max_dd)) lo_dd, hi_dd = np.percentile(max_dd, [2.5, 97.5]) p_loss = float(np.mean(final_net < invested_total)) * 100.0 c1, c2, c3 = st.columns(3) with c1: st.markdown('
', unsafe_allow_html=True) st.write("Capitale versato (nominale)") st.write(f"**{invested_total:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c2: st.markdown('
', unsafe_allow_html=True) st.write("Valore finale netto (mediana; 95% CI)") st.write(f"**{med_final:,.0f} €**") st.write(f"_Intervallo 95%: {lo_final:,.0f} – {hi_final:,.0f} €_") st.markdown('
', unsafe_allow_html=True) with c3: st.markdown('
', unsafe_allow_html=True) st.write("Max drawdown lordo (mediana; 95% CI)") st.write(f"**{med_dd*100:.1f}%**") st.write(f"_Intervallo 95%: {lo_dd*100:.1f}% – {hi_dd*100:.1f}%_") st.markdown('
', unsafe_allow_html=True) st.markdown( f"**Probabilità di recuperare meno di quanto investito:** {p_loss:.1f}% " f"(criterio: _valore finale netto < capitale versato_)." ) # Grafici st.markdown("### 📈 Evoluzione del capitale") fig1 = plot_paths_with_band(paths, months, invested_total, sample_paths=50) st.pyplot(fig1, use_container_width=True) st.markdown("### 📦 Distribuzione del capitale finale (netto)") fig2 = plot_hist_final(final_net, invested_total) st.pyplot(fig2, use_container_width=True) st.markdown("### 📉 Distribuzione dei massimi drawdown (lordi)") fig3 = plot_hist_drawdown(max_dd) st.pyplot(fig3, use_container_width=True) st.markdown("---") st.subheader("ℹ️ Come viene effettuata la simulazione Monte Carlo") st.markdown( """
""", unsafe_allow_html=True ) # =============== TAB BACKTEST =============== with tab_backtest: st.subheader("⏱️ Backtest PAC Storico (ticker singolo)") preset_selector("Ticker (Backtest)", "bt_ticker") ticker_bt = st.text_input("Ticker Yahoo (es. AAPL, ^GSPC, VUSA.L, BTC-USD)", value=st.session_state.get("bt_ticker", "AAPL"), key="bt_ticker") years_bt = st.slider("Anni storici da scaricare", 3, 30, 15, key="bt_years") initial_bt = st.number_input("Versamento iniziale (€)", value=1000.0, step=100.0, min_value=0.0, key="bt_initial") monthly_bt = st.number_input("Versamento mensile (€)", value=300.0, step=50.0, min_value=0.0, key="bt_monthly") infl_bt = st.number_input("Adeguamento annuo % della quota mensile", value=0.0, step=0.25, format="%.2f", key="bt_infl") infl_rate_bt = infl_bt / 100.0 df_bt = None if YF_OK and ticker_bt.strip(): try: df_m_bt, last_price_bt, ccy_bt = load_yahoo_monthly(ticker_bt, years_bt) min_date = df_m_bt.index.min() max_date = df_m_bt.index.max() start_date = st.date_input("Data di inizio PAC", min_value=min_date.date(), max_value=max_date.date(), value=min_date.date(), key="bt_start") run_bt = st.button("▶️ Calcola backtest", key="bt_run") if run_bt: df_bt = backtest_pac_storico(df_m_bt, pd.to_datetime(start_date), initial_bt, monthly_bt, infl_rate_bt) except Exception as e: st.error(f"Errore nel caricamento dati: {e}") else: st.info("Inserisci un ticker valido.") if df_bt is not None: latest_row = df_bt.iloc[-1] invested = float(latest_row["Invested"]) value = float(latest_row["Value"]) pnl = float(latest_row["PnL"]) ret = float(latest_row["Return"]) * 100.0 c1, c2, c3, c4 = st.columns(4) with c1: st.markdown('
', unsafe_allow_html=True) st.write("Investito totale") st.write(f"**{invested:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c2: st.markdown('
', unsafe_allow_html=True) st.write("Valore PAC attuale") st.write(f"**{value:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c3: st.markdown('
', unsafe_allow_html=True) st.write("P&L assoluto") st.write(f"**{pnl:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c4: st.markdown('
', unsafe_allow_html=True) st.write("Rendimento su investito") st.write(f"**{ret:.2f}%**") st.markdown('
', unsafe_allow_html=True) st.markdown("### 📈 Andamento PAC vs Capitale versato") fig_bt = plot_backtest(df_bt, f"Backtest PAC storico su {ticker_bt.upper()}") st.pyplot(fig_bt, use_container_width=True) st.markdown("### 📋 Dettaglio mensile (ultimi 24 mesi)") st.dataframe(df_bt.tail(24)[["Close", "Contrib", "ContribCum", "Shares", "Value", "Invested", "PnL", "Return"]]) st.markdown( """ *Metodo:* acquisti al **close di fine mese**; la rata cresce ogni anno del tasso d'inflazione impostato. Non considera commissioni, dividendi e tasse. """ ) # =============== TAB MULTI-TICKER =============== with tab_multi: st.subheader("📊 Overview Multi-Ticker (up/down & performance)") tickers_raw = st.text_input("Lista ticker separati da virgola (es. AAPL, MSFT, NVDA, ^GSPC, VUSA.L, BTC-USD)", value="AAPL, MSFT, NVDA, ^GSPC, VUSA.L, BTC-USD", key="mt_tickers") years_mt = st.slider("Anni storici da scaricare", 1, 15, 5, key="mt_years") run_mt = st.button("📥 Carica e calcola overview", key="mt_run") if run_mt: tickers = [t.strip() for t in tickers_raw.split(",") if t.strip()] summary_rows = [] last_month_map = {} if not YF_OK: st.warning("Per la overview è necessario 'yfinance'. Esegui `pip install yfinance`.") else: for t in tickers: try: df_m_t, lp, ccy = load_yahoo_monthly(t, years_mt) if df_m_t is None or df_m_t.empty or len(df_m_t) < 2: continue # statistiche df_m_t["Chg%"] = df_m_t["Close"].pct_change() * 100.0 last_month = float(df_m_t["Chg%"].iloc[-1]) pos_12 = int((df_m_t["Chg%"].tail(12) > 0).sum()) neg_12 = int((df_m_t["Chg%"].tail(12) < 0).sum()) # YTD e 12M cur_year = df_m_t.index[-1].year ytd_mask = df_m_t.index.year == cur_year ytd = float((df_m_t["Close"].iloc[-1] / df_m_t.loc[ytd_mask, "Close"].iloc[0] - 1.0) * 100.0) if ytd_mask.any() else float("nan") ret_12m = float((df_m_t["Close"].iloc[-1] / df_m_t["Close"].iloc[-13] - 1.0) * 100.0) if len(df_m_t) > 13 else float("nan") summary_rows.append({ "Ticker": t.upper(), "Ultimo close": float(df_m_t["Close"].iloc[-1]), "Ultimo mese %": last_month, "YTD %": ytd, "12M %": ret_12m, "Mesi ↑ (12)": pos_12, "Mesi ↓ (12)": neg_12, }) last_month_map[t.upper()] = last_month except Exception: pass if len(summary_rows) == 0: st.error("Nessun dato disponibile per i ticker indicati.") else: df_sum = pd.DataFrame(summary_rows).set_index("Ticker") st.markdown("### 🧾 Riepilogo performance e up/down (ultimi 12 mesi)") st.dataframe(df_sum) st.markdown("### 📊 Variazione % ultimo mese per ticker") fig_bar = plot_bar_last_month(last_month_map) st.pyplot(fig_bar, use_container_width=True) # =============== TAB PORTAFOGLIO MULTI-TITOLO =============== with tab_pf: st.subheader("📦 PAC multi-titolo: backtest storico e Monte Carlo del portafoglio") st.markdown("Suggerimento: puoi combinare **indici** (es. ^GSPC), **ETF** (es. VUSA.L, IWDA.AS) e **cripto** (es. BTC-USD, ETH-USD) insieme alle azioni.") tickers_pf_raw = st.text_input("Ticker (es. AAPL, ^GSPC, VUSA.L, BTC-USD)", value="AAPL, ^GSPC, VUSA.L, BTC-USD", key="pf_tickers_str") weights_pf_raw = st.text_input("Pesi (%) corrispondenti (es. 40, 30, 20, 10)", value="40, 30, 20, 10", key="pf_weights_str") years_pf = st.slider("Anni storici da scaricare", 3, 20, 10, key="pf_years") initial_pf = st.number_input("Versamento iniziale (€)", value=2000.0, step=100.0, min_value=0.0, key="pf_initial") monthly_pf = st.number_input("Versamento mensile (€)", value=600.0, step=50.0, min_value=0.0, key="pf_monthly") infl_pf = st.number_input("Adeguamento annuo % della quota mensile", value=0.0, step=0.25, format="%.2f", key="pf_infl") infl_rate_pf = infl_pf / 100.0 tax_choice_pf = st.selectbox("Tassazione su plusvalenza a fine periodo", options=["0%", "12.5%", "26%"], index=2, key="pf_tax") tax_rate_pf = {"0%": 0.0, "12.5%": 0.125, "26%": 0.26}[tax_choice_pf] run_pf_load = st.button("📥 Carica prezzi e prepara portafoglio", key="pf_load") # Session state per mantenere dati fra i rerun st.session_state.setdefault("pf_close_df", None) st.session_state.setdefault("pf_ret_df", None) st.session_state.setdefault("pf_ccys", None) st.session_state.setdefault("pf_weights_map", None) st.session_state.setdefault("pf_tickers_list", None) tickers_pf = [t.strip().upper() for t in tickers_pf_raw.split(",") if t.strip()] try: weights_list = [float(x.strip().replace(",", ".")) for x in weights_pf_raw.split(",")] except Exception: weights_list = [] if not YF_OK: st.error("Per il portafoglio è necessario 'yfinance' (pip install yfinance).") elif run_pf_load: if len(tickers_pf) == 0 or len(weights_list) != len(tickers_pf): st.error("Numero di pesi non coerente con il numero di ticker.") else: weights_arr = np.array(weights_list, dtype=float) if weights_arr.sum() <= 0: st.error("La somma dei pesi deve essere positiva.") else: weights_arr = weights_arr / weights_arr.sum() weights_pf_map = {t: w for t, w in zip(tickers_pf, weights_arr)} try: close_df, ret_df, ccys = load_multiple_monthly(tickers_pf, years_pf) st.session_state["pf_close_df"] = close_df st.session_state["pf_ret_df"] = ret_df st.session_state["pf_ccys"] = ccys st.session_state["pf_weights_map"] = weights_pf_map st.session_state["pf_tickers_list"] = tickers_pf except Exception as e: st.error(f"Errore durante la preparazione del portafoglio: {e}") # --- Usa sempre i dati dalla sessione --- close_df = st.session_state.get("pf_close_df", None) ret_df = st.session_state.get("pf_ret_df", None) weights_pf = st.session_state.get("pf_weights_map", None) tickers_pf = st.session_state.get("pf_tickers_list", tickers_pf) if close_df is None or weights_pf is None: st.info("Imposta i ticker/pesi e premi **Carica prezzi e prepara portafoglio**.") else: min_date = close_df.index.min() max_date = close_df.index.max() start_date_pf = st.date_input( "Data di inizio PAC portafoglio", min_value=min_date.date(), max_value=max_date.date(), value=min_date.date(), key="pf_start" ) # Backtest if st.button("▶️ Backtest portafoglio", key="pf_run_bt"): df_pf_bt = backtest_pac_portafoglio(close_df, weights_pf, pd.to_datetime(start_date_pf), initial_pf, monthly_pf, infl_rate_pf) last = df_pf_bt.iloc[-1] invested = float(last["Invested"]); value = float(last["Value"]) pnl = float(last["PnL"]); ret = float(last["Return"]) * 100.0 c1, c2, c3, c4 = st.columns(4) with c1: st.markdown('
', unsafe_allow_html=True) st.write("Investito totale"); st.write(f"**{invested:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c2: st.markdown('
', unsafe_allow_html=True) st.write("Valore portafoglio"); st.write(f"**{value:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c3: st.markdown('
', unsafe_allow_html=True) st.write("P&L assoluto"); st.write(f"**{pnl:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c4: st.markdown('
', unsafe_allow_html=True) st.write("Rendimento su investito"); st.write(f"**{ret:.2f}%**") st.markdown('
', unsafe_allow_html=True) st.markdown("### 📈 Andamento PAC portafoglio vs Capitale versato") fig_pfb = plot_backtest(df_pf_bt, "Backtest PAC storico del portafoglio") st.pyplot(fig_pfb, use_container_width=True) st.markdown("### 📋 Dettaglio mensile (ultimi 24 mesi)") show_cols = [c for c in df_pf_bt.columns if c.startswith("Shares_")] + ["Value", "Invested", "PnL", "Return"] st.dataframe(df_pf_bt.tail(24)[show_cols]) # Monte Carlo (parametri stimati dal portafoglio) if ret_df is not None and not ret_df.empty: ret_df2 = ret_df[[t for t in tickers_pf if t in ret_df.columns]].dropna() w_vec = np.array([weights_pf[t] for t in ret_df2.columns], dtype=float) w_vec = w_vec / w_vec.sum() mu_m_vec = ret_df2.mean().values cov_m = ret_df2.cov().values mu_p_m = float(np.dot(w_vec, mu_m_vec)) sigma_p_m = float(np.sqrt(np.dot(w_vec, np.dot(cov_m, w_vec)))) mu_p_a = mu_p_m * 12.0 sigma_p_a = sigma_p_m * math.sqrt(12.0) st.markdown("### 🎲 Monte Carlo sul portafoglio (parametri stimati)") st.write(f"Parametri portafoglio (annuali, log): **μ = {mu_p_a:.4f}**, **σ = {sigma_p_a:.4f}**") years_pf_h = st.slider("Orizzonte d'investimento (anni) per la simulazione", 1, 40, 15, key="pf_horizon") n_sims_pf = st.slider("Numero di simulazioni", 500, 20000, 5000, 500, key="pf_nsims") seed_pf = st.number_input("Seed casuale (opzionale)", value=0, step=1, key="pf_seed") seed_pf = int(seed_pf) if seed_pf != 0 else None if st.button("🚀 Esegui simulazione portafoglio", key="pf_run_mc"): months_pf = years_pf_h * 12 paths_pf, final_pf, dd_pf, invested_pf = simulate_pac_montecarlo( months=months_pf, n_sims=n_sims_pf, mu_log_annual=mu_p_a, sigma_log_annual=sigma_p_a, initial=initial_pf, monthly=monthly_pf, inflation_rate=infl_rate_pf, tax_rate=tax_rate_pf, seed=seed_pf, ) med_final = float(np.median(final_pf)) lo_final, hi_final = np.percentile(final_pf, [2.5, 97.5]) med_dd = float(np.median(dd_pf)) lo_dd, hi_dd = np.percentile(dd_pf, [2.5, 97.5]) p_loss = float(np.mean(final_pf < invested_pf)) * 100.0 c1, c2, c3 = st.columns(3) with c1: st.markdown('
', unsafe_allow_html=True) st.write("Capitale versato (nominale)"); st.write(f"**{invested_pf:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c2: st.markdown('
', unsafe_allow_html=True) st.write("Valore finale netto (mediana; 95% CI)") st.write(f"**{med_final:,.0f} €**") st.write(f"_Intervallo 95%: {lo_final:,.0f} – {hi_final:,.0f} €_") st.markdown('
', unsafe_allow_html=True) with c3: st.markdown('
', unsafe_allow_html=True) st.write("Max drawdown lordo (mediana; 95% CI)") st.write(f"**{med_dd*100:.1f}%**") st.write(f"_Intervallo 95%: {lo_dd*100:.1f}% – {hi_dd*100:.1f}%_") st.markdown('
', unsafe_allow_html=True) st.markdown( f"**Probabilità di recuperare meno di quanto investito:** {p_loss:.1f}% " f"(criterio: _valore finale netto < capitale versato_)." ) st.markdown("### 📈 Evoluzione del capitale (portafoglio)") st.pyplot(plot_paths_with_band(paths_pf, months_pf, invested_pf, sample_paths=50), use_container_width=True) st.markdown("### 📦 Distribuzione del capitale finale (netto)") st.pyplot(plot_hist_final(final_pf, invested_pf), use_container_width=True) st.markdown("### 📉 Distribuzione dei massimi drawdown (lordi)") st.pyplot(plot_hist_drawdown(dd_pf), use_container_width=True) # =============== TAB CRIPTO =============== with tab_crypto: st.subheader("💱 Cripto: Backtest PAC e Simulazione") preset_selector("Ticker (Cripto)", "crypto_ticker") ticker_c = st.text_input("Ticker cripto (es. BTC-USD, ETH-USD, SOL-USD)", value=st.session_state.get("crypto_ticker", "BTC-USD"), key="crypto_ticker") years_c = st.slider("Anni storici da scaricare", 1, 15, 7, key="c_years") initial_c = st.number_input("Versamento iniziale (€)", value=500.0, step=100.0, min_value=0.0, key="c_initial") monthly_c = st.number_input("Versamento mensile (€)", value=200.0, step=50.0, min_value=0.0, key="c_monthly") infl_c = st.number_input("Adeguamento annuo % della quota mensile", value=0.0, step=0.25, format="%.2f", key="c_infl") infl_rate_c = infl_c / 100.0 tax_choice_c = st.selectbox("Tassazione su plusvalenza a fine periodo", options=["0%", "12.5%", "26%"], index=2, key="c_tax") tax_rate_c = {"0%": 0.0, "12.5%": 0.125, "26%": 0.26}[tax_choice_c] run_c_load = st.button("📥 Carica dati cripto", key="c_load") # Inizializza contenitori in sessione st.session_state.setdefault("crypto_df", None) st.session_state.setdefault("crypto_meta", {}) if not YF_OK: st.warning("Per i dati cripto è necessario 'yfinance'. Esegui `pip install yfinance`.") elif run_c_load and ticker_c.strip(): try: df_m_c, last_price_c, ccy_c = load_yahoo_monthly(ticker_c, years_c) st.session_state["crypto_df"] = df_m_c st.session_state["crypto_meta"] = {"last_price": last_price_c, "ccy": ccy_c, "ticker": ticker_c, "years": years_c} except Exception as e: st.error(f"Errore nel caricamento dati cripto: {e}") # --- Da qui in poi usiamo SEMPRE la session_state --- df_m_c = st.session_state.get("crypto_df", None) meta_c = st.session_state.get("crypto_meta", {}) if df_m_c is None: st.info("Scegli un ticker e premi **Carica dati cripto**.") else: ccy_c = meta_c.get("ccy") years_c_eff = meta_c.get("years", years_c) ticker_c_eff = meta_c.get("ticker", ticker_c) # metriche veloci last_close_c = float(df_m_c["Close"].iloc[-1]) prev_close_c = float(df_m_c["Close"].iloc[-2]) if len(df_m_c) > 1 else last_close_c m_ret_c = (last_close_c / prev_close_c - 1.0) * 100.0 mu_c_m = float(df_m_c["LogRet_M"].mean()) sigma_c_m = float(df_m_c["LogRet_M"].std(ddof=1)) col1, col2, col3 = st.columns(3) with col1: st.markdown('
', unsafe_allow_html=True) st.metric("Ultimo close mensile", f"{last_close_c:,.2f} {ccy_c or ''}", delta=f"{m_ret_c:+.2f}% vs mese prec.") st.markdown('
', unsafe_allow_html=True) with col2: st.markdown('
', unsafe_allow_html=True) st.write("Media log-ret. mensile (stima)") st.write(f"**{mu_c_m*100:.2f}%**") st.write("Dev. std log-ret. mensile (stima)") st.write(f"**{sigma_c_m*100:.2f}%**") st.markdown('
', unsafe_allow_html=True) with col3: st.markdown('
', unsafe_allow_html=True) st.write("Campione storico (mesi)") st.write(f"**{len(df_m_c)}**") st.write("Finestra (anni)") st.write(f"**{years_c_eff}**") st.markdown('
', unsafe_allow_html=True) # grafico prezzo fig_price_c, axpc = plt.subplots(figsize=(9, 3.2)) axpc.plot(df_m_c.index, df_m_c["Close"], linewidth=2.0) axpc.set_title(f"Andamento mensile di {ticker_c_eff.upper()} (ultimi {years_c_eff} anni)") axpc.set_ylabel(f"Prezzo ({ccy_c or ''})") axpc.grid(alpha=0.2) st.pyplot(fig_price_c, use_container_width=True) st.markdown("---") # Backtest PAC storico cripto min_date_c = df_m_c.index.min(); max_date_c = df_m_c.index.max() start_date_c = st.date_input("Data di inizio PAC (cripto)", min_value=min_date_c.date(), max_value=max_date_c.date(), value=min_date_c.date(), key="c_start") if st.button("▶️ Backtest PAC cripto", key="c_run_bt"): df_bt_c = backtest_pac_storico(df_m_c, pd.to_datetime(start_date_c), initial_c, monthly_c, infl_rate_c) last_c = df_bt_c.iloc[-1] invested_c = float(last_c["Invested"]); value_c = float(last_c["Value"]) pnl_c = float(last_c["PnL"]); ret_c = float(last_c["Return"]) * 100.0 c1, c2, c3, c4 = st.columns(4) for lbl, val in [("Investito totale", f"{invested_c:,.0f} €"), ("Valore PAC attuale", f"{value_c:,.0f} €"), ("P&L assoluto", f"{pnl_c:,.0f} €"), ("Rendimento su investito", f"{ret_c:.2f}%")]: col = c1 if lbl=="Investito totale" else c2 if lbl=="Valore PAC attuale" else c3 if lbl=="P&L assoluto" else c4 with col: st.markdown('
', unsafe_allow_html=True) st.write(lbl); st.write(f"**{val}**") st.markdown('
', unsafe_allow_html=True) st.markdown("### 📈 Andamento PAC cripto vs Capitale versato") fig_bt_c = plot_backtest(df_bt_c, f"Backtest PAC storico su {ticker_c_eff.upper()}") st.pyplot(fig_bt_c, use_container_width=True) st.markdown("---") # Monte Carlo su cripto (parametri stimati) mu_c_a = mu_c_m * 12.0 sigma_c_a = sigma_c_m * math.sqrt(12.0) st.write(f"Parametri (annuali, log) stimati: **μ = {mu_c_a:.4f}**, **σ = {sigma_c_a:.4f}**") years_c_h = st.slider("Orizzonte d'investimento (anni) – Simulazione cripto", 1, 20, 10, key="c_horizon") n_sims_c = st.slider("Numero di simulazioni (cripto)", 500, 20000, 5000, 500, key="c_nsims") seed_c = st.number_input("Seed casuale (opzionale)", value=0, step=1, key="c_seed") seed_c = int(seed_c) if seed_c != 0 else None if st.button("🚀 Esegui simulazione cripto", key="c_run_mc"): months_c = years_c_h * 12 paths_c, final_c, dd_c, invested_c2 = simulate_pac_montecarlo( months=months_c, n_sims=n_sims_c, mu_log_annual=mu_c_a, sigma_log_annual=sigma_c_a, initial=initial_c, monthly=monthly_c, inflation_rate=infl_rate_c, tax_rate=tax_rate_c, seed=seed_c, ) med_final_c = float(np.median(final_c)) lo_final_c, hi_final_c = np.percentile(final_c, [2.5, 97.5]) med_dd_c = float(np.median(dd_c)) lo_dd_c, hi_dd_c = np.percentile(dd_c, [2.5, 97.5]) p_loss_c = float(np.mean(final_c < invested_c2)) * 100.0 c1, c2, c3 = st.columns(3) with c1: st.markdown('
', unsafe_allow_html=True) st.write("Capitale versato (nominale)") st.write(f"**{invested_c2:,.0f} €**") st.markdown('
', unsafe_allow_html=True) with c2: st.markdown('
', unsafe_allow_html=True) st.write("Valore finale netto (mediana; 95% CI)") st.write(f"**{med_final_c:,.0f} €**") st.write(f"_Intervallo 95%: {lo_final_c:,.0f} – {hi_final_c:,.0f} €_") st.markdown('
', unsafe_allow_html=True) with c3: st.markdown('
', unsafe_allow_html=True) st.write("Max drawdown lordo (mediana; 95% CI)") st.write(f"**{med_dd_c*100:.1f}%**") st.write(f"_Intervallo 95%: {lo_dd_c*100:.1f}% – {hi_dd_c*100:.1f}%_") st.markdown('
', unsafe_allow_html=True) st.markdown(f"**Probabilità di recuperare meno di quanto investito:** {p_loss_c:.1f}%.") st.markdown("### 📈 Evoluzione del capitale (cripto)") st.pyplot(plot_paths_with_band(paths_c, months_c, invested_c2, sample_paths=50), use_container_width=True) st.markdown("### 📦 Distribuzione del capitale finale (netto)") st.pyplot(plot_hist_final(final_c, invested_c2), use_container_width=True) st.markdown("### 📉 Distribuzione dei massimi drawdown (lordi)") st.pyplot(plot_hist_drawdown(dd_c), use_container_width=True) # Footer st.markdown("
", unsafe_allow_html=True) st.markdown('
Credit: Giovanni Vignola e GPT-5 Plus
', unsafe_allow_html=True) # if __name__ == "__main__": main()