00996640fb
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>
292 lines
14 KiB
Python
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())
|