"""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()