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:
@@ -17,8 +17,8 @@ from pathlib import Path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np
|
||||
from src.data.downloader import load_data
|
||||
from scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, VAL_START, HOLDOUT_START
|
||||
import pandas as pd
|
||||
from scripts.analysis.research_lab import backtest, buy_hold, mc_pvalue, load_tf, ts, _net_series, VAL_START, HOLDOUT_START
|
||||
|
||||
|
||||
def load_signal(path):
|
||||
@@ -58,7 +58,7 @@ def main():
|
||||
res = {"asset": asset, "tf": tf, "sigfile": sigfile}
|
||||
try:
|
||||
signal = load_signal(sigfile)
|
||||
df = load_data(asset, tf)
|
||||
df = load_tf(asset, tf)
|
||||
pos = np.asarray(signal(df, asset, tf), float)
|
||||
res["n"] = int(len(df))
|
||||
res["len_ok"] = bool(len(pos) == len(df))
|
||||
@@ -81,6 +81,19 @@ def main():
|
||||
null_p=round(p, 4),
|
||||
beats_bh=bool(full.sharpe > bh.sharpe and oos.sharpe > 0),
|
||||
)
|
||||
# breadth per-anno (pre-hold-out): % anni positivi, anni rossi consecutivi
|
||||
net, _, _, _ = _net_series(df, pos)
|
||||
s = pd.Series(net, index=ts(df))
|
||||
s = s[s.index < pd.Timestamp(HOLDOUT_START, tz="UTC")]
|
||||
yr = {int(y): float((1 + g).prod() - 1) for y, g in s.groupby(s.index.year)}
|
||||
vals = list(yr.values())
|
||||
max_consec_red = 0; cur = 0
|
||||
for v in vals:
|
||||
cur = cur + 1 if v < 0 else 0
|
||||
max_consec_red = max(max_consec_red, cur)
|
||||
res["per_year_preho"] = {y: round(v, 3) for y, v in yr.items()}
|
||||
res["pct_years_pos"] = round(sum(v > 0 for v in vals) / len(vals), 2) if vals else 0.0
|
||||
res["max_consec_red_years"] = int(max_consec_red)
|
||||
if holdout:
|
||||
ho = backtest(df, pos, tf, lo=HOLDOUT_START)
|
||||
res["holdout_sharpe"] = round(ho.sharpe, 3)
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""FETCH + CERTIFY universo Hyperliquid (Cerbero MCP MAINNET) — espansione cross-sectional.
|
||||
|
||||
Hyperliquid (via cerbero-mcp mainnet) offre ~230 perp liquidi, ma storia nativa REALE solo dal
|
||||
2024 (pre-2024 = backfill, volume 0). Qui scarico un set liquido a 1d (2024+), e CERTIFICO ogni
|
||||
asset come BTC/ETH: cross-venue vs Binance (realismo) + flat-bar (liquidita'). Scrivo SOLO i puliti
|
||||
in data/raw/hl_<sym>_1d.parquet (namespace dedicato, NON mischiato col Deribit BTC/ETH).
|
||||
|
||||
Disciplina: Cerbero ci ha gia' bruciato (testnet) -> niente fiducia, solo certificazione.
|
||||
|
||||
uv run python scripts/analysis/fetch_hyperliquid.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys, time
|
||||
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, requests, ccxt
|
||||
|
||||
RAW = PROJECT_ROOT / "data" / "raw"
|
||||
START = "2024-01-01"; END = "2026-06-17"
|
||||
# set liquido (volume recente alto + storia 2024); MATIC morto, HYPE 2025-only esclusi qui
|
||||
SYMS = ["BTC","ETH","SOL","BNB","XRP","DOGE","AVAX","LINK","LTC","ADA","ARB","OP","SUI","APT","INJ","TIA","SEI","NEAR","AAVE"]
|
||||
BINANCE = {"BTC":"BTC/USDT","ETH":"ETH/USDT","SOL":"SOL/USDT","BNB":"BNB/USDT","XRP":"XRP/USDT",
|
||||
"DOGE":"DOGE/USDT","AVAX":"AVAX/USDT","LINK":"LINK/USDT","LTC":"LTC/USDT","ADA":"ADA/USDT",
|
||||
"ARB":"ARB/USDT","OP":"OP/USDT","SUI":"SUI/USDT","APT":"APT/USDT","INJ":"INJ/USDT",
|
||||
"TIA":"TIA/USDT","SEI":"SEI/USDT","NEAR":"NEAR/USDT","AAVE":"AAVE/USDT"}
|
||||
|
||||
|
||||
def _h():
|
||||
env={}
|
||||
for ln in open(PROJECT_ROOT/".env.mainnet"):
|
||||
ln=ln.strip()
|
||||
if ln and not ln.startswith("#") and "=" in ln: k,v=ln.split("=",1); env[k]=v.strip()
|
||||
return {"Authorization":f"Bearer {env['CERBERO_TOKEN']}","X-Bot-Tag":env.get('CERBERO_BOT_TAG','fetch'),"Content-Type":"application/json"}
|
||||
|
||||
|
||||
def fetch_hl(sym, H, interval="1d"):
|
||||
r=requests.post("https://cerbero-mcp.tielogic.xyz/mcp/tools/get_historical",
|
||||
headers=H, json={"exchange":"hyperliquid","instrument":sym,"interval":interval,
|
||||
"start_date":START,"end_date":END}, timeout=60)
|
||||
c=r.json().get("candles",[])
|
||||
if not c: return pd.DataFrame()
|
||||
df=pd.DataFrame(c)[["timestamp","open","high","low","close","volume"]]
|
||||
return df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
|
||||
|
||||
|
||||
def binance_daily(sym_b, start_ms, end_ms):
|
||||
ex=ccxt.binance({"enableRateLimit":True})
|
||||
out={}; since=start_ms
|
||||
while since<=end_ms:
|
||||
try: r=ex.fetch_ohlcv(sym_b,"1d",since=since,limit=500)
|
||||
except Exception: break
|
||||
r=[x for x in r if x[0]>=since]
|
||||
if not r: break
|
||||
for x in r:
|
||||
if start_ms<=x[0]<=end_ms and x[4]: out[int(x[0])]=float(x[4])
|
||||
nxt=int(r[-1][0])+86400000
|
||||
if nxt<=since: break
|
||||
since=nxt
|
||||
return pd.Series(out)
|
||||
|
||||
|
||||
def main():
|
||||
H=_h()
|
||||
print("="*92); print(" FETCH + CERTIFY Hyperliquid 1d (Cerbero mainnet) — cross-venue vs Binance + liquidita'"); print("="*92)
|
||||
print(f" {'sym':<6}{'barre':>7}{'start':>12}{'flat%':>7}{'med_bps':>9}{'>1%':>7}{'verdetto':>12}")
|
||||
certified=[]
|
||||
for s in SYMS:
|
||||
df=fetch_hl(s,H)
|
||||
if df.empty: print(f" {s:<6} vuoto"); continue
|
||||
ts=pd.to_datetime(df["timestamp"],unit="ms",utc=True)
|
||||
flat=((df.open==df.high)&(df.high==df.low)&(df.low==df.close)).mean()*100
|
||||
# cross-venue vs Binance USDT (daily close)
|
||||
ref=binance_daily(BINANCE[s], int(df["timestamp"].iloc[0]), int(df["timestamp"].iloc[-1]))
|
||||
a=df.set_index("timestamp")["close"]
|
||||
m=pd.concat([a.rename("a"),ref.rename("b")],axis=1,join="inner").dropna()
|
||||
if len(m)>5:
|
||||
bps=(m["a"]-m["b"]).abs()/m["b"]*1e4
|
||||
med=bps.median(); g1=(bps>100).mean()*100
|
||||
else: med=g1=float("nan")
|
||||
clean = (not np.isnan(med)) and med<60 and g1<3 and flat<5
|
||||
v = "PULITO" if clean else "scarta"
|
||||
print(f" {s:<6}{len(df):>7}{str(ts.iloc[0].date()):>12}{flat:>6.1f}%{med:>9.1f}{g1:>6.1f}%{v:>12}")
|
||||
if clean:
|
||||
df.to_parquet(RAW/f"hl_{s.lower()}_1d.parquet", index=False); certified.append(s)
|
||||
print(f"\n CERTIFICATI ({len(certified)}): {certified}")
|
||||
print(" Scritti in data/raw/hl_<sym>_1d.parquet (namespace dedicato). Universo per cross-sectional.")
|
||||
|
||||
|
||||
if __name__=="__main__":
|
||||
main()
|
||||
@@ -29,7 +29,23 @@ import pandas as pd
|
||||
from src.data.downloader import load_data
|
||||
|
||||
FEE_RT = 0.001 # 0.10% round-trip taker Deribit (0.05%/lato)
|
||||
BARS_PER_YEAR = {"5m": 105192.0, "15m": 35064.0, "1h": 8766.0}
|
||||
BARS_PER_YEAR = {"5m": 105192.0, "15m": 35064.0, "1h": 8766.0,
|
||||
"4h": 2191.5, "12h": 730.5, "1d": 365.25}
|
||||
|
||||
|
||||
def load_tf(asset: str, tf: str):
|
||||
"""Carica un TF certificato. 5m/15m/1h diretti; 4h/12h/1d DERIVATI per resample dal 1h
|
||||
(confini 00:00 UTC). >=12h e' il regime raccomandato (sotto, costi+overfit dominano)."""
|
||||
if tf in ("5m", "15m", "1h"):
|
||||
return load_data(asset, tf)
|
||||
rule = {"4h": "4h", "12h": "12h", "1d": "1D"}[tf]
|
||||
df = load_data(asset, "1h").copy()
|
||||
df.index = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
out = df.resample(rule, label="left", closed="left").agg(
|
||||
{"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}).dropna(subset=["open"])
|
||||
epoch = pd.Timestamp("1970-01-01", tz="UTC")
|
||||
out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64")
|
||||
return out.reset_index(drop=True)[["timestamp", "open", "high", "low", "close", "volume"]]
|
||||
# Hold-out FINALE bloccato: NIENTE ricerca/tuning lo tocca finché non è il verdetto (Fase 3).
|
||||
HOLDOUT_START = "2025-01-01"
|
||||
# Finestra di validazione OOS usata in ricerca (out-of-sample ma PRE hold-out).
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
"""GIUDICE DEI CONTENDER — valuta un segnale candidato a livello PORTAFOGLIO vs TP01.
|
||||
|
||||
Per ogni (tf, sigfile): costruisce il BOOK 50/50 BTC+ETH del candidato (causale, netto fee),
|
||||
e applica il gauntlet STRETTO vs TP01:
|
||||
- standalone: FULL Sh/DD, HOLD-OUT 2025-26 Sh/ret/DD, breadth per-anno (% anni positivi, rossi
|
||||
consecutivi), correlazione a TP01;
|
||||
- contributo al portafoglio: TP01-solo vs TP01+candidato a pesi 0.2/0.3/0.5 (Δ FULL e Δ HOLD).
|
||||
VERDETTO WINNER se: (A) batte TP01 standalone (book FULL Sh>1.30, hold-out Sh>~0.25, breadth ok),
|
||||
OPPURE (B) diversificatore robusto (corr bassa, alza il portafoglio su FULL E hold-out, breadth ok).
|
||||
|
||||
uv run python scripts/portfolio/verify_contender.py 1d /tmp/beat_sig_0.py 12h /tmp/beat_sig_10.py ...
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from scripts.analysis.research_lab import load_tf, _net_series
|
||||
from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, HOLDOUT
|
||||
from src.portfolio.sleeves import tp01_sleeve
|
||||
|
||||
TP01_FULL_SH = 1.30
|
||||
TP01_HOLD_SH = 0.31
|
||||
|
||||
|
||||
def load_signal(path):
|
||||
spec = importlib.util.spec_from_file_location("csig_" + Path(path).stem, path)
|
||||
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
||||
return m.signal
|
||||
|
||||
|
||||
def book_perbar(signal, tf) -> pd.Series:
|
||||
s = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
df = load_tf(a, tf)
|
||||
net, _, _, _ = _net_series(df, np.asarray(signal(df, a, tf), float))
|
||||
s[a] = pd.Series(net, index=pd.to_datetime(df["timestamp"], unit="ms", utc=True))
|
||||
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
|
||||
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index)
|
||||
|
||||
|
||||
def breadth(daily):
|
||||
pre = daily[daily.index < HOLDOUT]
|
||||
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 (sum(v > 0 for v in yr) / len(yr) if yr else 0.0), mx, yr
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
pairs = [(args[i], args[i + 1]) for i in range(0, len(args) - 1, 2)]
|
||||
tp = tp01_sleeve(1.0)
|
||||
tp_daily = tp.daily()
|
||||
base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest()
|
||||
print("=" * 100)
|
||||
print(f" GIUDICE CONTENDER vs TP01 (book FULL Sh {base['full']['sharpe']:.2f} / HOLD {base['holdout']['sharpe']:.2f})")
|
||||
print("=" * 100)
|
||||
|
||||
winners = []
|
||||
for tf, sig in pairs:
|
||||
name = Path(sig).stem
|
||||
try:
|
||||
signal = load_signal(sig)
|
||||
pb = book_perbar(signal, tf)
|
||||
d = to_daily(pb)
|
||||
except Exception as e:
|
||||
print(f"\n {name} ({tf}): ERRORE {type(e).__name__}: {str(e)[:80]}"); continue
|
||||
f = metrics(d); h = 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) > 2 else float("nan")
|
||||
pct, consec, yr = breadth(d)
|
||||
print(f"\n {name} ({tf}) BOOK 50/50")
|
||||
print(f" standalone: FULL Sh {f['sharpe']:>5.2f} DD {f['maxdd']*100:>4.1f}% | HOLD Sh {h['sharpe']:>5.2f} ret {h['ret']*100:>+6.1f}% DD {h['maxdd']*100:>4.1f}%"
|
||||
f" | anni+ {pct*100:>3.0f}% rossi-consec {consec} | corr_TP01 {corr:+.2f} | turn n/a")
|
||||
# contributo al portafoglio
|
||||
contrib = []
|
||||
for w in (0.2, 0.3, 0.5):
|
||||
sl = Sleeve(name, w, lambda pb=pb: pb)
|
||||
bt = StrategyPortfolio([tp01_sleeve(1 - w), sl]).backtest()
|
||||
dF = bt["full"]["sharpe"] - base["full"]["sharpe"]
|
||||
dH = bt["holdout"]["sharpe"] - base["holdout"]["sharpe"]
|
||||
contrib.append((w, bt["full"]["sharpe"], dF, bt["holdout"]["sharpe"], dH))
|
||||
print(f" +TP01 w{w:.0%}: FULL {bt['full']['sharpe']:.2f} ({dF:+.2f}) | HOLD {bt['holdout']['sharpe']:.2f} ({dH:+.2f})")
|
||||
breadth_ok = pct >= 0.6 and consec <= 1
|
||||
standalone_beats = f["sharpe"] > TP01_FULL_SH and h["sharpe"] > 0.25 and breadth_ok
|
||||
# diversificatore: corr<0.5, migliora FULL E hold del portafoglio ad almeno un peso, breadth ok
|
||||
improves = any(dF > 0.05 and dH > 0.0 for _, _, dF, _, dH in contrib)
|
||||
diversifier = (not np.isnan(corr) and corr < 0.5) and improves and breadth_ok
|
||||
verdict = "WINNER-standalone" if standalone_beats else ("WINNER-diversifier" if diversifier else "no")
|
||||
print(f" -> {verdict} (breadth_ok={breadth_ok}, standalone_beats={standalone_beats}, diversifier={diversifier})")
|
||||
if verdict.startswith("WINNER"):
|
||||
winners.append((name, tf, verdict))
|
||||
|
||||
print("\n" + "=" * 100)
|
||||
print(f" WINNERS: {len(winners)}")
|
||||
for n, tf, v in winners:
|
||||
print(f" {n} ({tf}): {v}")
|
||||
if not winners:
|
||||
print(" nessuno batte TP01 con criterio onesto -> serve un'altra ondata.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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