"""FETCH + CERTIFY funding rate Hyperliquid (API pubblica, tokenless) — per ricerca CARRY cross-sectional. CONTESTO (2026-06-22, onda "nuova ricerca mirata"). Le due grandi ondate (sweep 104-ipotesi + ortho relative-value) hanno esaurito gli angoli DIREZIONALI e RELATIVE-VALUE sul *prezzo* BTC/ETH. L'unico meccanismo con una fonte di ritorno DIVERSA non ancora testato su dati certi e' il CARRY da funding (incassare il cashflow perp, delta-neutral). Scan di fattibilita': * funding price-clock sul feed Deribit certificato -> gia' testato (agent_03 intraday) = FAIL. * funding carry su Deribit (dove eseguiamo) -> ccxt fetch_funding_rate_history = 0 righe (bloccato). * funding carry su Hyperliquid -> API pubblica /info {"type":"fundingHistory"} = DISPONIBILE, cadenza ORARIA, tokenless, serie native dal 2023-05-12. HL e' gia' l'universo certificato di XS01. DISCIPLINA (lezione v2.0.0): nessuna fiducia nel dato finche' non e' certificato. Qui certifichiamo: (1) cadenza ~1h coerente, (2) gap interni, (3) copertura (giorni nativi reali per coin), (4) plausibilita' magnitudine (|funding orario| tipico < ~0.06%/h = cap HL; outlier flaggati), (5) il funding e' un CASHFLOW, non un prezzo -> niente cross-venue OHLC; il sanity check e' che il funding medio sia ~positivo e piccolo (premio long-pays-short tipico dei perp crypto). Universo = i 19 major di XS01 (quelli che la strategia live userebbe). Output: data/raw/hlfund__1h.parquet (namespace dedicato 'hlfund', NON tocca hl__1d di XS01). """ import sys, time, datetime as dt from pathlib import Path import numpy as np, pandas as pd, requests ROOT = Path(__file__).resolve().parents[2] RAW = ROOT / "data" / "raw" RAW.mkdir(parents=True, exist_ok=True) # 19 major di XS01 (CLAUDE.md) UNIVERSE = ["BTC","ETH","SOL","BNB","XRP","DOGE","AVAX","LINK","LTC","ADA", "ARB","OP","SUI","APT","INJ","TIA","SEI","NEAR","AAVE"] HL_INFO = "https://api.hyperliquid.xyz/info" START = int(dt.datetime(2023, 1, 1, tzinfo=dt.timezone.utc).timestamp() * 1000) HOUR_MS = 3600 * 1000 def _post(payload, max_retry=6): """POST con backoff esponenziale su 429/5xx (l'API pubblica HL throttla).""" delay = 1.0 for attempt in range(max_retry): r = requests.post(HL_INFO, json=payload, timeout=30) if r.status_code == 429 or r.status_code >= 500: time.sleep(delay) delay = min(delay * 2, 20) # 1,2,4,8,16,20 continue r.raise_for_status() return r.json() r.raise_for_status() return r.json() def fetch_funding(coin: str) -> pd.DataFrame: """Pagina fundingHistory (max 500/req, orario) avanzando startTime fino a oggi.""" rows, start = [], START seen = set() while True: d = _post({"type": "fundingHistory", "coin": coin, "startTime": start}) if not d: break new = [x for x in d if x["time"] not in seen] for x in new: seen.add(x["time"]) rows.append((x["time"], float(x["fundingRate"]), float(x.get("premium", "nan")))) last = d[-1]["time"] if len(d) < 500: # ultima pagina break nxt = last + 1 if nxt <= start: # niente progresso -> stop break start = nxt time.sleep(0.35) # gentile con l'API pubblica if not rows: return pd.DataFrame(columns=["ts", "funding", "premium"]).set_index("ts") df = pd.DataFrame(rows, columns=["ts", "funding", "premium"]).drop_duplicates("ts").sort_values("ts") df["ts"] = pd.to_datetime(df["ts"], unit="ms", utc=True) return df.set_index("ts") def certify(coin: str, df: pd.DataFrame) -> dict: if df.empty: return {"coin": coin, "n": 0, "status": "VUOTO"} idx = df.index span_days = (idx[-1] - idx[0]).total_seconds() / 86400 # cadenza: differenze in ore deltas_h = np.diff(idx.view("int64")) / 1e9 / 3600 median_dt = float(np.median(deltas_h)) if len(deltas_h) else float("nan") gaps = int((deltas_h > 1.5).sum()) # buchi > 1.5h expected = int(round(span_days * 24)) + 1 coverage = len(df) / expected if expected else float("nan") f = df["funding"].values # statistiche funding (orario) ann = float(np.nanmean(f)) * 24 * 365 # funding annualizzato (carry teorico per chi paga) cap_hits = int((np.abs(f) > 0.0006).sum()) # cap HL ~0.06%/h (4%/8h clamp); fuori = sospetto status = "OK" if coverage < 0.97 or gaps > 50: status = "GAP" if span_days < 365: status = "corto<365g" return {"coin": coin, "n": len(df), "primo": idx[0].date(), "ultimo": idx[-1].date(), "giorni": round(span_days), "cad_h": round(median_dt, 3), "gap>1.5h": gaps, "cover%": round(coverage * 100, 1), "fund_med_bps": round(float(np.nanmedian(f)) * 1e4, 4), "fund_ann%": round(ann * 100, 1), "cap_hit": cap_hits, "status": status} def main(): print("=" * 100) print(" FETCH + CERTIFY funding Hyperliquid (orario, tokenless) — 19 major XS01 -> data/raw/hlfund_*") print("=" * 100) rep = [] for sym in UNIVERSE: try: df = fetch_funding(sym) except Exception as e: print(f" {sym:5} ERR {repr(e)[:80]}") rep.append({"coin": sym, "n": 0, "status": "ERR"}) continue c = certify(sym, df) rep.append(c) if c.get("n", 0) > 0: out = RAW / f"hlfund_{sym.lower()}_1h.parquet" df.to_parquet(out) print(f" {sym:5} n={c.get('n',0):>6} {str(c.get('primo','')):>10}->{str(c.get('ultimo','')):>10} " f"cad={c.get('cad_h','?')}h gap={c.get('gap>1.5h','?')} cov={c.get('cover%','?')}% " f"med={c.get('fund_med_bps','?')}bps ann={c.get('fund_ann%','?')}% cap_hit={c.get('cap_hit','?')} " f"[{c['status']}]") ok = [r["coin"] for r in rep if r.get("status") in ("OK",)] short = [r["coin"] for r in rep if r.get("status") == "corto<365g"] print("-" * 100) print(f" CERTIFICATI OK ({len(ok)}): {ok}") if short: print(f" CORTI <365g ({len(short)}): {short}") print(f" Scritti in data/raw/hlfund__1h.parquet (funding ORARIO, serie nativa).") if __name__ == "__main__": main()