research(report): resoconto PROTETTO (soft-guard DD -4%) anno-per-anno -> combo + TP01 + GTAA singoli

Report che mette combo (TP01+GTAA 50/50), TP01 e GTAA sulla stessa griglia giorni-di-borsa
(esposizione 1x, come dentro al combo), applica la guardia-DD -4% a ciascuna serie e tira fuori
per anno: NL (net liquidation da $2000), DD intra-anno, rendimento, Sharpe + riga TOT con CAGR.
Combo protetto: CAGR +9.1% / DD 5.8% / Sh 1.38 (2022 -1.8%); baseline +11.3% / 8.4% / 1.48.
Aggiunto data/paper_combo/ al .gitignore (stato paper runtime, come gli altri paper dir).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-23 14:16:59 +00:00
parent 856a02fcc5
commit 237ca8da13
2 changed files with 91 additions and 0 deletions
@@ -0,0 +1,90 @@
"""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()