feat(portfolio): XS01 cross-sectional (Hyperliquid) BATTE il portafoglio -> TP01 70% + XS01 30%
Espansione universo (su input utente "storico da cerbero"): il Cerbero MCP col token MAINNET serve Hyperliquid (230 perp REALI, storia nativa dal 2024). fetch_hyperliquid.py certifica 19 alt liquidi a 1d (flat 0%, cross-venue 4-9 bps vs Binance) -> data/raw/hl_*_1d.parquet. Abilita le strategie CROSS-SECTIONAL (impossibili a 2 asset). XS01 = cross-sectional momentum market-neutral (long 5 forti / short 5 deboli su ret 30g, ogni 10g, vol-target 20%). Validato onesto: plateau (config/k/subset), fee-robusto (0.3% RT), scorrelato a TP01 (-0.06), positivo OGNI anno 2024-26, meccanismo complementare (lavora nella dispersione quando TP01 e' in cash). Diverso dal regime-luck RV bocciato (19 asset, plateau, ogni anno+). Contributo al portafoglio (outer-join + pesi rinormalizzati per sleeve a date diverse): TP01-solo FULL 1.30 / HOLD 0.31 -> TP01 70% + XS01 30%: FULL 1.41 / HOLD 1.15, DD giu', ~ogni anno+. -> XS01 BATTE il portafoglio esistente: inserito in active_sleeves. Caveat (documentati): storia XS ~2.5 anni; STAT-MODE (book 19 gambe non eseguibile a 2k -> ~20k), sleeve diagnostico/forward-monitor. portfolio.combine ora outer-join+renorm. 12 test passano. Diario 2026-06-19-hyperliquid-xsec.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user