research: universo top-liquidità DINAMICO per XS — anch'esso peggiore del fisso-19 (memecoin diluiscono)
xsec_dynuniverse.py: a ogni ribilancio top-N per dollar-volume 30g causale (ragged-aware), poi XS momentum. Esito: best dinamico top12 FULL 0.65/OOS0.54 (un anno neg) vs fisso-19 FULL 0.80/OOS1.20 (100% anni+). Contributo TP01+DYN 1.10/0.60 vs TP01+XS19 1.25/1.15. La classifica per volume ammette i MEMECOIN ad alto volume (WIF/ORDI/JUP) erratici -> diluiscono. Liquidità != qualità. Conclusione: ne' 52-all ne' top-liquidità dinamico battono i 19 major curati. XS01 resta sui 19. Portafoglio invariato TP01 70% + XS01 30% (FULL 1.41 / HOLD 1.15). 12 test ok. I 52 parquet restano per ricerca futura. Diario 2026-06-19-xsec-universe-expansion.md aggiornato. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,5 +35,28 @@ funziona fra i MAJOR liquidi, non sul long tail. Allargare l'universo NON è gra
|
||||
## Lezione
|
||||
"Più asset = più robusto" è FALSO per il cross-sectional momentum: il long tail di alt piccoli
|
||||
diluisce/inverte l'edge. La breadth utile è quella dei major liquidi (corr-strutturata), non il
|
||||
numero grezzo. Per rafforzare XS01 servirebbe semmai una selezione per LIQUIDITÀ/qualità, non più
|
||||
nomi. (Coerente con la disciplina: nessuna espansione senza che migliori il gauntlet.)
|
||||
numero grezzo.
|
||||
|
||||
## Tentativo 2: UNIVERSO TOP-LIQUIDITÀ DINAMICO (`xsec_dynuniverse.py`) — anch'esso PEGGIORE
|
||||
Provato a selezionare a ogni ribilancio i top-N per dollar-volume 30g (causale) dai 52, poi XS
|
||||
momentum fra quelli (adattivo, ragged-aware). Esito:
|
||||
|
||||
| Universo | FULL Sh | OOS25 | anni+ |
|
||||
|---|---|---|---|
|
||||
| top12 dinamico (L30H10k5) | 0.65 | 0.54 | 67% (2026 −4%) |
|
||||
| top15/20/25 dinamico | 0.14-0.38 | ≤0.30 | 33-67% |
|
||||
| **fisso-19 major (L30H10k5)** | **0.80** | **1.20** | **100%** |
|
||||
| fisso-19 major (L90H10k5) | 0.88 | 0.90 | 100% |
|
||||
Contributo: TP01+DYN 70/30 = FULL 1.10 / HOLD 0.60 vs **TP01+XS19 = FULL 1.25 / HOLD 1.15**.
|
||||
|
||||
**Perché fallisce:** la classifica per dollar-volume ammette comunque i MEMECOIN ad alto volume
|
||||
(WIF, ORDI, JUP, PEPE...) che hanno volumi enormi ma momentum erratico/mean-reverting →
|
||||
diluiscono. **Liquidità ≠ qualità** nelle crypto. I 19 major *curati* (established, corr-strutturati,
|
||||
non solo alto volume) restano il sweet spot.
|
||||
|
||||
## Conclusione
|
||||
Né più nomi (52) né top-liquidità dinamico migliorano XS01. **XS01 resta sui 19 major curati**
|
||||
(FULL 0.80 / OOS 1.20, 100% anni+). Portafoglio invariato: TP01 70% + XS01 30% (FULL 1.41/HOLD 1.15).
|
||||
Per rafforzarlo davvero servirebbe una curatela di QUALITÀ (established majors), che è già ciò che i
|
||||
19 sono. Coerente con la disciplina: nessuna espansione senza che migliori il gauntlet. I 52 parquet
|
||||
certificati restano per ricerca futura (es. trend multi-asset, dove il long tail non diluisce).
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"""XS cross-sectional con UNIVERSO TOP-LIQUIDITÀ DINAMICO (Hyperliquid 52 certificati).
|
||||
|
||||
Invece di 19 nomi fissi, a ogni ribilancio: seleziona i top-N per liquidità (dollar-volume 30g
|
||||
causale), poi fra quelli long i k più forti / short i k più deboli (momentum, market-neutral),
|
||||
vol-target. Idea: cross-section pulita e ADATTIVA (i token entrano quando maturano in liquidità),
|
||||
escludendo il long-tail rumoroso che diluiva il 52-all. Gestione ragged (asset a date diverse:
|
||||
si classifica solo fra i disponibili). Causale. Confronto vs fisso-19 + 52-all + contributo TP01.
|
||||
|
||||
uv run python scripts/portfolio/xsec_dynuniverse.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
|
||||
from src.portfolio.sleeves import tp01_sleeve, XS_UNIVERSE
|
||||
|
||||
RAW = PROJECT_ROOT / "data" / "raw"
|
||||
FEE = 0.001
|
||||
|
||||
|
||||
def load_close_vol():
|
||||
close, vol = {}, {}
|
||||
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)
|
||||
ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
|
||||
close[sym] = pd.Series(d["close"].values.astype(float), index=ix)
|
||||
vol[sym] = pd.Series(d["volume"].values.astype(float), index=ix)
|
||||
C = pd.concat(close, axis=1, join="outer").sort_index()
|
||||
V = pd.concat(vol, axis=1, join="outer").sort_index().reindex(C.index)
|
||||
return C, V
|
||||
|
||||
|
||||
def xs_dynamic(C, V, N=20, lb=60, hold=10, k=5, mode="mom", tv=0.20, fixed=None):
|
||||
"""fixed=lista simboli -> universo statico (ignora liquidità). Altrimenti top-N per liquidità."""
|
||||
cols = list(C.columns); A = len(cols)
|
||||
px = C.values; n = len(px)
|
||||
dret = np.full((n, A), 0.0); dret[1:] = np.where(np.isfinite(px[1:]) & np.isfinite(px[:-1]), px[1:] / px[:-1] - 1.0, 0.0)
|
||||
dvol = V.values * px
|
||||
liq = pd.DataFrame(dvol, index=C.index, columns=cols).rolling(30, min_periods=15).mean().shift(1).values
|
||||
fixed_mask = np.array([c in fixed for c in cols]) if fixed else None
|
||||
W = np.zeros((n, A)); w = np.zeros(A)
|
||||
for i in range(n):
|
||||
if i >= lb and i % hold == 0:
|
||||
retlb = np.where(np.isfinite(px[i]) & np.isfinite(px[i - lb]), px[i] / px[i - lb] - 1.0, np.nan)
|
||||
avail = np.isfinite(retlb) & np.isfinite(px[i])
|
||||
if fixed is not None:
|
||||
avail &= fixed_mask
|
||||
cand = np.where(avail)[0]
|
||||
else:
|
||||
avail &= np.isfinite(liq[i])
|
||||
idx = np.where(avail)[0]
|
||||
if len(idx) > N:
|
||||
cand = idx[np.argsort(liq[i][idx])[-N:]] # top-N per liquidità
|
||||
else:
|
||||
cand = idx
|
||||
w = np.zeros(A)
|
||||
ke = min(k, len(cand) // 2)
|
||||
if ke >= 1:
|
||||
order = cand[np.argsort(retlb[cand])]
|
||||
lo, hi = order[:ke], order[-ke:]
|
||||
if mode == "mom": w[hi] = 0.5 / ke; w[lo] = -0.5 / ke
|
||||
else: w[lo] = 0.5 / ke; w[hi] = -0.5 / ke
|
||||
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=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 to_daily(pd.Series(s.values * scale, index=C.index))
|
||||
|
||||
|
||||
def ev(d):
|
||||
f = metrics(d); o = 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
|
||||
return f, o, pct
|
||||
|
||||
|
||||
def main():
|
||||
C, V = load_close_vol()
|
||||
print("=" * 96)
|
||||
print(f" XS UNIVERSO TOP-LIQUIDITÀ DINAMICO — {len(C.columns)} asset certificati [{C.index[0].date()} -> {C.index[-1].date()}]")
|
||||
print("=" * 96)
|
||||
tp = tp01_sleeve().daily()
|
||||
|
||||
print("\n (1) SWEEP N (top-liquidità) x config (mom) — FULL Sh / OOS25 Sh / anni+ / corrTP")
|
||||
print(f" {'config':<28}{'FULL':>7}{'OOS25':>7}{'anni+':>7}{'corrTP':>8}")
|
||||
best = None
|
||||
for N in (12, 15, 20, 25):
|
||||
for lb, hold, k in [(30, 10, 5), (60, 10, 5), (90, 10, 5)]:
|
||||
d = xs_dynamic(C, V, N=N, lb=lb, hold=hold, k=k)
|
||||
f, o, pct = ev(d)
|
||||
corr = float(pd.concat({"a": tp, "b": d}, axis=1, join="inner").dropna().corr().iloc[0, 1])
|
||||
tag = f"top{N} L{lb}H{hold}k{k}"
|
||||
print(f" {tag:<28}{f['sharpe']:>7.2f}{o['sharpe']:>7.2f}{pct*100:>6.0f}%{corr:>+8.2f}")
|
||||
if (best is None or f['sharpe'] > best[1]['sharpe']) and corr < 0.4 and o['sharpe'] > 0:
|
||||
best = (tag, f, o, corr, d, (N, lb, hold, k))
|
||||
|
||||
print("\n (2) BASELINE di confronto (stessa finestra):")
|
||||
for name, kw in [("fisso-19 major (L30H10k5)", dict(lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))),
|
||||
("fisso-19 major (L90H10k5)", dict(lb=90, hold=10, k=5, fixed=set(XS_UNIVERSE))),
|
||||
("52-all (L60H10k5)", dict(lb=60, hold=10, k=5))]:
|
||||
d = xs_dynamic(C, V, **kw); f, o, pct = ev(d)
|
||||
print(f" {name:<28} FULL {f['sharpe']:.2f} OOS25 {o['sharpe']:.2f} anni+ {pct*100:.0f}%")
|
||||
|
||||
if best is None:
|
||||
print("\n Nessuna config dinamica scorrelata+positiva. Il top-liquidità non aiuta.")
|
||||
return
|
||||
tag, f, o, corr, d, cfg = best
|
||||
print(f"\n === MIGLIOR DINAMICO: {tag} | FULL {f['sharpe']:.2f} ret {f['ret']*100:+.0f}% DD {f['maxdd']*100:.0f}% | OOS25 {o['sharpe']:.2f} | corrTP {corr:+.2f} ===")
|
||||
per = [(int(y), round(float((1 + g).prod() - 1), 3)) for y, g in d.groupby(d.index.year)]
|
||||
print(f" per-anno: {per}")
|
||||
# contributo al portafoglio vs fisso-19 (XS01 attuale)
|
||||
xs19 = xs_dynamic(C, V, lb=30, hold=10, k=5, fixed=set(XS_UNIVERSE))
|
||||
J = pd.concat({"tp": tp, "dyn": d, "x19": xs19}, axis=1, join="inner").dropna()
|
||||
print(f"\n CONTRIBUTO (finestra comune {J.index[0].date()}->{J.index[-1].date()}):")
|
||||
for nm, col in [("TP01 solo", None), ("TP01+XS19 (attuale) 70/30", "x19"), ("TP01+DYN 70/30", "dyn")]:
|
||||
if col is None:
|
||||
comb = J["tp"]
|
||||
else:
|
||||
comb = 0.7 * J["tp"] + 0.3 * J[col]
|
||||
mf = metrics(comb); mh = metrics(comb[comb.index >= HOLDOUT])
|
||||
print(f" {nm:<28} FULL Sh {mf['sharpe']:.2f} DD {mf['maxdd']*100:.0f}% | HOLD Sh {mh['sharpe']:.2f}")
|
||||
print("\n -> DINAMICO meglio del fisso-19? guarda FULL/OOS + contributo. Sennò: fisso-19 resta.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user