feat(combo): paper combo NUDO vs PROTETTO (guardia-DD -4%) affiancati + dashboard

paper_combo traccia forward entrambe le versioni; dashboard mostra nudo + protetto. Guardia-DD:
de-risk 0.4x a DD>-4%, ri-rischia a -1.6% (backtest MaxDD 8.4->5.8%, 2022 -4.4->-1.8%). Opzioni
escluse (non aiutano il grind). Container ricostruito.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-23 12:59:07 +00:00
parent 1c15c3c1be
commit 010d1f0733
2 changed files with 59 additions and 39 deletions
+52 -36
View File
@@ -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__":
+7 -3
View File
@@ -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"<b>{cb['equity']:.2f}</b> (start {cb['initial']:.0f}, {cb['start'][:10]} "
f"{cb['last'][:10]}, {cdays}g, {cb['n_days']} barre) &nbsp; ret <b>{cret*100:+.2f}%</b> "
f"&nbsp; maxDD {cb['max_dd']*100:.1f}% &nbsp; 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}<br><b>NUDO</b> &nbsp; eq <b>{cb['equity']:.2f}</b> &nbsp; ret <b>{cret*100:+.2f}%</b> &nbsp; maxDD {cb['max_dd']*100:.1f}%"
if "equity_g" in cb:
crg = cb["equity_g"] / cb["initial"] - 1
combo_html += (f"<br><b>PROTETTO</b> (guardia-DD {cb.get('dd_trigger',0.04)*100:.0f}%) &nbsp; "
f"eq <b>{cb['equity_g']:.2f}</b> &nbsp; ret <b>{crg*100:+.2f}%</b> &nbsp; maxDD {cb['max_dd_g']*100:.1f}%")
else:
combo_html = "non inizializzato (gira <code>scripts/live/paper_combo.py</code>)"
gw = d.get("gtaa_weights")