docs(research): XEX — discordanze Deribit testnet vs Hyperliquid
Spread D/H mean-reverting (half-life 3-7h); su BTC/ETH inverse il BOOK stesso e' dislocato 1-2% (reale, eseguibile), su DOGE/SOL e' stale-print illusion (book allineato a HL, 87%/35% barre flat). Edge testnet-only: non deployato, record + telemetria. Vedi diario. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
# 2026-06-12 — XEX: discordanze Deribit testnet vs Hyperliquid
|
||||||
|
|
||||||
|
**Domanda.** I prezzi Deribit (testnet, dove eseguiamo) e Hyperliquid (feed
|
||||||
|
realistico, proxy della realtà) divergono? La divergenza è usabile per fare trade?
|
||||||
|
|
||||||
|
**Strumenti comuni validati:** 9 base-coin (ADA, AVAX, BNB, BTC, DOGE, ETH, PAXG,
|
||||||
|
SOL, TRUMP). Analisi su BTC/ETH (inverse, liquidi) + SOL/DOGE (lineari USDC).
|
||||||
|
Script: `scripts/analysis/xex_divergence_research.py`.
|
||||||
|
|
||||||
|
## Misure (1h, 2026-03-01 → 2026-06-12, ~2470 barre)
|
||||||
|
|
||||||
|
| Coin | spread medio | std | half-life | flat Deribit | Δbook−HL live |
|
||||||
|
|------|--------------|-----|-----------|--------------|----------------|
|
||||||
|
| BTC | −0.29% | 1.25% | 7.3h | 6% | **−0.97%** |
|
||||||
|
| ETH | −0.55% | 1.33% | 5.1h | 9% | **−1.54…−2.16%** |
|
||||||
|
| SOL | −0.38% | 3.60% | 5.2h | 35% | −0.05% (allineato) |
|
||||||
|
| DOGE | +0.34% | 1.88% | 4.2h | 87% | +0.16% (allineato) |
|
||||||
|
|
||||||
|
Per standard mainnet questi spread sono enormi (reale <0.05%): la divergenza è
|
||||||
|
l'artefatto del feed testnet che periodicamente si stacca dalla realtà e rientra.
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
1. **Lo spread è mean-reverting e il gap viene chiuso ANCHE da Deribit** (il lato
|
||||||
|
che possiamo tradare): beta del ritorno futuro Deribit sullo spread negativo e
|
||||||
|
crescente con l'orizzonte (ETH −0.36, BTC −0.23 a 24h). Non è solo HL che si
|
||||||
|
muove.
|
||||||
|
2. **Trappola smascherata — stale print arb.** Il backtest su DOGE dava Sharpe
|
||||||
|
6.7 (OOS 9.1!) e SOL 2.7, MA il book live di entrambi sta attaccato a HL
|
||||||
|
(±0.16%) mentre i print restano vecchi (DOGE 87% barre flat): l'edge era
|
||||||
|
**finzione** — i fill reali avverrebbero al prezzo vero, non al print stantio.
|
||||||
|
Stessa classe di illusione del look-ahead squeeze: il backtest compra un
|
||||||
|
prezzo a cui nessuno fa fill.
|
||||||
|
3. **Su BTC/ETH inverse la dislocazione è REALE: è il book stesso a essere
|
||||||
|
spostato** (misurato live: bid/ask −0.97% / −1.54…−2.16% sotto HL con depth
|
||||||
|
>$1M sui primi 5 livelli, spread bid/ask 1-3 bps). Lì si può davvero comprare
|
||||||
|
sotto/vendere sopra la realtà.
|
||||||
|
4. **Edge netto moderato e timing-sensitive** (fee 0.10% RT, entry |s|≥1%, exit
|
||||||
|
|s|≤0.25% o 24 barre): BTC FULL Sh 1.0 / OOS 2.05; ETH FULL 2.28 / OOS 1.37.
|
||||||
|
Con entry ritardato di 1 barra (stress staleness) BTC FULL→0, ETH OOS→0.1:
|
||||||
|
con half-life di 3-7 ore, un'ora di ritardo erode quasi tutto. Un'eventuale
|
||||||
|
implementazione live deve leggere il **book in tempo reale** (non il close 1h)
|
||||||
|
— a quel punto il lag reale è minuti, non un'ora, e l'edge eseguibile sta fra
|
||||||
|
lag0 e lag1.
|
||||||
|
|
||||||
|
## Segnale live al momento dell'analisi
|
||||||
|
|
||||||
|
ETH book Deribit **−2.16%** sotto HL (z=−1.7, 6.7° percentile storico), BTC
|
||||||
|
−0.97%: entrambi in zona "long Deribit" per il fade dello spread. (Solo
|
||||||
|
osservazione: nessun deploy senza gate.)
|
||||||
|
|
||||||
|
## Verdetto e prossimi passi
|
||||||
|
|
||||||
|
- Discordanza tradabile trovata: **fade dello spread D/H su BTC/ETH-PERPETUAL,
|
||||||
|
segnale dal book, soglia ~1%, exit a convergenza o 24h**. Da NON estendere a
|
||||||
|
DOGE/SOL (illusione stale-print).
|
||||||
|
- È un **edge di testnet** (non trasferibile a mainnet): legittimo per il
|
||||||
|
paper/shadow corrente, ma va dichiarato per quello che è.
|
||||||
|
- Prima di qualsiasi deploy, metodologia standard: gate PORT06 (correlazione con
|
||||||
|
le fade esistenti, che già fadano in parte questi stessi spike testnet —
|
||||||
|
rischio doppio conteggio), robustezza griglia, validazione worker.
|
||||||
|
|
||||||
|
Script: `scripts/analysis/xex_divergence_research.py` (fetch v2 due exchange,
|
||||||
|
tabella convergenza, backtest lag0/lag1, book reality-check live).
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
"""XEX — Discordanze cross-exchange Deribit (testnet) vs Hyperliquid.
|
||||||
|
|
||||||
|
Ricerca 2026-06-12. Domanda: il prezzo Deribit testnet si discosta da quello
|
||||||
|
Hyperliquid (proxy della realta'); lo scostamento e' tradabile dal nostro conto?
|
||||||
|
|
||||||
|
Esito (vedi diario docs/diary/2026-06-12-xex-divergence.md):
|
||||||
|
- Lo spread log(D/H) e' enorme per standard reali (std 0.9-4.5%) e MEAN-REVERTING
|
||||||
|
(AR1 rho 0.77-0.94, half-life 2.7-12 barre 1h).
|
||||||
|
- Il gap viene chiuso da ENTRAMBI i lati: beta del ritorno futuro Deribit sullo
|
||||||
|
spread e' negativo e cresce con l'orizzonte (ETH -0.36, BTC -0.23 a 24h)
|
||||||
|
-> tradabile dal lato Deribit (il nostro conto).
|
||||||
|
- TRAPPOLA SMASCHERATA: su DOGE/SOL (lineari USDC illiquidi, 87%/35% barre flat)
|
||||||
|
l'edge del backtest (Sharpe 6.7/2.7) e' FINZIONE da print stantii: il BOOK
|
||||||
|
live sta attaccato a HL (+0.16%/-0.05%) mentre i print restano vecchi.
|
||||||
|
Su BTC/ETH inverse invece il BOOK STESSO e' dislocato (-0.94%/-2.16% misurati
|
||||||
|
live con depth >$1M) -> la' la discordanza e' reale ed eseguibile.
|
||||||
|
- Candidati: solo BTC-PERPETUAL / ETH-PERPETUAL. Edge netto (fee 0.10% RT)
|
||||||
|
moderato e sensibile al timing (half-life corta: lag 1h di entry lo erode).
|
||||||
|
NON deployare senza: segnale dal BOOK (non dal close), poll fitto, gate PORT06.
|
||||||
|
|
||||||
|
NB: e' un edge da TESTNET (la dislocazione e' l'artefatto del feed testnet che
|
||||||
|
rientra verso la realta'): non trasferibile a mainnet, dove lo spread D/H reale
|
||||||
|
e' <0.05%. Utile per il paper/shadow corrente, non per capitale vero.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from src.live.cerbero_client import CerberoClient
|
||||||
|
|
||||||
|
FEE_RT = 0.001
|
||||||
|
START, END = "2026-03-01", "2026-06-12"
|
||||||
|
SPLIT = pd.Timestamp("2026-05-10", tz="UTC")
|
||||||
|
PAIRS = [
|
||||||
|
("BTC", "BTC-PERPETUAL"),
|
||||||
|
("ETH", "ETH-PERPETUAL"),
|
||||||
|
("SOL", "SOL_USDC-PERPETUAL"),
|
||||||
|
("DOGE", "DOGE_USDC-PERPETUAL"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(c: CerberoClient, coin: str, d_inst: str) -> pd.DataFrame:
|
||||||
|
def hist(ex: str, inst: str) -> pd.Series:
|
||||||
|
rows = c.get_historical_v2(inst, START, END, interval="1h", exchange=ex)
|
||||||
|
df = pd.DataFrame(rows)
|
||||||
|
df["ts"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
df = df.set_index("ts")
|
||||||
|
return df.loc[~df.index.duplicated(), "close"]
|
||||||
|
|
||||||
|
return pd.DataFrame({"d": hist("deribit", d_inst), "h": hist("hyperliquid", coin)}).dropna()
|
||||||
|
|
||||||
|
|
||||||
|
def convergence_table(j: pd.DataFrame) -> None:
|
||||||
|
"""Chi chiude il gap: regressione spread[i] -> ritorno futuro per venue."""
|
||||||
|
s = np.log(j["d"] / j["h"])
|
||||||
|
for hz in (1, 6, 12, 24):
|
||||||
|
rd = np.log(j["d"].shift(-hz) / j["d"])
|
||||||
|
rh = np.log(j["h"].shift(-hz) / j["h"])
|
||||||
|
m = s.notna() & rd.notna() & rh.notna()
|
||||||
|
bd = np.polyfit(s[m], rd[m], 1)[0]
|
||||||
|
bh = np.polyfit(s[m], rh[m], 1)[0]
|
||||||
|
print(f" h={hz:>2}: beta_D={bd:+.2f} (lato tradabile) beta_H={bh:+.2f}")
|
||||||
|
|
||||||
|
|
||||||
|
def backtest(j: pd.DataFrame, entry: float = 1.0, exit_: float = 0.25,
|
||||||
|
max_bars: int = 24, fee: float = FEE_RT, lag: int = 0):
|
||||||
|
"""Fade dello spread sul solo lato Deribit. Entry al close (o close+lag per
|
||||||
|
stress staleness), skip barre flat, exit a |s|<=exit_ o max_bars."""
|
||||||
|
d, h = j["d"].values, j["h"].values
|
||||||
|
s = np.log(d / h) * 100
|
||||||
|
dret = np.r_[0.0, np.diff(np.log(d))]
|
||||||
|
flat = np.r_[True, dret[1:] == 0]
|
||||||
|
pos, entry_i, pnl, pend = 0, -1, 0.0, None
|
||||||
|
eq, trades = [0.0], []
|
||||||
|
for i in range(1, len(j)):
|
||||||
|
r = 0.0
|
||||||
|
if pos != 0:
|
||||||
|
r = pos * dret[i]
|
||||||
|
pnl += r
|
||||||
|
if abs(s[i]) <= exit_ or (i - entry_i) >= max_bars:
|
||||||
|
r -= fee / 2
|
||||||
|
trades.append(pnl - fee)
|
||||||
|
pos, pnl = 0, 0.0
|
||||||
|
if pend is not None and pend[0] == i:
|
||||||
|
if pos == 0:
|
||||||
|
pos, entry_i, pnl = pend[1], i, 0.0
|
||||||
|
r -= fee / 2
|
||||||
|
pend = None
|
||||||
|
if pos == 0 and pend is None and abs(s[i]) >= entry and not flat[i]:
|
||||||
|
if lag == 0:
|
||||||
|
pos, entry_i, pnl = -np.sign(s[i]), i, 0.0
|
||||||
|
r -= fee / 2
|
||||||
|
else:
|
||||||
|
pend = (i + lag, -np.sign(s[i]))
|
||||||
|
eq.append(r)
|
||||||
|
return pd.Series(eq, index=j.index), np.array(trades)
|
||||||
|
|
||||||
|
|
||||||
|
def report(rets: pd.Series, trades: np.ndarray, label: str) -> None:
|
||||||
|
ann = np.sqrt(24 * 365)
|
||||||
|
sh = rets.mean() / rets.std() * ann if rets.std() > 0 else 0.0
|
||||||
|
cum = rets.cumsum()
|
||||||
|
dd = (cum - cum.cummax()).min() * 100
|
||||||
|
wr = (trades > 0).mean() * 100 if len(trades) else 0.0
|
||||||
|
print(f" {label:10} ret={rets.sum() * 100:+7.1f}% Sh={sh:5.2f} DD={dd:6.2f}% "
|
||||||
|
f"n={len(trades):3d} WR={wr:4.1f}%")
|
||||||
|
|
||||||
|
|
||||||
|
def book_reality_check(c: CerberoClient) -> None:
|
||||||
|
"""Il test che separa edge vero da illusione: il BOOK e' dislocato o solo i print?"""
|
||||||
|
print("\n== Book Deribit vs mark Hyperliquid (live) ==")
|
||||||
|
for coin, inst in PAIRS:
|
||||||
|
try:
|
||||||
|
ob = c._post("/mcp-deribit/tools/get_orderbook", {"instrument_name": inst, "depth": 5})
|
||||||
|
ht = c._post("/mcp-hyperliquid/tools/get_ticker", {"instrument": coin})
|
||||||
|
bb, ba = ob["bids"][0][0], ob["asks"][0][0]
|
||||||
|
mid, hm = (bb + ba) / 2, ht["mark_price"]
|
||||||
|
print(f" {inst:22} book {bb}/{ba} Δbook-HL={100 * (mid / hm - 1):+.2f}% "
|
||||||
|
f"depth5 bid={sum(b[1] for b in ob['bids']):.3g} ask={sum(a[1] for a in ob['asks']):.3g}")
|
||||||
|
except Exception as e: # endpoint o strumento indisponibile: solo report
|
||||||
|
print(f" {inst:22} ERR {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def run() -> None:
|
||||||
|
c = CerberoClient()
|
||||||
|
data = {coin: fetch(c, coin, inst) for coin, inst in PAIRS}
|
||||||
|
for coin, j in data.items():
|
||||||
|
s = np.log(j["d"] / j["h"]) * 100
|
||||||
|
rho = (s - s.mean()).autocorr(1)
|
||||||
|
hlife = -np.log(2) / np.log(rho) if 0 < rho < 1 else float("inf")
|
||||||
|
flat = (j["d"].pct_change() == 0).mean() * 100
|
||||||
|
print(f"\n== {coin} ({len(j)} barre 1h) spread mean={s.mean():+.2f}% std={s.std():.2f}% "
|
||||||
|
f"half-life={hlife:.1f}h flatD={flat:.0f}%")
|
||||||
|
convergence_table(j)
|
||||||
|
for lag in (0, 1):
|
||||||
|
r, t = backtest(j, lag=lag)
|
||||||
|
report(r, t, f"FULL lag{lag}")
|
||||||
|
roo, too = backtest(j[j.index >= SPLIT], lag=lag)
|
||||||
|
report(roo, too, f"OOS lag{lag}")
|
||||||
|
book_reality_check(c)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
Reference in New Issue
Block a user