research: strato trend multi-asset (52 alt) RIDONDANTE col trend TP01 -> non aggiunto

TSMOM CANONICAL applicato a ogni alt dei 52, equal-weight. Standalone FULL 0.66 ma HOLD-OUT -1.03
(long negli alt nel calo 2025-26), corr a TP01 +0.74 (stessa beta direzionale). Contributo al
portafoglio NEGATIVO (HOLD -0.16/-0.27). Broadenizzare il TREND non diversifica: e' la stessa
direzionalita' su asset piu' rumorosi. Solo il market-neutral (XS01) diversifica davvero.

Chiude il filone espansione-universo (XS-52, top-liquidita' dinamico, trend-52: tutti peggiori).
Configurazione validata invariata: TP01 70% + XS01 (19 major) 30%, FULL Sh 1.41 / HOLD 1.15.
I margini reali sono in un MECCANISMO diverso (opzioni VRP), non nell'universo crypto-direzionale.
Diario 2026-06-19-trend-multiasset.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-19 21:17:16 +00:00
parent 182d4eeac2
commit bf6ade51af
2 changed files with 115 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
"""STRATO TREND MULTI-ASSET sui 52 alt Hyperliquid certificati (diversificazione del trend).
TP01 e' TSMOM vol-target long-flat su BTC+ETH (2 asset). Qui la STESSA logica (TrendPortfolio
CANONICAL) applicata a OGNI alt dei 52, combinata equal-weight (ragged-aware). Idea: un trend
piu' diversificato. Test onesto: e' correlato a TP01 (entrambi trend)? aggiunge al portafoglio
TP01+XS01 nel hold-out? Causale, netto fee.
uv run python scripts/portfolio/trend_multiasset.py
"""
from __future__ import annotations
import sys, glob
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.strategies.trend_portfolio import TrendPortfolio, CANONICAL, simple_returns
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve, xsec_sleeve
RAW = PROJECT_ROOT / "data" / "raw"
def alt_trend_returns(min_assets=8):
"""Net returns per-asset (TSMOM CANONICAL long-flat vol-target) -> book equal-weight ragged."""
eng = TrendPortfolio(**CANONICAL)
series = {}
for p in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(p).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(p)
d = d.copy(); d["datetime"] = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
c = d["close"].values.astype(float)
r = simple_returns(c); tgt = eng.target_series(d)
held = np.zeros(len(tgt)); held[1:] = tgt[:-1]
net = held * r - eng.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0
series[sym] = pd.Series(np.clip(net, -0.99, None), index=d["datetime"])
M = pd.concat(series, axis=1, join="outer").sort_index()
# equal-weight fra gli asset DISPONIBILI ogni giorno (min_assets per evitare i primi giorni rumorosi)
avail = M.notna().sum(axis=1)
book = M.mean(axis=1, skipna=True).where(avail >= min_assets)
return book.dropna(), M
def ev(d, label):
f = metrics(d); h = metrics(d[d.index >= HOLDOUT])
yr = [float((1 + g).prod() - 1) for _, g in d.groupby(d.index.year)]
pct = sum(v > 0 for v in yr) / len(yr) if yr else 0
print(f" {label:<28} FULL Sh {f['sharpe']:>5.2f} ret {f['ret']*100:>+6.0f}% DD {f['maxdd']*100:>4.0f}% | "
f"HOLD Sh {h['sharpe']:>5.2f} | anni+ {pct*100:.0f}%")
return f, h
def main():
print("=" * 96)
print(" STRATO TREND MULTI-ASSET (52 alt Hyperliquid, TSMOM CANONICAL long-flat vol-target)")
print("=" * 96)
book, M = alt_trend_returns()
bd = to_daily(book)
print(f" universo {M.shape[1]} alt, book [{bd.index[0].date()} -> {bd.index[-1].date()}]\n")
ev(bd, "TREND-52alt standalone")
tp = tp01_sleeve().daily(); xs = xsec_sleeve().daily()
def corr(a, b):
J = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return float(J["a"].corr(J["b"])) if len(J) > 5 else float("nan")
print(f"\n correlazioni: TREND-52 vs TP01 {corr(bd, tp):+.2f} | vs XS01 {corr(bd, xs):+.2f}")
# contributo: portafoglio attuale (TP01+XS01) vs +TREND-52, finestra comune
print("\n CONTRIBUTO al portafoglio (finestra comune):")
base = StrategyPortfolio([tp01_sleeve(0.70), xsec_sleeve(0.30)]).backtest()
J = pd.concat({"tp": tp, "xs": xs, "tr": bd}, axis=1, join="inner").dropna()
print(f" [comune {J.index[0].date()} -> {J.index[-1].date()}]")
# baseline sulla finestra comune (TP01 0.7 + XS 0.3 rinormalizzato)
base_c = 0.7 * J["tp"] + 0.3 * J["xs"]
bf, bh = metrics(base_c), metrics(base_c[base_c.index >= HOLDOUT])
print(f" TP01 70 + XS 30 (attuale) FULL Sh {bf['sharpe']:.2f} DD {bf['maxdd']*100:.0f}% | HOLD Sh {bh['sharpe']:.2f}")
for wtr in (0.2, 0.3):
wt, wx = 0.7 * (1 - wtr), 0.3 * (1 - wtr)
comb = wt * J["tp"] + wx * J["xs"] + wtr * J["tr"]
cf, ch = metrics(comb), metrics(comb[comb.index >= HOLDOUT])
print(f" +TREND-52 w{wtr:.0%} FULL Sh {cf['sharpe']:.2f} ({cf['sharpe']-bf['sharpe']:+.2f}) DD {cf['maxdd']*100:.0f}% | HOLD Sh {ch['sharpe']:.2f} ({ch['sharpe']-bh['sharpe']:+.2f})")
print("\n -> aggiungere se: scorrelato a TP01/XS01 e migliora FULL E HOLD. Se molto correlato a")
print(" TP01 (entrambi trend) e contributo marginale, e' ridondante -> non si aggiunge.")
if __name__ == "__main__":
main()