Files
PythagorasGoal/scripts/research/r0724_hlp_deepdive.py
T
Adriano Dal Pastro 00996640fb research(cases): 3 agenti live sui 3 case — PM<->Deribit SKIP (gap=spec non alpha), HLP SKIP/WATCH (carry ex-evento 0.2%/a), 0DTE capture avviata
PM: 34 binarie riconciliate vs daily options; il gap 11pp si decompone in basis USDT
(+4-5pp ATM), tempo 8h (-+5pp) e replica su book morti; residuo tail 2-3pp sotto costi;
trade coperto reale: lock /bin/bash.1-5, P(conflitto gambe) 2-22%, margine SM domina -> SKIP.
HLP: serie daily trovata (wHLP): Sharpe 0.69 non ~2, +12% 2026 = 1 evento, ex-evento
+0.2%/a, coda JELLY = inventario ereditato troncato da voto discrezionale, Kelly f*=0
-> SKIP/WATCH con trigger meccanici. 0DTE: fee cap binding = drag 2-3x weekly, IV<RV
al front stanotte; snapshot pipeline scritta e primo capture su disco; decisione
pre-registrata a 90g di serie. Book INVARIATO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 22:46:59 +00:00

292 lines
14 KiB
Python

"""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())