diff --git a/scripts/analysis/projection_3y.py b/scripts/analysis/projection_3y.py new file mode 100644 index 0000000..c1375e3 --- /dev/null +++ b/scripts/analysis/projection_3y.py @@ -0,0 +1,148 @@ +"""Proiezione a 3 anni del portafoglio live (PORT06) ESCLUDENDO il 2024. + +Risponde a: "partendo da 1000 EUR, con capitale che compone e puntata che cresce +di conseguenza, quale guadagno giornaliero atteso e quale traiettoria a 3 anni?" + +Punti chiave (perche' i numeri differiscono da report_families): + - report_families/Portfolio.backtest usano le curve sleeve NATIVE a leva 3x. + - Il container LIVE (src.portfolio.runner via portfolios.yml) gira a pos 0.15 x 2x. + - Il PnL giornaliero scala ESATTAMENTE con la leva: pnl = cap * pos * lev * (ret - fee), + stesso segnale -> ratio 2/3 per gli sleeve leverati. + - ROT02 e TSM01 NON si riscalano: usano `gross` (0.45 / 0.30), indipendente dalla leva + e identico fra backtest e worker live. + - Il 2024 e' escluso perche' anno eccezionale (crypto +; gonfia ogni stima). + +CAVEAT (le proiezioni sono OTTIMISTICHE): + - 2021-2025 e' quasi tutto bull/recovery; poco orso/flat prolungato nel campione. + - Dati alt = testnet (volume sottile, fill/slippage NON modellati). + - OOS singolo (2024-25) = regime calmo -> ~50% ottimistico (vedi report_families (D)). + Lo scenario SOBRIO (haircut ~50%) e' il numero prudente su cui pianificare. + +Run: uv run python scripts/analysis/projection_3y.py +""" +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import pandas as pd + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(PROJECT_ROOT)) + +from scripts.portfolios._defs import PORTFOLIOS +from src.portfolio.sleeves import all_sleeve_equities, sleeve_returns_df +from scripts.analysis.combine_portfolio import port_returns + +# sleeve la cui esposizione NON dipende dalla leva (gross-based) -> non si riscalano +NOSCALE = {"ROT02_rot", "TSM01"} +NATIVE_LEV = 3.0 # leva delle curve sleeve in combine_portfolio +EXCLUDE_YEAR = 2024 +START_CAPITAL = 1000.0 + + +def live_portfolio_returns(live_leverage: float = 2.0) -> pd.Series: + """Rendimenti giornalieri di PORT06 al sizing LIVE (pos 0.15 x `live_leverage`). + Riscala gli sleeve leverati di live_leverage/NATIVE_LEV; ROT/TSM invariati.""" + p = PORTFOLIOS["PORT06"] + p.leverage = live_leverage + p.weighting = "cap" + p.caps = {"PAIRS": 0.33} + + eq = all_sleeve_equities() + dr = sleeve_returns_df(p.sleeve_ids) + w = p.weight_vector(dr) + + scale = live_leverage / NATIVE_LEV + eq_live = {} + for sid in p.sleeve_ids: + r = eq[sid].pct_change().fillna(0.0) + r = r if sid in NOSCALE else r * scale + eq_live[sid] = (1 + r).cumprod() + pr = port_returns(eq_live, w) + pr.index = pd.to_datetime(pr.index) + return pr + + +def stats_ex_year(pr: pd.Series, exclude: int = EXCLUDE_YEAR) -> dict: + ex = pr[pr.index.year != exclude] + n = len(ex) + years = n / 365.0 + tot = (1 + ex).prod() - 1 + cagr = (1 + tot) ** (1 / years) - 1 + yvals = [(1 + g).prod() - 1 for _, g in ex.groupby(ex.index.year)] + return { + "days": n, "years": years, "total": tot, "cagr": cagr, + "year_median": float(np.median(yvals)), "year_mean": float(np.mean(yvals)), + "daily_mean_eur": float(ex.mean() * START_CAPITAL), + "daily_median_eur": float(ex.median() * START_CAPITAL), + "daily_std_eur": float(ex.std() * START_CAPITAL), + "pos_days": float((ex > 0).mean()), + "per_year": {int(y): float((1 + g).prod() - 1) for y, g in pr.groupby(pr.index.year)}, + } + + +def project(annual: float, years: int = 3, start: float = START_CAPITAL) -> list[dict]: + """Capitale che compone; la puntata cresce col capitale -> EUR/giorno cresce.""" + rows, cap = [], start + for yr in range(1, years + 1): + s = cap + cap = cap * (1 + annual) + rows.append({"year": yr, "start": s, "end": cap, + "gain": cap - s, "eur_per_day": (cap - s) / 365.0}) + return rows + + +def years_to_target(daily_target: float, annual: float, start: float = START_CAPITAL) -> float: + """Anni per raggiungere un certo EUR/giorno componendo (capitale = target*365/cagr).""" + cap_needed = daily_target * 365.0 / annual + if cap_needed <= start: + return 0.0 + return float(np.log(cap_needed / start) / np.log(1 + annual)) + + +def main(): + pr = live_portfolio_returns(live_leverage=2.0) + s = stats_ex_year(pr) + + print("=" * 72) + print(" PORT06 LIVE (pos 0.15 x 2x) — proiezione 3 anni, ESCLUSO 2024") + print("=" * 72) + print(" Rendimento live per anno:") + for y, v in s["per_year"].items(): + flag = " <-- ESCLUSO" if y == EXCLUDE_YEAR else "" + print(f" {y}: {v * 100:+6.1f}%{flag}") + print() + print(f" CAGR (escl 2024): {s['cagr'] * 100:5.1f}% " + f"[{s['years']:.2f} anni di dati]") + print(f" anno mediano: {s['year_median'] * 100:5.1f}%") + print(f" anno medio: {s['year_mean'] * 100:5.1f}%") + print(f" EUR/giorno su 1000: media {s['daily_mean_eur']:.2f} | " + f"mediana {s['daily_median_eur']:.2f} | std {s['daily_std_eur']:.2f}") + print(f" giorni positivi: {s['pos_days'] * 100:.1f}%") + print() + + scenarios = [ + ("CAGR backtest escl-2024", s["cagr"]), + ("anno mediano", s["year_median"]), + ("SOBRIO (haircut ~50%)", s["cagr"] * 0.5), + ] + for name, g in scenarios: + print(f" -- 3 anni @ {g * 100:.0f}%/anno ({name}) --") + for r in project(g): + print(f" anno {r['year']}: {r['start']:7.0f} -> {r['end']:7.0f} EUR " + f"(+{r['gain']:5.0f}, ~{r['eur_per_day']:4.2f} EUR/g medi)") + print() + + print(" -- Target 50 EUR/giorno (reality check) --") + for name, g in scenarios[:1] + scenarios[2:]: + cap_needed = 50.0 * 365.0 / g + t = years_to_target(50.0, g) + print(f" @ {g * 100:.0f}%/anno: servono ~{cap_needed:,.0f} EUR schierati " + f"-> da 1000 EUR, ~{t:.0f} anni componendo") + print(" => il collo di bottiglia e' il CAPITALE iniziale, non la strategia.") + + +if __name__ == "__main__": + main()