"""SLEEVE del portafoglio + REGISTRY degli sleeve attivi. Per AGGIUNGERE una strategia al portafoglio: 1. Validala col gauntlet onesto (scripts/analysis/research_lab.py + hold-out + cross-asset). 2. Scrivi una funzione `__returns() -> pd.Series` che ritorna i suoi rendimenti netti per-barra (datetime-indexed, CAUSALE, netto fee). Deve passare il guard di causalità. 3. Avvolgila in uno Sleeve(nome, peso, fn[, pos_fn]) e aggiungila a active_sleeves(). Niente sleeve non validati: il portafoglio è solo per edge che reggono il gauntlet. """ from __future__ import annotations import numpy as np import pandas as pd from src.data.downloader import load_data from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d, simple_returns from src.portfolio.portfolio import Sleeve ASSETS = ("BTC", "ETH") # ----------------------------- TP01 (PORT LF1d) ----------------------------- def _tp01_returns() -> pd.Series: """TP01: TSMOM vol-target long-flat, 50/50 BTC+ETH, a 1d (>=12h: vedi nota look-ahead nel modulo). Rendimenti netti per-barra del portafoglio (causale: posizione decisa a close[i-1], tenuta in i).""" tp = TrendPortfolio(**CANONICAL) series = {} for a in ASSETS: df = resample_1d(load_data(a, "1h")) r = simple_returns(df["close"].values.astype(float)) tgt = tp.target_series(df) held = np.zeros(len(tgt)); held[1:] = tgt[:-1] net = held * r - tp.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 series[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) J = pd.concat(series, axis=1, join="inner").fillna(0.0) return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) def _tp01_positions() -> dict: tp = TrendPortfolio(**CANONICAL) return {a: round(tp.current_target(resample_1d(load_data(a, "1h"))), 4) for a in ASSETS} def tp01_sleeve(weight: float = 1.0) -> Sleeve: return Sleeve("TP01_trend_1d", weight, _tp01_returns, pos_fn=_tp01_positions) # ----------------------------- XS01: Cross-Sectional Momentum (Hyperliquid) ----------------------------- # Universo certificato Hyperliquid (19 alt, 1d, dal 2024) in data/raw/hl_*_1d.parquet # (fetch+certify: scripts/analysis/fetch_hyperliquid.py). Market-neutral, scorrelato a TP01 (~-0.06). # CAVEAT ONESTI: storia corta (~2.5 anni, 2024-2026); STAT-MODE (book a 19 gambe market-neutral # non eseguibile a 2k, serve ~20k); l'edge e' nella DISPERSIONE cross-section (complementare al # trend di TP01: lavora quando TP01 e' in cash). Validato: scripts/portfolio/xsec_research.py. import glob as _glob from pathlib import Path as _Path # BLEND di lookback (2026-06-19): fonde 30g+90g del momentum cross-sectional (z-score per # lookback, mediato) come TP01 fonde gli orizzonti -> piu' robusto del singolo L=30: FULL Sh # 0.80->1.10, DD 21%->14%, corr a TP01 -0.06->-0.12, 100% anni+. Diario 2026-06-19-xsec-blend.md. XS_CFG = dict(lookbacks=(30, 90), H=10, k=5, mode="mom", target_vol=0.20) _HL_DIR = _Path(__file__).resolve().parents[2] / "data" / "raw" # UNIVERSO ESPLICITO = 19 ALT LIQUIDI MAJOR. NB (2026-06-19): allargare a 52 asset (incluso # small-cap WIF/JUP/ORDI/PYTH/TAO...) DILUISCE l'edge -> momentum cross-section NEGATIVO sui 52. # I major sono il sweet spot. NON usare glob-all (i parquet extra certificati servono ad altra # ricerca, non a XS01). Vedi diario 2026-06-19-xsec-universe-expansion.md. XS_UNIVERSE = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA", "ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"] def _xsec_returns() -> pd.Series: cols = {} for sym in XS_UNIVERSE: p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet" if not p.exists(): continue d = pd.read_parquet(p) cols[sym] = pd.Series(d["close"].values.astype(float), index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)) if len(cols) < 10: raise FileNotFoundError("universo Hyperliquid XS01 incompleto: gira scripts/analysis/fetch_hyperliquid.py") C = pd.concat(cols, axis=1, join="inner").sort_index().dropna() px = C.values; n, A = px.shape lookbacks, H, k, mode, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], XS_CFG["mode"], XS_CFG["target_vol"] dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0]) W = np.zeros((n, A)); w = np.zeros(A) for i in range(n): if i >= max(lookbacks) and i % H == 0: score = np.zeros(A); cnt = 0 # blend: media z-score cross-sectional per lookback for L in lookbacks: rL = px[i] / px[i - L] - 1.0 sd = rL.std() if sd > 0: score += (rL - rL.mean()) / sd; cnt += 1 if cnt: score /= cnt order = np.argsort(score) w = np.zeros(A); lo, hi = order[:k], order[-k:] if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k else: w[lo] = 0.5 / k; w[hi] = -0.5 / k W[i] = w gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1) turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1) net = gross - turn * (0.001 / 2.0) s = pd.Series(net, index=C.index) rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25) scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0) return pd.Series(s.values * scale, index=C.index) def xsec_sleeve(weight: float = 0.3) -> Sleeve: return Sleeve("XS01_xsec_hl", weight, _xsec_returns) # ----------------------------- REGISTRY ----------------------------- def active_sleeves() -> list[Sleeve]: """Sleeve ATTIVI nel portafoglio (pesi rinormalizzati; sleeve a date diverse si attivano quando parte la loro storia). Aggiungere qui SOLO strategie validate col gauntlet.""" return [ tp01_sleeve(weight=0.70), # trend difensivo, BTC/ETH, dal 2019 xsec_sleeve(weight=0.30), # cross-sectional momentum Hyperliquid, dal 2024 (scorrelato, stat-mode) ]