010d1f0733
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>
118 lines
5.1 KiB
Python
118 lines
5.1 KiB
Python
"""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()
|