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:
+52
-36
@@ -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):
|
Le due gambe eseguibili a basso capitale (XS01/VRP01 STAT-MODE esclusi), scorrelate (corr ~0.21) ->
|
||||||
* TP01 = TSMOM difensivo BTC/ETH long-flat, gia' armato su Deribit;
|
blend Sharpe ~1.5, DD dimezzato. Traccia FORWARD-ONLY DUE versioni in parallelo:
|
||||||
* GTAA = trend difensivo multi-asset su ETF, eseguibile su IB.
|
* NUDO = blend 50/50 TP01+GTAA
|
||||||
Scorrelati (corr ~0.21) -> blend Sharpe ~1.5, maxDD dimezzato (diari 2026-06-22-deployable-combo).
|
* 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
|
uv run python scripts/live/paper_combo.py [--status|--reset]
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import sys, json
|
import sys, json
|
||||||
@@ -27,11 +23,11 @@ STATE_DIR = PROJECT_ROOT / "data" / "paper_combo"
|
|||||||
STATE = STATE_DIR / "state.json"
|
STATE = STATE_DIR / "state.json"
|
||||||
EQ = STATE_DIR / "equity.csv"
|
EQ = STATE_DIR / "equity.csv"
|
||||||
INITIAL = 2000.0
|
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:
|
def combo_daily(wc: float = W_CRYPTO) -> pd.Series:
|
||||||
"""Rendimenti netti daily del blend sul grid giorni-di-borsa (crypto compoundato)."""
|
|
||||||
tp = _tp01_returns()
|
tp = _tp01_returns()
|
||||||
if tp.index.tz is None:
|
if tp.index.tz is None:
|
||||||
tp.index = tp.index.tz_localize("UTC")
|
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()
|
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():
|
def load():
|
||||||
return json.loads(STATE.read_text()) if STATE.exists() else None
|
return json.loads(STATE.read_text()) if STATE.exists() else None
|
||||||
|
|
||||||
@@ -53,23 +66,26 @@ def save(st):
|
|||||||
|
|
||||||
|
|
||||||
def advance():
|
def advance():
|
||||||
r = combo_daily()
|
naked, guard = both_daily()
|
||||||
st = load()
|
st = load()
|
||||||
if st is None:
|
if st is None or "equity_g" not in st: # init (o migrazione a doppia versione)
|
||||||
last = str(r.index[-1])
|
last = str(naked.index[-1])
|
||||||
st = dict(start=last, last=last, equity=INITIAL, initial=INITIAL, peak=INITIAL,
|
st = dict(start=last, last=last, initial=INITIAL, n_days=0, w_crypto=W_CRYPTO, dd_trigger=DD_TRIGGER,
|
||||||
max_dd=0.0, n_days=0, w_crypto=W_CRYPTO)
|
equity=INITIAL, peak=INITIAL, max_dd=0.0,
|
||||||
save(st)
|
equity_g=INITIAL, peak_g=INITIAL, max_dd_g=0.0)
|
||||||
EQ.write_text("date,equity\n" + f"{last},{INITIAL}\n")
|
save(st); EQ.write_text("date,nudo,protetto\n" + f"{last},{INITIAL},{INITIAL}\n")
|
||||||
return st
|
return st
|
||||||
last = pd.Timestamp(st["last"])
|
last = pd.Timestamp(st["last"])
|
||||||
new = r[r.index > last]
|
nn = naked[naked.index > last]; gg = guard[guard.index > last]
|
||||||
if len(new):
|
if len(nn):
|
||||||
eq = st["equity"]; peak = st["peak"]; dd = st["max_dd"]; lines = []
|
e = st["equity"]; pk = st["peak"]; dd = st["max_dd"]
|
||||||
for d, ret in new.items():
|
eg = st["equity_g"]; pkg = st["peak_g"]; ddg = st["max_dd_g"]; lines = []
|
||||||
eq *= (1.0 + float(ret)); peak = max(peak, eq); dd = max(dd, (peak - eq) / peak if peak > 0 else 0)
|
for d in nn.index:
|
||||||
lines.append(f"{d},{eq:.4f}")
|
e *= (1 + float(nn[d])); pk = max(pk, e); dd = max(dd, (pk - e) / pk if pk > 0 else 0)
|
||||||
st.update(equity=eq, last=str(new.index[-1]), peak=peak, max_dd=dd, n_days=st["n_days"] + len(new))
|
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)
|
save(st)
|
||||||
with open(EQ, "a") as f:
|
with open(EQ, "a") as f:
|
||||||
f.write("\n".join(lines) + "\n")
|
f.write("\n".join(lines) + "\n")
|
||||||
@@ -83,18 +99,18 @@ def main():
|
|||||||
f.unlink(missing_ok=True)
|
f.unlink(missing_ok=True)
|
||||||
print("paper combo azzerato.")
|
print("paper combo azzerato.")
|
||||||
st = load() if "--status" in a else advance()
|
st = load() if "--status" in a else advance()
|
||||||
if st is None:
|
if st is None or "equity_g" not in st:
|
||||||
st = advance()
|
st = advance()
|
||||||
days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days
|
days = (pd.Timestamp(st["last"]) - pd.Timestamp(st["start"])).days
|
||||||
ret = st["equity"] / st["initial"] - 1
|
rn = st["equity"] / st["initial"] - 1; rg = st["equity_g"] / st["initial"] - 1
|
||||||
gw = gtaa_weights()
|
gw = gtaa_weights(); asof = gw.pop("_asof", "?"); cash = gw.pop("_cash", None)
|
||||||
asof = gw.pop("_asof", "?"); cash = gw.pop("_cash", None)
|
|
||||||
print("PAPER COMBO — TP01 (Deribit) + GTAA (IB), forward-only, blend 50/50")
|
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" 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" --- POSIZIONI AZIONABILI ---")
|
||||||
print(f" TP01 (Deribit, frazione equity x leva): {_tp01_positions()}")
|
print(f" TP01 (Deribit): {_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" GTAA (IB, asof {asof}): " + ", ".join(f"{k} {v:.0%}" for k, v in gw.items() if v) + f" | cash {cash:.0%}")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -117,9 +117,13 @@ def html():
|
|||||||
cdays = (pd.Timestamp(cb["last"]) - pd.Timestamp(cb["start"])).days
|
cdays = (pd.Timestamp(cb["last"]) - pd.Timestamp(cb["start"])).days
|
||||||
cret = cb["equity"] / cb["initial"] - 1
|
cret = cb["equity"] / cb["initial"] - 1
|
||||||
wc = cb.get("w_crypto", 0.5)
|
wc = cb.get("w_crypto", 0.5)
|
||||||
combo_html = (f"<b>{cb['equity']:.2f}</b> (start {cb['initial']:.0f}, {cb['start'][:10]} → "
|
head = (f"start {cb['initial']:.0f} · {cb['start'][:10]} → {cb['last'][:10]} ({cdays}g, "
|
||||||
f"{cb['last'][:10]}, {cdays}g, {cb['n_days']} barre) ret <b>{cret*100:+.2f}%</b> "
|
f"{cb['n_days']} barre) · blend {wc*100:.0f}/{(1-wc)*100:.0f} TP01/GTAA")
|
||||||
f" maxDD {cb['max_dd']*100:.1f}% blend {wc*100:.0f}/{(1-wc)*100:.0f} TP01/GTAA")
|
combo_html = f"{head}<br><b>NUDO</b> eq <b>{cb['equity']:.2f}</b> ret <b>{cret*100:+.2f}%</b> 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}%) "
|
||||||
|
f"eq <b>{cb['equity_g']:.2f}</b> ret <b>{crg*100:+.2f}%</b> maxDD {cb['max_dd_g']*100:.1f}%")
|
||||||
else:
|
else:
|
||||||
combo_html = "non inizializzato (gira <code>scripts/live/paper_combo.py</code>)"
|
combo_html = "non inizializzato (gira <code>scripts/live/paper_combo.py</code>)"
|
||||||
gw = d.get("gtaa_weights")
|
gw = d.get("gtaa_weights")
|
||||||
|
|||||||
Reference in New Issue
Block a user