diff --git a/docs/diary/2026-06-19-trend-multiasset.md b/docs/diary/2026-06-19-trend-multiasset.md new file mode 100644 index 0000000..57692e8 --- /dev/null +++ b/docs/diary/2026-06-19-trend-multiasset.md @@ -0,0 +1,28 @@ +# 2026-06-19 — Strato trend multi-asset sui 52 alt: RIDONDANTE col trend di TP01 + +Tentativo: aggiungere un terzo sleeve = TSMOM (stessa logica TP01 CANONICAL, long-flat vol-target) +applicato a OGNI alt dei 52 Hyperliquid certificati, equal-weight ragged. Idea: trend più +diversificato che diversifichi TP01 (BTC/ETH). `scripts/portfolio/trend_multiasset.py`. + +## Esito: ridondante e peggiore +- **TREND-52 standalone**: FULL Sh 0.66, **HOLD-OUT −1.03** (negativo), anni+ 33%. Gli alt sono + stati long nel calo 2025-26 e hanno sanguinato — a differenza di TP01 (BTC/ETH) che il + vol-target+trend portò in cash. I trend degli alt sono più rumorosi/whippy. +- **corr a TP01 = +0.74** (stessa beta direzionale, come previsto) | corr a XS01 −0.05. +- **Contributo al portafoglio (TP01 70 + XS 30):** +TREND-52 w20% → FULL −0.01, **HOLD −0.16**; + w30% → FULL −0.02, **HOLD −0.27**. PEGGIORA. + +## Lezione +Broadenizzare il TREND su molti alt NON diversifica: è la **stessa direzionalità** (corr 0.74 con +TP01) su asset più rumorosi → aggiunge perdita/rumore, non edge. La dimensione trend è già catturata +in modo pulito da TP01 (BTC/ETH, vol-targeted). L'unica espansione che diversifica davvero resta +quella **market-neutral** (XS01 cross-sectional), perché è ortogonale alla beta direzionale. + +## Conclusione (chiusura del filone "espansione universo") +Esplorate tutte le vie di espansione sui certificati Hyperliquid: +1. XS su 52-all → diluito (memecoin), negativo. +2. XS top-liquidità dinamico → peggiore del fisso-19 (liquidità ≠ qualità). +3. Trend multi-asset su 52 → ridondante (corr 0.74) + hold-out negativo. +Nessuna migliora il portafoglio. **Configurazione validata e invariata: TP01 70% + XS01 (19 major) +30% — FULL Sh 1.41 / HOLD 1.15.** I margini reali per crescere NON sono nell'universo crypto- +direzionale (saturo), ma in un MECCANISMO diverso (opzioni VRP, in attesa di dati di stress reali). diff --git a/scripts/portfolio/trend_multiasset.py b/scripts/portfolio/trend_multiasset.py new file mode 100644 index 0000000..cb95319 --- /dev/null +++ b/scripts/portfolio/trend_multiasset.py @@ -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()