Files
Adriano Dal Pastro 9612560479 research(xsec): sweep cross-sectional su Hyperliquid (43 script/257 config) + verifica avversariale
Nuova harness condivisa xslib.py (panel HL certificato, score per-asset causale, book
long-k/short-k vol-targeted leak-free) + 43 script in runs/ su 11 famiglie (MOM/REV/VOL/
DIST/LIQ/VAL/STRUCT/UNIV). Scoring = earns_slot (full>0 AND hold-out>0 AND marginal ADDS
al portafoglio live AND corr XS01<0.6, con jackknife drop-one-month).

Find: 42/257 config earns_slot=True, ma TUTTE con corr TP01 -0.2..-0.4 e PnL ~solo 2025.
Verify (verify_survivors.py, 3 scettici deterministici):
 - S1 redundancy: cluster low-vol = UNA scommessa (XV01=XU02=1.00, XV02/XV03 r 0.44-0.67);
   XM09/XL02/XS06b/XR02 distinti (corr media off-diag +0.20).
 - S2 short-beta: cluster low-vol carica 0.44-0.70 su short-market -> NON market-neutral,
   e' un tilt short-alt-beta di regime. XM09(0.08)/XR02(-0.21) NON short-beta.
 - S3 per-anno: cluster low-vol decade (XV01/XU02 2026 -0.09); XL02 morto (2025 -0.14,
   2026 -0.43); XM09 (0.82/0.50/0.74) e XR02 (0.84/0.40/2.68) positivi in tutti e 3 gli anni.

Esito: nessuna sleeve nuova. Cluster low-vol RIGETTATO (regime-bet), XL02 RIGETTATO (overfit).
2 LEAD genuini (XM09 trend-gated x-sec momentum, XR02 reversal vol-gated) -> forward-monitor,
non deployabili (panel 2.5y regime unico + STAT-MODE esecuzione). Portafoglio live invariato.

Incluso anche options_vrp_managed.py (A/B VRP01 hold-to-expiry vs gestione attiva del doc
credit-spread): la gestione attiva DISTRUGGE l'edge (combo FULL managed Sh -1.29 vs HtE +0.96,
il delta-exit taglia i vincenti) -> scartata, VRP01 resta hold-to-expiry.

Diari: 2026-06-20-xsec-strategies-sweep.md, 2026-06-20-vrp-active-management.md.
gitignore: data/paper_portfolio/ (stato runtime paper) + scripts/research/xsec/runs/out/ (output rigenerabile).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 21:36:57 +00:00

359 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""xslib — SHARED CROSS-SECTIONAL research harness over the certified Hyperliquid alt panel.
Built for the "cerca altre strategie" wave (2026-06-20, follow-up to the 104-hypothesis BTC/ETH
sweep that exhausted the single-asset directional space). The frontier the prior synthesis pointed
to: CROSS-SECTIONAL / multi-asset mechanisms on the 51 certified Hyperliquid alts (1d, 2024-2026),
where the ~1.3 BTC/ETH-directional ceiling does NOT bind, and DISTINCT from XS01 (plain x-sec momentum).
Why a new harness: the panel is N assets × ~900 days. A strategy = a per-asset SCORE computed
causally (data <= close[i]); the harness ranks it cross-sectionally each rebalance, goes long the
top-k / short the bottom-k (market-neutral) or long-only top-k, vol-targets, charges fee on turnover,
and — crucially — the weight decided at bar i is applied to the return of bar i+1, so look-ahead is
structurally impossible (same convention as src.portfolio xs_book / sleeves._xsec_returns).
A candidate only matters if it (a) is robust (positive FULL + hold-out 2025+ + jackknife), AND
(b) is DISTINCT from XS01 (low correlation), AND (c) ADDS to the live TP01+XS01+VRP01 portfolio.
CAVEAT baked in: the panel is ~2.5 years — every result is SUGGESTIVE, not robust like 6y BTC/ETH.
Quick start (agent script):
import sys; sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/xsec")
import xslib as xs, numpy as np
p = xs.load_panel("all") # or "majors", a list, or an int N (top-N liquidity)
score = xs.past_return(p.close, 30) # momentum: higher = long
rep = xs.study_xs("MOM30", lambda P: xs.past_return(P.close, 30), H=10, k=5)
print(xs.fmt(rep)); print("JSON:", xs.as_json(rep))
"""
from __future__ import annotations
import glob
import json
import sys
import warnings
from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
# panel research has many all-NaN edge windows (rolling beta/vol on first rows) -> benign
warnings.filterwarnings("ignore", category=RuntimeWarning)
_ROOT = Path(__file__).resolve().parents[3]
if str(_ROOT) not in sys.path:
sys.path.insert(0, str(_ROOT))
sys.path.insert(0, str(_ROOT / "scripts" / "research" / "alt"))
import altlib as al # noqa: E402 (reuse _sh, _dd_ret, _to_daily, HOLDOUT, metric helpers)
RAW = _ROOT / "data" / "raw"
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
FEE = 0.001 # round-trip; charged /2 per side on turnover
MAJORS = ["BTC", "ETH", "SOL", "BNB", "XRP", "DOGE", "AVAX", "LINK", "LTC", "ADA",
"ARB", "OP", "SUI", "APT", "INJ", "TIA", "SEI", "NEAR", "AAVE"]
# ===========================================================================
# PANEL
# ===========================================================================
@dataclass
class Panel:
syms: list
index: pd.DatetimeIndex
close: np.ndarray
open: np.ndarray
high: np.ndarray
low: np.ndarray
vol: np.ndarray
ret: np.ndarray # daily simple returns, ret[0]=0
@lru_cache(maxsize=16)
def load_panel(universe="all", min_rows: int = 700) -> Panel:
"""Common-date OHLCV panel of the certified HL alts (1d). `universe`:
'all' -> every alt with >= min_rows of history (drops short ones e.g. ALGO/SAND),
'majors' -> the 19 XS01 majors, a list of symbols, or an int N (top-N by median $-volume)."""
close, vol, high, low, opn = {}, {}, {}, {}, {}
for f in sorted(glob.glob(str(RAW / "hl_*_1d.parquet"))):
sym = Path(f).stem.replace("hl_", "").replace("_1d", "").upper()
d = pd.read_parquet(f)
if len(d) < min_rows:
continue
idx = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
close[sym] = pd.Series(d["close"].values.astype(float), index=idx)
vol[sym] = pd.Series(d["volume"].values.astype(float), index=idx)
high[sym] = pd.Series(d["high"].values.astype(float), index=idx)
low[sym] = pd.Series(d["low"].values.astype(float), index=idx)
opn[sym] = pd.Series(d["open"].values.astype(float), index=idx)
C = pd.concat(close, axis=1, join="inner").sort_index().dropna()
syms = list(C.columns)
if universe == "majors":
syms = [s for s in MAJORS if s in syms]
elif isinstance(universe, (list, tuple)):
syms = [s for s in universe if s in syms]
elif isinstance(universe, int):
dollar = {s: float(np.nanmedian(C[s].values * pd.concat(vol, axis=1)[s].reindex(C.index).values))
for s in syms}
syms = sorted(syms, key=lambda s: -dollar[s])[:universe]
C = C[syms]
idx = C.index
def stack(dd):
return pd.concat(dd, axis=1).reindex(index=idx)[syms].values.astype(float)
cl = C.values
ret = np.zeros_like(cl)
ret[1:] = cl[1:] / cl[:-1] - 1.0
return Panel(syms, idx, cl, stack(opn), stack(high), stack(low), stack(vol), ret)
# ===========================================================================
# CAUSAL CROSS-SECTIONAL HELPERS (value at row i uses data <= i)
# ===========================================================================
def past_return(close, L):
out = np.full_like(close, np.nan)
out[L:] = close[L:] / close[:-L] - 1.0
return out
def roll_std(mat, win):
return pd.DataFrame(mat).rolling(win, min_periods=max(2, win // 2)).std().values
def roll_mean(mat, win):
return pd.DataFrame(mat).rolling(win, min_periods=max(2, win // 2)).mean().values
def roll_skew(mat, win):
return pd.DataFrame(mat).rolling(win, min_periods=max(3, win // 2)).skew().values
def ewm_mean(mat, span):
return pd.DataFrame(mat).ewm(span=span, adjust=False).mean().values
def xs_zscore(mat):
"""Cross-sectional z-score per row (across assets). NaN-safe."""
m = np.nanmean(mat, axis=1, keepdims=True)
s = np.nanstd(mat, axis=1, keepdims=True)
return (mat - m) / np.where(s > 0, s, np.nan)
def xs_rank(mat):
"""Cross-sectional rank in [0,1] per row (0=lowest)."""
out = np.full_like(mat, np.nan, dtype=float)
for i in range(mat.shape[0]):
row = mat[i]
ok = np.isfinite(row)
if ok.sum() >= 2:
r = pd.Series(row[ok]).rank().values
out[i, ok] = (r - 1) / (ok.sum() - 1)
return out
def market_ret(ret):
"""Equal-weight market return per day (n,)."""
return np.nanmean(ret, axis=1)
def roll_beta(ret, win):
"""Rolling beta of each asset to the equal-weight market (n,A), causal."""
mkt = market_ret(ret)
ms = pd.Series(mkt)
var = ms.rolling(win, min_periods=max(5, win // 2)).var()
out = np.full_like(ret, np.nan)
for a in range(ret.shape[1]):
cov = pd.Series(ret[:, a]).rolling(win, min_periods=max(5, win // 2)).cov(ms)
out[:, a] = (cov / var.replace(0, np.nan)).values
return out
def residual_return(ret, win):
"""Idiosyncratic daily return = ret - beta*market (beta rolling, causal)."""
beta = roll_beta(ret, win)
mkt = market_ret(ret)[:, None]
return ret - beta * mkt
def volume_z(vol, win):
m = roll_mean(vol, win)
s = roll_std(vol, win)
return (vol - m) / np.where(s > 0, s, np.nan)
# ===========================================================================
# BACKTEST — generic cross-sectional book from a per-asset SCORE matrix.
# score[i] (data <= i) -> rank assets -> long top-k / short bottom-k; W[i] earns dret[i+1].
# ===========================================================================
def xs_backtest(panel: Panel, score, H=10, k=5, long_short=True, target_vol=0.20,
fee=FEE, vt_cap=3.0):
px = panel.close
n, A = px.shape
dret = panel.ret
score = np.asarray(score, float)
if score.shape != (n, A):
raise ValueError(f"score shape {score.shape} != panel {(n, A)}")
W = np.zeros((n, A))
w = np.zeros(A)
for i in range(n):
if i % H == 0:
row = score[i]
fin = np.isfinite(row)
if fin.sum() >= 2 * k:
ranked = np.where(fin, row, -np.inf)
order = np.argsort(ranked)
order = order[np.isfinite(ranked[order])]
lo, hi = order[:k], order[-k:]
w = np.zeros(A)
if long_short:
w[hi] = 0.5 / k
w[lo] = -0.5 / k
else:
w[hi] = 1.0 / 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 * (fee / 2.0)
s = pd.Series(net, index=panel.index)
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, vt_cap)
return pd.Series(s.values * scale, index=panel.index)
# ===========================================================================
# BASELINES (live stack) + MARGINAL scoring
# ===========================================================================
@lru_cache(maxsize=1)
def baselines():
"""Daily returns of the LIVE stack: TP01, XS01, and the combined active portfolio."""
from src.portfolio.portfolio import StrategyPortfolio, to_daily
from src.portfolio.sleeves import _tp01_returns, _xsec_returns, active_sleeves
tp = to_daily(_tp01_returns())
xs01 = to_daily(_xsec_returns())
active = StrategyPortfolio(active_sleeves()).combined_daily()
return dict(tp01=tp, xs01=xs01, active=active)
def _corr(a, b):
J = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return round(float(J["a"].corr(J["b"])), 3) if len(J) > 5 else None
def marginal_vs(cand, base, weights=(0.2, 0.35)):
"""Does `cand` improve `base`? blend uplift (full & hold-out), + OOS jackknife robustness."""
J = pd.concat({"B": base, "C": cand}, axis=1, join="inner").dropna()
if len(J) < 30:
return dict(verdict="N/A", reason="overlap < 30d")
JH = J[J.index >= HOLDOUT]
has_h = len(JH) > 20
out = dict(corr=_corr(J["B"], J["C"]), base_full=round(al._sh(J["B"]), 3),
base_hold=round(al._sh(JH["B"]), 3) if has_h else None,
cand_full=round(al._sh(J["C"]), 3), cand_hold=round(al._sh(JH["C"]), 3) if has_h else None,
blends={})
for w in weights:
bf, bh = (1 - w) * J["B"] + w * J["C"], (1 - w) * JH["B"] + w * JH["C"]
out["blends"][f"w{int(w * 100)}"] = dict(
uplift_full=round(al._sh(bf) - al._sh(J["B"]), 3),
uplift_hold=round(al._sh(bh) - al._sh(JH["B"]), 3) if has_h else None,
dd=round(al._dd_ret(bf), 4))
# OOS jackknife at w=0.2
robust = False
cu = jk = None
if has_h:
def _u(sub):
return al._sh(0.8 * sub["B"] + 0.2 * sub["C"]) - al._sh(sub["B"])
months = sorted(set(zip(JH.index.year, JH.index.month)))
cu = round(_u(JH), 3)
jk = round(min(_u(JH[~((JH.index.year == y) & (JH.index.month == m))]) for y, m in months), 3) \
if len(months) > 1 else cu
robust = bool(cu > 0.02 and jk > 0.0)
out["holdout_uplift_w20"] = cu
out["jackknife_min_uplift"] = jk
out["robust_oos"] = robust
up = out["blends"][f"w{int(weights[0] * 100)}"]["uplift_hold"]
cc = out["corr"] if out["corr"] is not None else 0.0
if cc is not None and cc > 0.85 and (up is None or abs(up) < 0.05):
out["verdict"] = "REDUNDANT"
elif up is not None and up >= 0.05 and robust:
out["verdict"] = "ADDS"
elif up is not None and up <= -0.05:
out["verdict"] = "DILUTES"
else:
out["verdict"] = "NEUTRAL"
return out
# ===========================================================================
# DRIVER
# ===========================================================================
def study_xs(name, score_fn, universe="all", H=10, k=5, long_short=True,
target_vol=0.20, min_rows=700) -> dict:
"""Backtest one cross-sectional hypothesis and score it honestly:
FULL + hold-out 2025+ + yearly, correlation to TP01 & XS01 (distinctness),
and marginal contribution to the LIVE active portfolio. `score_fn(panel) -> (n,A)`
per-asset score (higher = long), computed CAUSALLY (data <= close[i])."""
p = load_panel(universe, min_rows=min_rows)
score = score_fn(p)
daily = al._to_daily(xs_backtest(p, score, H=H, k=k, long_short=long_short, target_vol=target_vol))
net = daily.values
idx = daily.index
full = al._metrics_from_net(net, idx)
hmask = idx >= HOLDOUT
hold = al._metrics_from_net(net[hmask], idx[hmask]) if hmask.sum() > 20 else dict(sharpe=0.0, n=int(hmask.sum()))
bl = baselines()
marg = marginal_vs(daily, bl["active"])
earns_slot = (full["sharpe"] > 0 and hold.get("sharpe", 0) > 0
and marg.get("verdict") == "ADDS"
and (_corr(daily, bl["xs01"]) or 0) < 0.6) # distinct from existing x-sec
return dict(
name=name, universe=str(universe), H=H, k=k, long_short=long_short,
n_assets=len(p.syms), n_days=int(len(idx)),
full=full, holdout=hold, yearly=al._yearly(net, idx),
corr_tp01=_corr(daily, bl["tp01"]), corr_xs01=_corr(daily, bl["xs01"]),
corr_active=_corr(daily, bl["active"]),
marginal=marg, earns_slot=earns_slot,
caveat="panel ~2.5y (2024-26): suggestive, not robust",
)
def _clean(o):
if isinstance(o, dict):
return {k: _clean(v) for k, v in o.items()}
if isinstance(o, (list, tuple)):
return [_clean(x) for x in o]
if isinstance(o, (np.floating,)):
return round(float(o), 4)
if isinstance(o, (np.integer,)):
return int(o)
if isinstance(o, (np.bool_,)):
return bool(o)
return o
def as_json(rep):
return json.dumps(_clean(rep), default=str)
def fmt(rep):
m = rep["marginal"]
yr = " ".join(f"{y}:{d['ret'] * 100:+.0f}%" for y, d in rep["yearly"].items())
return (f"=== {rep['name']} [{rep['universe']} H{rep['H']} k{rep['k']} "
f"{'LS' if rep['long_short'] else 'LO'}] EARNS_SLOT={rep['earns_slot']}\n"
f" FULL Sh {rep['full']['sharpe']:+.2f} DD {rep['full']['maxdd'] * 100:.0f}% "
f"ret {rep['full']['ret'] * 100:+.0f}% | HOLD Sh {rep['holdout'].get('sharpe', 0):+.2f} "
f"| corr TP01 {rep['corr_tp01']} XS01 {rep['corr_xs01']}\n"
f" marginal vs active: {m.get('verdict')} (corr {m.get('corr')}, "
f"holdUplift_w20 {m.get('holdout_uplift_w20')}, jackknife {m.get('jackknife_min_uplift')}, "
f"robust_oos {m.get('robust_oos')}) | {yr}")
if __name__ == "__main__":
print("--- SMOKE TEST xslib ---")
# 1) x-sec momentum (should resemble XS01 ballpark) ; 2) short-term reversal ; 3) low-vol
print(fmt(study_xs("MOM30-90", lambda P: xs_zscore(past_return(P.close, 30)) + xs_zscore(past_return(P.close, 90)), H=10, k=5)))
print(fmt(study_xs("REV5", lambda P: -past_return(P.close, 5), H=5, k=5)))
print(fmt(study_xs("LOWVOL", lambda P: -roll_std(P.ret, 30), H=10, k=5)))
print("\nJSON sample:", as_json(study_xs("MOM30", lambda P: past_return(P.close, 30)))[:240])