"""r0724_hlp_deepdive — Deep-dive ALLOCATION sul vault HLP di Hyperliquid. Contesto (2026-07-24): il probe HLP (89-95 punti ~14g dall'API nativa, vedi `data/external/hlp_vault.json`) ha mostrato l'unico stream crash-long mai visto dal progetto (corr col book -0.19, +10.4% nel cascade ott-2025). Questo script risponde alle domande aperte con dati reali: 1. GRANULARITA' DAILY — l'API nativa (`vaultDetails`/`portfolio`) e' downsampled: allTime ~14g (95 pti), month ~10h ma SOLO ultimi 30g, week ~2.4h ultimi 7g → nessun daily storico ufficiale. Fonti esterne trovate: - CoinGecko `wrapped-hlp` (wHLP di Hyperbeat, redimibile a NAV): prezzo DAILY dal 2025-07-25 → oggi. E' un prezzo di mercato (puo' fare sconto sotto stress), ma e' l'unico mark giornaliero di lungo periodo liberamente accessibile. - DefiLlama `protocol/hyperliquid-hlp`: TVL DAILY dal 2024-12 (per il modello di diluizione return-vs-TVL). - Thunderhead (cloudfront d2v1fiwobg9w6): `hlp_liquidator_pnl` DAILY, ma solo 2025-03-05 → 2025-07-12 (stantio; copre pero' il 12-mar e JELLY 26-mar-2025) e `hlp_positions` (esposizione daily per coin dal 2023-06, per la leva). - MORTI: ASXN api-hyperliquid.asxn.xyz `/hlp_pnl` (daily completo, dietro Turnstile anti-bot); DefiLlama yields (HLP non ha token → non e' un pool); stats-data.hyperliquid.xyz espone solo `Mainnet/vaults` (stessi dati downsampled). 2. CODA — ricostruzione numerica 12-mar-2025 (whale ETH, -$4.28M realizzati in 1 giorno dal liquidator) e JELLY 26-mar-2025 (unrealized peak ~-$13.5M su TVL ~$240M ≈ -5.6%, salvato da delist+settlement dei validator a $0.0095 → +$703k realizzati). Vedi diario per il worst-case ragionato. 3-5. Fisco/accesso e allocation math: nel diario. Qui i numeri. Uso: uv run python scripts/research/r0724_hlp_deepdive.py # usa cache se c'e' uv run python scripts/research/r0724_hlp_deepdive.py --refresh # ri-scarica I fetch sono salvati in data/external/hlp_deepdive/ (gitignored di fatto: non committare i dati). Nessuna azione live: SOLO analisi. """ from __future__ import annotations import argparse import json import sys import urllib.request from datetime import datetime, timezone from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] EXT = ROOT / "data" / "external" CACHE = EXT / "hlp_deepdive" CACHE.mkdir(parents=True, exist_ok=True) VAULT = "0xdfc24b077bc1425ad1dea75bcb6f8158e10df303" SOURCES = { # prezzo daily wHLP (Hyperbeat Wrapped HLP) — mark di mercato del NAV HLP "whlp_coingecko.json": ( "https://api.coingecko.com/api/v3/coins/wrapped-hlp/market_chart" "?vs_currency=usd&days=365&interval=daily" ), # TVL daily del vault HLP "llama_hlp_tvl.json": "https://api.llama.fi/protocol/hyperliquid-hlp", # PnL daily del liquidator HLP (stantio: 2025-03-05 → 2025-07-12, copre JELLY) "thunderhead_liq_pnl.json": "https://d2v1fiwobg9w6.cloudfront.net/hlp_liquidator_pnl", } def fetch(name: str, url: str, refresh: bool) -> dict | list | None: p = CACHE / name if p.exists() and not refresh: return json.loads(p.read_text()) try: req = urllib.request.Request(url, headers={"User-Agent": "pythagoras-research/1.0"}) raw = urllib.request.urlopen(req, timeout=60).read() p.write_bytes(raw) return json.loads(raw) except Exception as e: # noqa: BLE001 print(f" [WARN] fetch {name} fallito ({e}); uso cache se esiste") return json.loads(p.read_text()) if p.exists() else None def refresh_vault_details(refresh: bool) -> dict: """Snapshot vaultDetails dall'API nativa (stesso formato del probe originale).""" p = EXT / "hlp_vault.json" if p.exists() and not refresh: return json.loads(p.read_text()) body = json.dumps({"type": "vaultDetails", "vaultAddress": VAULT}).encode() req = urllib.request.Request( "https://api.hyperliquid.xyz/info", data=body, headers={"Content-Type": "application/json"}, ) raw = urllib.request.urlopen(req, timeout=60).read() p.write_bytes(raw) return json.loads(raw) def series_from_period(d: dict, period: str) -> pd.DataFrame: for per, pdata in d["portfolio"]: if per == period: av = pd.DataFrame(pdata["accountValueHistory"], columns=["ts", "av"]) pnl = pd.DataFrame(pdata["pnlHistory"], columns=["ts", "pnl"]) df = av.merge(pnl, on="ts") df["t"] = pd.to_datetime(df["ts"], unit="ms", utc=True) df["av"] = df["av"].astype(float) df["pnl"] = df["pnl"].astype(float) # PnL CUMULATIVO return df.set_index("t")[["av", "pnl"]] raise KeyError(period) def ret_stats(r: pd.Series, periods_per_year: float, label: str) -> dict: r = r.replace([np.inf, -np.inf], np.nan).dropna() mu, sd = r.mean(), r.std() sharpe = mu / sd * np.sqrt(periods_per_year) if sd > 0 else np.nan eq = (1 + r).cumprod() dd = (eq / eq.cummax() - 1).min() ann = (1 + mu) ** periods_per_year - 1 out = dict(label=label, n=len(r), ann_ret=ann, sharpe=sharpe, maxdd=dd, worst=r.min(), worst_t=str(r.idxmin())[:10], best=r.max()) print(f" {label:28s} n={out['n']:4d} ann~{ann*100:6.1f}% Sh={sharpe:5.2f} " f"maxDD={dd*100:5.1f}% worst={r.min()*100:6.2f}% ({out['worst_t']})") return out def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--refresh", action="store_true") args = ap.parse_args() print("=" * 78) print("HLP DEEP-DIVE — dati al", datetime.now(timezone.utc).isoformat()[:16]) print("=" * 78) d = refresh_vault_details(args.refresh) print(f"\nVault: {d['name']} APR corrente dichiarato: {d['apr']*100:.3f}%") print(f"isClosed={d['isClosed']} allowDeposits={d['allowDeposits']} " f"maxDistributable=${d['maxDistributable']/1e6:.0f}M") # ---------------- A. serie nativa 14g (allTime) ---------------- print("\n[A] Serie NATIVA allTime (~14g/punto) — return su AUM di inizio periodo") at = series_from_period(d, "allTime") at["dpnl"] = at["pnl"].diff() at["ret"] = at["dpnl"] / at["av"].shift(1) at.loc[at["av"].shift(1) < 5e6, "ret"] = np.nan # AUM<5M: return non significativo ret14 = at["ret"].iloc[1:] ret_stats(ret14, 365.25 / 14, "HLP 14g FULL (AUM>5M)") for y in (2023, 2024, 2025, 2026): sub = ret14[ret14.index.year == y] if len(sub) > 3: ret_stats(sub, 365.25 / 14, f" anno {y}") print(" Traiettoria 2026 (per periodo 14g):", " ".join(f"{x*100:+.1f}" for x in ret14[ret14.index.year == 2026].dropna())) worst5 = ret14.dropna().nsmallest(5) print(" 5 peggiori periodi 14g:", [(str(i.date()), f"{v*100:+.2f}%") for i, v in worst5.items()]) # month nativo (~10h granularita', ultimi 30g) mo = series_from_period(d, "month") mo_ret = mo["pnl"].diff() / mo["av"].shift(1) tot30 = (1 + mo_ret.dropna()).prod() - 1 print(f" Ultimi 30g (periodo 'month', {len(mo)} pti ~10h): tot {tot30*100:+.2f}%") # ---------------- B. wHLP daily (CoinGecko) ---------------- print("\n[B] wHLP (CoinGecko, prezzo DAILY di mercato ~ NAV; sconto possibile)") cg = fetch("whlp_coingecko.json", SOURCES["whlp_coingecko.json"], args.refresh) if cg and cg.get("prices"): pr = pd.DataFrame(cg["prices"], columns=["ts", "px"]) pr["t"] = pd.to_datetime(pr["ts"], unit="ms", utc=True).dt.normalize() pr = pr.drop_duplicates("t").set_index("t")["px"].astype(float) r1d = pr.pct_change() ret_stats(r1d, 365.25, "wHLP daily FULL (12 mesi)") for lbl, a, b in [ ("cascade 2025-10-09→13", "2025-10-09", "2025-10-13"), ("feb-2026 (liq event)", "2026-02-01", "2026-02-28"), ("ultimi 45g", str(pr.index[-1] - pd.Timedelta(days=45))[:10], None), ]: w = pr.loc[a:b] if b else pr.loc[a:] if len(w) > 1: print(f" {lbl:26s} {w.iloc[0]:.4f} → {w.iloc[-1]:.4f} " f"({(w.iloc[-1]/w.iloc[0]-1)*100:+.2f}%) min {w.min():.4f}") peak = pr.cummax() cur_dd = pr.iloc[-1] / peak.iloc[-1] - 1 print(f" Drawdown CORRENTE dal max ({pr.idxmax().date()} {pr.max():.4f}): " f"{cur_dd*100:+.2f}%") # ---------------- C. TVL daily (DefiLlama) → diluizione ---------------- print("\n[C] TVL daily (DefiLlama hyperliquid-hlp) + fit return-vs-TVL") ll = fetch("llama_hlp_tvl.json", SOURCES["llama_hlp_tvl.json"], args.refresh) if ll and ll.get("tvl"): tvl = pd.DataFrame(ll["tvl"]) tvl["t"] = pd.to_datetime(tvl["date"], unit="s", utc=True).dt.normalize() tvl = tvl.drop_duplicates("t").set_index("t")["totalLiquidityUSD"].astype(float) print(f" TVL {tvl.index[0].date()} ${tvl.iloc[0]/1e6:.0f}M → picco " f"{tvl.idxmax().date()} ${tvl.max()/1e6:.0f}M → oggi ${tvl.iloc[-1]/1e6:.0f}M " f"({(tvl.iloc[-1]/tvl.max()-1)*100:+.0f}% dal picco)") # fit sul nativo: PnL$ 14g vs AUM inizio periodo (tutta la storia) x = at["av"].shift(1).iloc[1:] / 1e6 y = at["dpnl"].iloc[1:] / 1e6 ok = x.notna() & y.notna() b1, b0 = np.polyfit(x[ok], y[ok], 1) corr_xy = np.corrcoef(x[ok], y[ok])[0, 1] print(f" Fit PnL$_14g = {b0:+.2f}M {b1:+.4f}·TVL(M) corr={corr_xy:+.2f} " f"(b1≈0 ⇒ PnL$ NON scala col TVL ⇒ return-on-AUM ∝ 1/TVL = diluizione)") for lo, hi in [(0, 150), (150, 300), (300, 450), (450, 700)]: m = (x >= lo) & (x < hi) if m.sum() >= 4: impl = (y[m].mean() / x[m].mean()) * (365.25 / 14) * 100 print(f" TVL {lo:3d}-{hi:3d}M: n={m.sum():3d} PnL medio " f"${y[m].mean()*1000:+7.0f}k/14g → ~{impl:+.1f}%/anno su AUM") # ---------------- D. liquidator daily (thunderhead) — la coda ---------------- print("\n[D] Liquidator PnL DAILY (thunderhead, 2025-03-05→2025-07-12 — copre JELLY)") th = fetch("thunderhead_liq_pnl.json", SOURCES["thunderhead_liq_pnl.json"], args.refresh) if th and th.get("chart_data"): liq = pd.DataFrame(th["chart_data"]).dropna() liq["t"] = pd.to_datetime(liq["time"], utc=True) liq = liq.set_index("t")["total_pnl"].astype(float) w = liq.sort_values() print(" 5 peggiori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.head(5).items()]) print(" 5 migliori giorni:", [(str(i.date()), f"{v/1e6:+.2f}M") for i, v in w.tail(5).items()]) for day, note in [("2025-03-12", "whale ETH 50x"), ("2025-03-26", "JELLY settle")]: if day in liq.index.strftime("%Y-%m-%d").tolist(): v = liq[liq.index.strftime("%Y-%m-%d") == day].iloc[0] print(f" {day} ({note}): {v/1e6:+.2f}M") # ---------------- F. decomposizioni oneste ---------------- print("\n[F] Decomposizioni oneste") # carry 2026 ex-evento: togli il singolo periodo +7% (evento liquidazione feb-2026) r26 = ret14[ret14.index.year == 2026].dropna() if len(r26): ev = r26.idxmax() ex = r26.drop(ev) cum_ex = (1 + ex).prod() - 1 ann_ex = (1 + cum_ex) ** (365.25 / (14 * len(ex))) - 1 print(f" 2026: evento {ev.date()} {r26.max()*100:+.1f}% | ex-evento cum " f"{cum_ex*100:+.2f}% su {len(ex)} periodi ≈ {ann_ex*100:+.1f}%/anno " f"→ IL CARRY E' MORTO, resta solo il crash-alpha") # TVL al giorno JELLY (per scalare il -13.5M unrealized) if ll and ll.get("tvl"): for day in ("2025-03-12", "2025-03-26", "2025-10-10", "2026-02-08"): ts = pd.Timestamp(day, tz="UTC") i = tvl.index.get_indexer([ts], method="nearest")[0] print(f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M " f"(-13.5M JELLY = {-13.5e6/tvl.iloc[i]*100:.1f}% MtM)" if day == "2025-03-26" else f" TVL {day}: ${tvl.iloc[i]/1e6:.0f}M") # sconto wrapper: wHLP mercato vs NAV nativo su finestra comune (da max wHLP a oggi) if cg and cg.get("prices"): w_from = pr.idxmax() nav_win = ret14[ret14.index >= w_from].dropna() nav_chg = (1 + nav_win).prod() - 1 whlp_chg = pr.iloc[-1] / pr.max() - 1 print(f" Da {w_from.date()}: wHLP mercato {whlp_chg*100:+.2f}% vs NAV nativo " f"{nav_chg*100:+.2f}% → sconto wrapper ≈ {(whlp_chg-nav_chg)*100:+.1f}pt " f"(costo di un exit-in-stress via wrapper)") # ---------------- E. allocation math (Kelly log-utility, outcome discreti) ---------------- print("\n[E] Allocation math — Kelly log-utility su outcome annui discreti") # Outcome annui dell'ALLOCAZIONE (non del bankroll): base 2 scenari di coda: # - evento socializzazione stile-JELLY-senza-salvataggio: -20% dell'allocazione # - morte protocollo/bridge (hack, insolvenza, socializzazione totale): -100% # mu base = run-rate onesto 2026 (decay in corso: usare 6%, non il 19% del 2025) scenarios = [ ("base mu=6%", [(0.06, 0.83), (-0.20, 0.15), (-1.00, 0.02)]), ("ottimista mu=12%", [(0.12, 0.87), (-0.20, 0.10), (-1.00, 0.03)]), ("pessimista mu=3%", [(0.03, 0.80), (-0.25, 0.17), (-1.00, 0.03)]), ] for name, outs in scenarios: mu_adj = sum(x * p for x, p in outs) fgrid = np.linspace(0.001, 0.999, 999) util = [sum(p * np.log(1 + f * x) for x, p in outs) for f in fgrid] f_star = float(fgrid[int(np.argmax(util))]) print(f" {name:20s} mu_adj={mu_adj:+.1%} Kelly pieno f*={f_star:.2f} " f"0.25·Kelly={0.25*f_star:.1%} del bankroll") f_use = 0.10 # ~0.25·Kelly dello scenario base, cap operativo print(f" → allocazione difendibile ~{f_use:.0%} del bankroll (VRP01 fu ~0.27 Kelly):") for cap in (600, 2000, 5000, 20000): a = f_use * cap print(f" capitale ${cap:>6,}: ~${a:,.0f} → atteso ~${a*0.06:,.0f}/anno; " f"coda -${a*0.20:,.0f} (JELLY-like) / -${a:,.0f} (protocollo)") print("\nFonti dati:") for k, v in SOURCES.items(): print(f" {k:28s} {v}") print(" hlp_vault.json https://api.hyperliquid.xyz/info " '{"type":"vaultDetails","vaultAddress":"' + VAULT + '"}') print(" MORTI: ASXN /api/hlp_pnl (Turnstile), DefiLlama yields (HLP non-token), " "stats-data.hyperliquid.xyz (downsampled)") if __name__ == "__main__": sys.exit(main())