research(funding-carry): FC01 cross-sectional su HL -> fragile, NON regge + infra IB paper

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>
This commit is contained in:
Adriano Dal Pastro
2026-06-22 20:56:18 +00:00
parent 745ba7d066
commit 61180637eb
7 changed files with 537 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
"""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()