diff --git a/docs/diary/2026-06-23-combo-paper-deploy.md b/docs/diary/2026-06-23-combo-paper-deploy.md new file mode 100644 index 0000000..4501d65 --- /dev/null +++ b/docs/diary/2026-06-23-combo-paper-deploy.md @@ -0,0 +1,33 @@ +# 2026-06-23 — Combo DEPLOYABLE in PAPER: TP01 (Deribit) + GTAA (IB), cross-venue + +## Decisione +Dopo aver esaurito (onestamente) la ricerca di nuovi edge e anticipazioni cross-mercato, l'unica cosa +VERA e deployabile e' la DIVERSIFICAZIONE: TP01 (crypto, Deribit) + GTAA (equity, IB), corr ~0.21 -> +blend Sharpe ~1.5, maxDD dimezzato (diari 2026-06-22-deployable-combo). Si va in PAPER cross-venue. + +## Costruito +- **`src/portfolio/gtaa.py`** — GTAA come sleeve di prima classe: trend difensivo long-flat TSMOM + [21/63/126/252g], vol-target 12%, EW su SPY/QQQ/IWM/TLT/GLD/HYG. Espone `gtaa_returns()` (Sharpe + full 0.64, 7542 barre 1996+) e `gtaa_weights()` (pesi ETF CORRENTI azionabili). Legge cache eq_*. +- **`scripts/live/paper_combo.py`** — paper-tracker FORWARD-ONLY del blend 50/50 TP01+GTAA (crypto + compoundato sul grid giorni-di-borsa). Stato in data/paper_combo/. Mostra le posizioni azionabili + su entrambi i venue. SOLO le gambe eseguibili (XS01/VRP01 STAT-MODE esclusi). +- **`fetch_ib_equities.py --only SPY,QQQ,...`** — refresh mirato dei 6 ETF GTAA (per il cron). +- **`scripts/cron_daily.sh`** — aggiunto: up gateway IB (idempotente) -> refresh ETF GTAA -> + avanza paper_combo. Dipendenza cross-venue gestita (gateway paper sempre-up, restart unless-stopped). + +## Stato iniziale (2026-06-23) +Paper combo init a 2000, forward da 2026-06-22. Posizioni azionabili: +- TP01 (Deribit): BTC/ETH 0.0x (flat, TSMOM risk-off — coerente col live). +- GTAA (IB): SPY 13% / QQQ 8% / IWM 9% / TLT 17% / GLD 2% / HYG 17% / cash 34% (difensivo). +Catena end-to-end testata: gateway -> refresh ETF -> avanza paper. OK. + +## Onesta' +- E' PAPER (rischio zero). Valida l'OPERATIVITA' cross-venue prima di capitale reale. +- Sharpe atteso ~1.5 e' ottimistico (finestra crypto corta/favorevole); il dato robusto e' la + diversificazione (corr 0.21, DD dimezzato), non il livello assoluto. +- A capitale reale e' un portafoglio su DUE conti (Deribit ~$600 + IB); GTAA frazionabile a basso + capitale, TP01 gia' armato. Prossimo passo eventuale: dashboard del combo + (molto dopo) capitale. + +## Prossimo +Lasciar girare il paper forward (cron giornaliero) e ricontrollare l'equity tra qualche settimana. diff --git a/scripts/cron_daily.sh b/scripts/cron_daily.sh index 4bceeb0..e16c1eb 100755 --- a/scripts/cron_daily.sh +++ b/scripts/cron_daily.sh @@ -11,5 +11,10 @@ mkdir -p logs uv run python scripts/live/paper_portfolio.py # avanza paper TP01+XS01 uv run python scripts/live/paper_prevday.py # forward-monitor lead prevday-breakout (PAPER, non deploy) uv run python scripts/live/live_execute.py --execute # TP01 LIVE su Deribit (gated da config/live.json) + # --- COMBO cross-venue (PAPER): refresh ETF IB (GTAA) + avanza paper TP01+GTAA --- + docker compose up -d ib-gateway >/dev/null 2>&1 # gateway IB paper (idempotente) + for i in $(seq 1 25); do (echo > /dev/tcp/127.0.0.1/4002) >/dev/null 2>&1 && break; sleep 6; done + uv run --with ib_async python scripts/research/fetch_ib_equities.py --only SPY,QQQ,IWM,TLT,GLD,HYG # ETF GTAA freschi + uv run python scripts/live/paper_combo.py # avanza paper combo (forward-only) echo "===== done $(date -u '+%H:%M:%SZ') =====" } >> logs/cron_daily.log 2>&1 diff --git a/scripts/live/paper_combo.py b/scripts/live/paper_combo.py new file mode 100644 index 0000000..1c53f56 --- /dev/null +++ b/scripts/live/paper_combo.py @@ -0,0 +1,101 @@ +"""PAPER COMBO — forward-only del combo DEPLOYABLE cross-venue: TP01 (Deribit) + GTAA (IB). + +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). + +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 +""" +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 # blend di capitale TP01/GTAA (50/50; risk-parity ~30/70 alternativa) + + +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") + 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 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(): + r = combo_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") + 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)) + 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: + 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) + 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" --- 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%}") + + +if __name__ == "__main__": + main() diff --git a/scripts/research/fetch_ib_equities.py b/scripts/research/fetch_ib_equities.py index 199992b..b385d2f 100644 --- a/scripts/research/fetch_ib_equities.py +++ b/scripts/research/fetch_ib_equities.py @@ -88,7 +88,11 @@ def main(): print("=" * 104) rep, ok = [], [] force = "--force" in sys.argv[1:] - for sym in UNIVERSE: + universe = UNIVERSE + if "--only" in sys.argv[1:]: # refresh mirato (es. solo i 6 ETF GTAA per il cron) + universe = sys.argv[sys.argv.index("--only") + 1].upper().split(",") + force = True # --only implica refresh dei simboli indicati + for sym in universe: out_path = RAW / f"eq_{sym.lower()}_1d.parquet" if out_path.exists() and not force: print(f" {sym:5} GIA' SU DISCO -> skip (usa --force per riscaricare)") diff --git a/src/portfolio/gtaa.py b/src/portfolio/gtaa.py new file mode 100644 index 0000000..c24f352 --- /dev/null +++ b/src/portfolio/gtaa.py @@ -0,0 +1,73 @@ +"""GTAA — sleeve EQUITY/ETF: trend difensivo multi-asset (analogo di TP01, su IB). + +Validato sul branch research/equities-ib (diari 2026-06-22-equities-*). Trend long-flat TSMOM +multi-orizzonte su un paniere di classi (azioni US/tech/small + bond + oro + credito), equal-weight +sugli asset in trend-up, vol-target 12%. Difensivo: Sharpe ~0.64 / maxDD ~15% standalone, e — il punto +— corr ~0.21 col crypto (TP01) -> diversificatore reale (blend Sharpe ~1.5, DD dimezzato). + +Eseguibile su IB a basso capitale (ETF frazionabili, switch mensile/basso turnover). Legge la CACHE +su disco data/raw/eq_*.parquet (ADJUSTED_LAST, scritta da scripts/research/fetch_ib_equities.py); +in produzione va rinfrescata giornalmente (gateway IB). Espone rendimenti + PESI CORRENTI (posizioni). +""" +from __future__ import annotations +from functools import lru_cache +from pathlib import Path +import numpy as np, pandas as pd + +RAW = Path(__file__).resolve().parents[2] / "data" / "raw" +EQ_UNIVERSE = ("SPY", "QQQ", "IWM", "TLT", "GLD", "HYG") +HORIZONS = (21, 63, 126, 252) +TARGET_VOL = 0.12 +FEE_SIDE = 0.0002 +ANN = np.sqrt(252.0) + + +@lru_cache(maxsize=16) +def _close(sym: str) -> pd.Series: + p = RAW / f"eq_{sym.lower()}_1d.parquet" + if not p.exists(): + raise FileNotFoundError(f"{p} assente — gira scripts/research/fetch_ib_equities.py") + d = pd.read_parquet(p) + return pd.Series(d["close"].astype(float).values, + index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index() + + +def _exposure(close: pd.Series) -> pd.Series: + """Esposizione long-flat [0,1] su un asset: frazione di orizzonti in trend-up, vol-targeted, cap 1. + Causale (solo dati <= i).""" + px = close.values; n = len(px); tgt = np.zeros(n); mh = max(HORIZONS) + for i in range(mh, n): + tgt[i] = np.mean([1.0 if px[i] > px[i - H] else 0.0 for H in HORIZONS]) + s = pd.Series(tgt, index=close.index) + rv = close.pct_change().rolling(63, min_periods=20).std().shift(1) * ANN + scale = np.clip(np.nan_to_num(TARGET_VOL / rv.replace(0, np.nan).values, nan=0.0), 0, 10.0) + return (s * scale).clip(0, 1.0) + + +def _gated_returns(sym: str) -> pd.Series: + close = _close(sym); ex = _exposure(close) + ret = close.pct_change().fillna(0.0).values + held = np.zeros(len(ex)); held[1:] = ex.values[:-1] # causale: esposizione decisa a i-1, tenuta in i + net = held * ret - FEE_SIDE * np.abs(np.diff(held, prepend=0.0)) + return pd.Series(net, index=close.index) + + +def gtaa_returns(universe=EQ_UNIVERSE) -> pd.Series: + """Rendimenti netti daily del GTAA: EW dei rendimenti trend-gated sugli asset disponibili.""" + cols = {a: _gated_returns(a) for a in universe} + return pd.concat(cols, axis=1).sort_index().mean(axis=1, skipna=True).dropna() + + +def gtaa_weights(universe=EQ_UNIVERSE) -> dict: + """Pesi target CORRENTI (ultima barra): quanto allocare a ciascun ETF (e quanto in cash). + weight_i = esposizione_i / N_disponibili. Azionabile su IB.""" + out = {}; n = len(universe) + for a in universe: + try: + ex = _exposure(_close(a)) + out[a] = round(float(ex.iloc[-1]) / n, 4) + except FileNotFoundError: + continue + out["_cash"] = round(1.0 - sum(out.values()), 4) + out["_asof"] = str(_close("SPY").index[-1].date()) + return out