"""PAPER COMBO — forward-only del combo cross-venue TP01 (Deribit) + GTAA (IB), NUDO vs PROTETTO. Le due gambe eseguibili a basso capitale (XS01/VRP01 STAT-MODE esclusi), scorrelate (corr ~0.21) -> blend Sharpe ~1.5, DD dimezzato. Traccia FORWARD-ONLY DUE versioni in parallelo: * NUDO = blend 50/50 TP01+GTAA * PROTETTO = stesso blend + GUARDIA-DRAWDOWN -4% (de-risk a 0.4x quando il DD da picco supera -4%, ri-rischia a -1.6%). Backtest: MaxDD 8.4%->5.8%, 2022 -4.4%->-1.8%, Sharpe 1.48->1.38 (diario 2026-06-23-tail-hedge-lab). Le opzioni NON aiutano il grind del 2022 -> escluse. Crypto compoundato sul grid giorni-di-borsa. NESSUNA esecuzione reale. Mostra posizioni azionabili. uv run python scripts/live/paper_combo.py [--status|--reset] """ from __future__ import annotations import sys, json from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np, pandas as pd from src.portfolio.sleeves import _tp01_returns, _tp01_positions from src.portfolio.gtaa import gtaa_returns, gtaa_weights STATE_DIR = PROJECT_ROOT / "data" / "paper_combo" STATE = STATE_DIR / "state.json" EQ = STATE_DIR / "equity.csv" INITIAL = 2000.0 W_CRYPTO = 0.5 DD_TRIGGER = 0.04 # guardia-drawdown della versione PROTETTA def combo_daily(wc: float = W_CRYPTO) -> pd.Series: 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.0 + 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() return (wc * J["c"] + (1 - wc) * J["e"]).dropna() def apply_dd_guard(r: pd.Series, trigger: float = DD_TRIGGER) -> pd.Series: """De-risk a 0.4x quando il DD da picco > trigger; ri-rischia a 1.0x quando < 0.4*trigger.""" rv = r.values; n = len(rv); eq = np.cumprod(1 + rv); 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 pk[i - 1] > 0 else 0.0 if ddi > trigger: on = False if ddi < trigger * 0.4: on = True expo[i] = 1.0 if on else 0.4 return pd.Series(expo * rv, index=r.index) def both_daily(): naked = combo_daily() return naked, apply_dd_guard(naked) def load(): return json.loads(STATE.read_text()) if STATE.exists() else None def save(st): STATE_DIR.mkdir(parents=True, exist_ok=True) STATE.write_text(json.dumps(st, indent=2)) def advance(): naked, guard = both_daily() st = load() if st is None or "equity_g" not in st: # init (o migrazione a doppia versione) last = str(naked.index[-1]) st = dict(start=last, last=last, initial=INITIAL, n_days=0, w_crypto=W_CRYPTO, dd_trigger=DD_TRIGGER, equity=INITIAL, peak=INITIAL, max_dd=0.0, equity_g=INITIAL, peak_g=INITIAL, max_dd_g=0.0) save(st); EQ.write_text("date,nudo,protetto\n" + f"{last},{INITIAL},{INITIAL}\n") return st last = pd.Timestamp(st["last"]) nn = naked[naked.index > last]; gg = guard[guard.index > last] if len(nn): e = st["equity"]; pk = st["peak"]; dd = st["max_dd"] eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = [] for d in nn.index: e *= (1 + float(nn[d])); pk = max(pk, e); dd = max(dd, (pk - e) / pk if pk > 0 else 0) eg *= (1 + float(gg[d])); pkg = max(pkg, eg); ddg = max(ddg, (pkg - eg) / pkg if pkg > 0 else 0) lines.append(f"{d},{e:.4f},{eg:.4f}") st.update(equity=e, peak=pk, max_dd=dd, equity_g=eg, peak_g=pkg, max_dd_g=ddg, last=str(nn.index[-1]), n_days=st["n_days"] + len(nn)) save(st) with open(EQ, "a") as f: f.write("\n".join(lines) + "\n") return st def main(): a = sys.argv[1:] if "--reset" in a: for f in (STATE, EQ): f.unlink(missing_ok=True) print("paper combo azzerato.") st = load() if "--status" in a else advance() if st is None or "equity_g" not in st: st = advance() days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days rn = st["equity"] / st["initial"] - 1; rg = st["equity_g"] / st["initial"] - 1 gw = gtaa_weights(); asof = gw.pop("_asof", "?"); cash = gw.pop("_cash", None) print("PAPER COMBO — TP01 (Deribit) + GTAA (IB), forward-only, blend 50/50") print(f" start {st['start'][:10]} -> last {st['last'][:10]} ({days}g, {st['n_days']} barre)") print(f" NUDO : eq {st['equity']:.2f} ret {rn*100:+.2f}% maxDD {st['max_dd']*100:.1f}%") print(f" PROTETTO : eq {st['equity_g']:.2f} ret {rg*100:+.2f}% maxDD {st['max_dd_g']*100:.1f}% (guardia-DD -{st.get('dd_trigger',DD_TRIGGER)*100:.0f}%)") print(f" --- POSIZIONI AZIONABILI ---") print(f" TP01 (Deribit): {_tp01_positions()}") print(f" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}") if __name__ == "__main__": main()