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(
"""
- Modelliamo i log-rendimenti mensili come variabili Normali indipendenti e identicamente distribuite,
con media e deviazione standard impostate (o stimate dai dati Yahoo). Il prezzo/progresso del portafoglio
evolve moltiplicando per
exp(r_t), dove r_t è il log-rendimento del mese t.
- I versamenti consistono in un importo iniziale e in una quota mensile, che può essere adeguata annualmente
all'inflazione attesa. La quota mensile viene aggiunta a fine mese dopo l'applicazione del rendimento.
- Il massimo drawdown è calcolato come il massimo calo percentuale dal picco al valore successivo
all'interno di ciascun percorso simulato.
- La tassazione è applicata in modo semplificato a fine periodo, come percentuale fissa (0%, 12.5% o 26%)
sulle sole plusvalenze (se presenti). Non consideriamo commissioni, costi, dividendi o altre imposte.
- Gli esiti sono distribuzioni: riportiamo mediana, intervallo al 95% e la probabilità di chiudere in perdita.
""",
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()