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:
@@ -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.
|
||||||
@@ -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_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/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)
|
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') ====="
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
} >> logs/cron_daily.log 2>&1
|
} >> logs/cron_daily.log 2>&1
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -88,7 +88,11 @@ def main():
|
|||||||
print("=" * 104)
|
print("=" * 104)
|
||||||
rep, ok = [], []
|
rep, ok = [], []
|
||||||
force = "--force" in sys.argv[1:]
|
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"
|
out_path = RAW / f"eq_{sym.lower()}_1d.parquet"
|
||||||
if out_path.exists() and not force:
|
if out_path.exists() and not force:
|
||||||
print(f" {sym:5} GIA' SU DISCO -> skip (usa --force per riscaricare)")
|
print(f" {sym:5} GIA' SU DISCO -> skip (usa --force per riscaricare)")
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user