"""Resoconto anno-per-anno CON PROTEZIONE (soft-guard DD -4%) — combo e singoli. Mette combo (TP01+GTAA 50/50), TP01 e GTAA sulla STESSA griglia giorni-di-borsa (come dentro al combo), applica la guardia-DD -4% a ciascuna serie (de-risk 1.0->0.4 a -4% dal picco, ri-rischia a -1.6%), e per ogni anno riporta: NL (net liquidation da $2000), DD intra-anno, rendimento (=CAGR 1y), Sharpe. Riga TOT con CAGR e Sharpe complessivi. """ import sys from pathlib import Path import numpy as np, pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from src.portfolio.sleeves import _tp01_returns from src.portfolio.gtaa import gtaa_returns INITIAL = 2000.0 ANN = np.sqrt(252.0) DD_TRIG = 0.04 def dd_guard(r, dd_trigger=DD_TRIG): """De-risk: esposizione 1.0->0.4 se DD da picco > dd_trigger; ri-rischia a dd_trigger*0.4.""" r = r.values; n = len(r); eq = np.cumprod(1 + r); pk = np.maximum.accumulate(eq) expo = np.ones(n); on = True for i in range(1, n): ddi = (pk[i - 1] - eq[i - 1]) / pk[i - 1] if ddi > dd_trigger: on = False if ddi < dd_trigger * 0.4: on = True expo[i] = 1.0 if on else 0.4 return pd.Series(expo * r, index=_idx) # set below def legs_on_grid(wc=0.5): """TP01(crypto, compoundato sul grid) e GTAA(equity) sulla stessa griglia giorni-di-borsa.""" tp = _tp01_returns() if tp.index.tz is None: tp.index = tp.index.tz_localize("UTC") eq = gtaa_returns().dropna() grid = eq.index[eq.index >= tp.index[0]] cum = (1 + tp).cumprod() tpg = cum.reindex(cum.index.union(grid)).ffill().reindex(grid).pct_change() J = pd.concat({"c": tpg, "e": eq.reindex(grid)}, axis=1).dropna() combo = wc * J["c"] + (1 - wc) * J["e"] return combo, J["c"], J["e"] def sh(r): r = r.dropna().values; return float(np.mean(r) / np.std(r) * ANN) if len(r) > 5 and np.std(r) > 0 else 0.0 def maxdd(curve): pk = np.maximum.accumulate(curve); return float(np.max((pk - curve) / pk)) if len(curve) else 0.0 def yearly(ret, label): ret = ret.dropna().sort_index() print(f"\n ===== {label} (guardia-DD -4%) =====") print(f" {'anno':6}{'NL inizio':>11}{'NL fine':>11}{'rend%':>9}{'DD%':>8}{'Sharpe':>9}") eq = INITIAL for y in sorted(set(ret.index.year)): r = ret[ret.index.year == y] if len(r) < 5: continue eq0 = eq curve = eq0 * np.cumprod(1 + r.values) eq = float(curve[-1]) print(f" {y:<6}{eq0:>11,.0f}{eq:>11,.0f}{(eq/eq0-1)*100:>+8.1f}%{maxdd(curve)*100:>7.1f}%{sh(r):>9.2f}") yrs = (ret.index[-1] - ret.index[0]).days / 365.25 cagr = (eq / INITIAL) ** (1 / yrs) - 1 if yrs > 0 else 0 full_curve = INITIAL * np.cumprod(1 + ret.values) print(f" {'TOT':<6}{INITIAL:>11,.0f}{eq:>11,.0f}{(eq/INITIAL-1)*100:>+8.1f}%{maxdd(full_curve)*100:>7.1f}%{sh(ret):>9.2f}" f" | CAGR {cagr*100:+.1f}% ({yrs:.1f}y)") def main(): global _idx print("=" * 78) print(" RESOCONTO PROTETTO (soft-guard DD -4%) — da $2.000, anno per anno") print(" Tutte e tre sulla griglia giorni-di-borsa del combo (dal 2019), esposizione 1x.") print("=" * 78) combo, tp, g = legs_on_grid() for ret, lbl in [(combo, "COMBO TP01+GTAA 50/50"), (tp, "solo TP01 (crypto)"), (g, "solo GTAA (equity)")]: _idx = ret.index yearly(dd_guard(ret), lbl) # confronto NON protetto (baseline) in coda, una riga TOT per riferimento print("\n --- riferimento NON protetto (baseline, TOT) ---") for ret, lbl in [(combo, "COMBO"), (tp, "TP01"), (g, "GTAA")]: yrs = (ret.index[-1] - ret.index[0]).days / 365.25 eqf = INITIAL * np.prod(1 + ret.values) print(f" {lbl:6} CAGR {((eqf/INITIAL)**(1/yrs)-1)*100:>+5.1f}% DD {maxdd(INITIAL*np.cumprod(1+ret.values))*100:>4.1f}% Sharpe {sh(ret):.2f}") if __name__ == "__main__": main()