diff --git a/scripts/live/paper_combo.py b/scripts/live/paper_combo.py index 1c53f56..05b8e53 100644 --- a/scripts/live/paper_combo.py +++ b/scripts/live/paper_combo.py @@ -1,18 +1,14 @@ -"""PAPER COMBO — forward-only del combo DEPLOYABLE cross-venue: TP01 (Deribit) + GTAA (IB). +"""PAPER COMBO — forward-only del combo cross-venue TP01 (Deribit) + GTAA (IB), NUDO vs PROTETTO. -Le DUE gambe realmente eseguibili a basso capitale (XS01/VRP01 restano STAT-MODE, esclusi qui): - * TP01 = TSMOM difensivo BTC/ETH long-flat, gia' armato su Deribit; - * GTAA = trend difensivo multi-asset su ETF, eseguibile su IB. -Scorrelati (corr ~0.21) -> blend Sharpe ~1.5, maxDD dimezzato (diari 2026-06-22-deployable-combo). +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. -Traccia FORWARD-ONLY l'equity del blend (default 50/50 capitale), applicando i rendimenti giornalieri -combinati man mano che arrivano barre nuove. Il crypto (calendario 7g) e' compoundato sul grid dei -giorni di borsa per allinearlo all'equity. NESSUNA esecuzione reale. Mostra le POSIZIONI azionabili -su entrambi i venue (TP01 target BTC/ETH + pesi ETF GTAA). - - uv run python scripts/live/paper_combo.py # avanza (init al 1o run) - uv run python scripts/live/paper_combo.py --status # solo stato - uv run python scripts/live/paper_combo.py --reset + uv run python scripts/live/paper_combo.py [--status|--reset] """ from __future__ import annotations import sys, json @@ -27,11 +23,11 @@ STATE_DIR = PROJECT_ROOT / "data" / "paper_combo" STATE = STATE_DIR / "state.json" EQ = STATE_DIR / "equity.csv" INITIAL = 2000.0 -W_CRYPTO = 0.5 # blend di capitale TP01/GTAA (50/50; risk-parity ~30/70 alternativa) +W_CRYPTO = 0.5 +DD_TRIGGER = 0.04 # guardia-drawdown della versione PROTETTA def combo_daily(wc: float = W_CRYPTO) -> pd.Series: - """Rendimenti netti daily del blend sul grid giorni-di-borsa (crypto compoundato).""" tp = _tp01_returns() if tp.index.tz is None: tp.index = tp.index.tz_localize("UTC") @@ -43,6 +39,23 @@ def combo_daily(wc: float = W_CRYPTO) -> pd.Series: 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 @@ -53,23 +66,26 @@ def save(st): def advance(): - r = combo_daily() + naked, guard = both_daily() st = load() - if st is None: - last = str(r.index[-1]) - st = dict(start=last, last=last, equity=INITIAL, initial=INITIAL, peak=INITIAL, - max_dd=0.0, n_days=0, w_crypto=W_CRYPTO) - save(st) - EQ.write_text("date,equity\n" + f"{last},{INITIAL}\n") + 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"]) - new = r[r.index > last] - if len(new): - eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"]; lines = [] - for d, ret in new.items(): - eq *= (1.0 + float(ret)); peak = max(peak, eq); dd = max(dd, (peak - eq) / peak if peak > 0 else 0) - lines.append(f"{d},{eq:.4f}") - st.update(equity=eq, last=str(new.index[-1]), peak=peak, max_dd=dd, n_days=st["n_days"] + len(new)) + 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") @@ -83,18 +99,18 @@ def main(): f.unlink(missing_ok=True) print("paper combo azzerato.") st = load() if "--status" in a else advance() - if st is None: + if st is None or "equity_g" not in st: st = advance() days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days - ret = st["equity"] / st["initial"] - 1 - gw = gtaa_weights() - asof = gw.pop("_asof", "?"); cash = gw.pop("_cash", None) + 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" equity {st['equity']:.2f} (start {st['initial']:.0f}) ret {ret*100:+.2f}% maxDD {st['max_dd']*100:.1f}%") + 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, frazione equity x leva): {_tp01_positions()}") - print(f" GTAA (IB, peso ETF, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}") + 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__": diff --git a/src/live/dashboard.py b/src/live/dashboard.py index 71bd5ff..1803147 100644 --- a/src/live/dashboard.py +++ b/src/live/dashboard.py @@ -117,9 +117,13 @@ def html(): cdays = (pd.Timestamp(cb["last"]) - pd.Timestamp(cb["start"])).days cret = cb["equity"] / cb["initial"] - 1 wc = cb.get("w_crypto", 0.5) - combo_html = (f"{cb['equity']:.2f} (start {cb['initial']:.0f}, {cb['start'][:10]} → " - f"{cb['last'][:10]}, {cdays}g, {cb['n_days']} barre)   ret {cret*100:+.2f}% " - f"  maxDD {cb['max_dd']*100:.1f}%   blend {wc*100:.0f}/{(1-wc)*100:.0f} TP01/GTAA") + head = (f"start {cb['initial']:.0f} · {cb['start'][:10]} → {cb['last'][:10]} ({cdays}g, " + f"{cb['n_days']} barre) · blend {wc*100:.0f}/{(1-wc)*100:.0f} TP01/GTAA") + combo_html = f"{head}
NUDO   eq {cb['equity']:.2f}   ret {cret*100:+.2f}%   maxDD {cb['max_dd']*100:.1f}%" + if "equity_g" in cb: + crg = cb["equity_g"] / cb["initial"] - 1 + combo_html += (f"
PROTETTO (guardia-DD −{cb.get('dd_trigger',0.04)*100:.0f}%)   " + f"eq {cb['equity_g']:.2f}   ret {crg*100:+.2f}%   maxDD {cb['max_dd_g']*100:.1f}%") else: combo_html = "non inizializzato (gira scripts/live/paper_combo.py)" gw = d.get("gtaa_weights")