feat(combo): paper-trade cross-venue TP01 (Deribit) + GTAA (IB), forward-only

L'unica cosa vera/deployabile della ricerca: diversificazione TP01+GTAA (corr 0.21, blend Sharpe ~1.5,
DD dimezzato). Si va in PAPER cross-venue.

- src/portfolio/gtaa.py: GTAA sleeve di prima classe (trend difensivo TSMOM vol-target 12% su
  SPY/QQQ/IWM/TLT/GLD/HYG). gtaa_returns() Sharpe 0.64; gtaa_weights() = pesi ETF correnti azionabili.
- scripts/live/paper_combo.py: tracker forward-only blend 50/50 TP01+GTAA (crypto compoundato su grid
  giorni-di-borsa), mostra posizioni azionabili su entrambi i venue. Solo gambe eseguibili.
- fetch_ib_equities.py --only: refresh mirato dei 6 ETF GTAA per il cron.
- cron_daily.sh: up gateway IB + refresh ETF GTAA + avanza paper_combo (dipendenza cross-venue gestita).

Init 2026-06-23: TP01 flat (risk-off), GTAA SPY13/QQQ8/IWM9/TLT17/GLD2/HYG17/cash34. Catena
gateway->refresh->paper testata end-to-end. PAPER (rischio zero), valida l'operativita' cross-venue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-23 12:26:51 +00:00
parent 09c8bb7de8
commit 3b552a92da
5 changed files with 217 additions and 1 deletions
@@ -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.
+5
View File
@@ -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
+101
View File
@@ -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()
+5 -1
View File
@@ -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)")
+73
View File
@@ -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