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,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