diff --git a/.gitignore b/.gitignore index 61156b6..261e124 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ scripts/research/blind/leaderboard.json # forward-monitor runtime state (regenerable, forward-only) data/paper_prevday/ +data/paper_combo/ diff --git a/scripts/research/combo_protected_yearly.py b/scripts/research/combo_protected_yearly.py new file mode 100644 index 0000000..5ee4e5c --- /dev/null +++ b/scripts/research/combo_protected_yearly.py @@ -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()