61180637eb
Onda "nuova ricerca mirata". Unico meccanismo non coperto dalle 2 ondate: carry da funding (cashflow perp, delta-neutral). Scan dati: price-clock gia' FAIL (intraday), Deribit ccxt 0 righe, Cerbero solo candele -> fonte = API pubblica Hyperliquid. - fetch_hl_funding.py: 19 major, funding orario reale dal 2023-05, certificato (0 gap, cov 98-100%, ann +1.0% APT .. +21.6% NEAR). backoff anti-429. - funding_carry_hl.py: book dollar-neutral short-alto-funding/long-basso, causale come XS01, vol-target 20%, fee 0.05%/lato. Giudizio: marginal_vs_tp01 indurito + overlap XS01. VERDETTO: il premio esiste (carry >> anti) ma il book NON regge il gauntlet. FULL -0.12, HOLD -0.50, DILUTES vs TP01, in-sample edge <0.5, no multicut. Jackknife universo: FULL oscilla [-0.39,+0.30] togliendo UN asset -> FRAGILE/overfit. (preview a 17 asset era +0.62 ADDS: fortuna, mancavano NEAR/AAVE). corr XS01 -0.19 (ortogonale, non re-skin). Meccanismo: carry-vs-momentum, gli alto-funding pompano. -> NON entra in portafoglio, fetcher NON in cron. Diario completo. Infra IB (thread parallelo): gateway paper gnzsnz/ib-gateway (127.0.0.1:4002, READ_ONLY) in docker-compose + ib_probe.py. Esito dati basis CME micro: backtest NON fattibile (ContFuture back-adjusted, scaduti=1 barra). IB ok per esecuzione/forward, non ricerca. .env.ibgw gitignored (credenziali paper), template in .env.ibgw.example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
6.1 KiB
Python
138 lines
6.1 KiB
Python
"""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_<sym>_1h.parquet (namespace dedicato 'hlfund', NON tocca hl_<sym>_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_<sym>_1h.parquet (funding ORARIO, serie nativa).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|