"""CROSS-SECTIONAL su universo Hyperliquid certificato (19 alt, 1d, 2024-2026). Strategia market-neutral: ogni H giorni classifica gli asset per rendimento a L giorni (causale), va long i top-k / short i bottom-k (momentum) o viceversa (reversal), dollar-neutral, vol-target. Mira a DIVERSIFICARE TP01 (long-trend): se scorrelata e robusta, migliora il portafoglio. Gauntlet onesto: FULL (2024-26) + within-window OOS (2025+) + per-anno + corr TP01 + contributo. Caveat: storia corta (~2.5 anni). Risultati suggestivi, non robusti come BTC/ETH 6 anni. uv run python scripts/portfolio/xsec_research.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.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio from src.portfolio.sleeves import tp01_sleeve RAW = PROJECT_ROOT / "data" / "raw" FEE = 0.001 def load_universe(): cols = {} for f in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))): s = Path(f).stem.replace("hl_", "").replace("_1d", "").upper() d = pd.read_parquet(f) cols[s] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() return C def xs_book(C, L, H, k, mode="mom", target_vol=0.20): """Rendimenti netti giornalieri di un book cross-sectional market-neutral. Causale.""" assets = list(C.columns); A = len(assets) px = C.values; n = len(px) dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) W = np.zeros((n, A)) # peso per asset per giorno (deciso a close[i], tenuto in i+1) w = np.zeros(A) for i in range(n): if i >= L and i % H == 0: lb = px[i] / px[i - L] - 1.0 order = np.argsort(lb) w = np.zeros(A) lo, hi = order[:k], order[-k:] # peggiori / migliori if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k # long forti / short deboli else: w[lo] = 0.5 / k; w[hi] = -0.5 / k # reversal W[i] = w # rendimento book: peso[i-1] guadagna dret[i]; fee su turnover gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) # W[i-1] guadagna dret[i] turn = np.zeros(n); turn[0] = np.abs(W[0]).sum() turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) # turnover per (ri)settare W[i] net = gross - turn * (FEE / 2.0) s = pd.Series(net, index=C.index) # vol-target (causale): scala per target/vol_realizzata(30) shiftata rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) return pd.Series(s.values * scale, index=C.index) def yr_breadth(daily): pre = daily yr = [float((1 + g).prod() - 1) for _, g in pre.groupby(pre.index.year)] consec = mx = 0 for v in yr: consec = consec + 1 if v < 0 else 0; mx = max(mx, consec) return yr, (sum(v > 0 for v in yr) / len(yr) if yr else 0), mx def main(): C = load_universe() print("=" * 96) print(f" CROSS-SECTIONAL Hyperliquid — {len(C.columns)} asset, {len(C)} giorni [{C.index[0].date()} -> {C.index[-1].date()}]") print("=" * 96) tp = tp01_sleeve(1.0); tp_daily = tp.daily() base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest() print(f"\n {'config':<24}{'FULL Sh':>9}{'OOS25 Sh':>10}{'ret%':>8}{'DD%':>7}{'corrTP':>8}{'anni+':>7}") cands = [] grid = [("mom",L,H,k) for L in (30,60,90) for H in (5,10,20) for k in (3,5)] \ + [("rev",L,H,k) for L in (3,7,14) for H in (3,5) for k in (3,5)] for mode,L,H,k in grid: d = to_daily(xs_book(C,L,H,k,mode)) f=metrics(d); oos=metrics(d[d.index>=HOLDOUT]) J=pd.concat({"tp":tp_daily,"x":d},axis=1,join="inner").dropna(); corr=float(J["tp"].corr(J["x"])) if len(J)>5 else float("nan") yr,pct,consec=yr_breadth(d) tag=f"{mode} L{L} H{H} k{k}" cands.append((tag,mode,L,H,k,f,oos,corr,pct,consec,d)) if f["sharpe"]>0.6 or oos["sharpe"]>0.8: print(f" {tag:<24}{f['sharpe']:>9.2f}{oos['sharpe']:>10.2f}{f['ret']*100:>+8.0f}{f['maxdd']*100:>7.1f}{corr:>+8.2f}{pct*100:>6.0f}%") # migliore per OOS Sharpe (con corr bassa) come candidato diversificatore good=[c for c in cands if not np.isnan(c[7]) and abs(c[7])<0.4 and c[5]["sharpe"]>0.5 and c[6]["sharpe"]>0] good.sort(key=lambda c:-(c[6]["sharpe"])) print(f"\n Candidati scorrelati(<0.4) e positivi (FULL>0.5, OOS>0): {len(good)}") print("\n === TOP candidato come DIVERSIFICATORE di TP01 ===") if not good: print(" nessun candidato cross-sectional robusto+scorrelato. Universo corto.") return tag,mode,L,H,k,f,oos,corr,pct,consec,d = good[0] print(f" {tag}: FULL Sh {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.1f}% | OOS25 Sh {oos['sharpe']:.2f} | corr TP01 {corr:+.2f} | anni+ {pct*100:.0f}% rossi-consec {consec}") per=[(y,round(v,3)) for y,(v) in zip([yy for yy,_ in d.groupby(d.index.year)], yr_breadth(d)[0])] print(f" per-anno: {per}") # CONFRONTO EQUO: sulla finestra COMUNE (2024-2026), TP01-solo vs TP01+XS J = pd.concat({"tp": tp_daily, "xs": d}, axis=1, join="inner").dropna() tpw, xsw = J["tp"], J["xs"] bw_f = metrics(tpw); bw_h = metrics(tpw[tpw.index >= HOLDOUT]) print(f"\n [finestra comune {J.index[0].date()}->{J.index[-1].date()}]") print(f" TP01 SOLO (su finestra comune): FULL Sh {bw_f['sharpe']:.2f} DD {bw_f['maxdd']*100:.1f}% | HOLD Sh {bw_h['sharpe']:.2f}") for w in (0.2, 0.3, 0.5): comb = (1 - w) * tpw + w * xsw cf = metrics(comb); ch = metrics(comb[comb.index >= HOLDOUT]) print(f" +XS w{w:.0%}: FULL {cf['sharpe']:.2f} ({cf['sharpe']-bw_f['sharpe']:+.2f}) DD {cf['maxdd']*100:.1f}%" f" | HOLD {ch['sharpe']:.2f} ({ch['sharpe']-bw_h['sharpe']:+.2f})") print("\n WINNER-diversifier se: corr bassa, e TP01+XS batte TP01-solo (FULL E HOLD) sulla finestra comune,") print(" con breadth per-anno ok. Altrimenti no (e attenzione: storia XS solo ~2.5 anni).") if __name__=="__main__": main()