""" Hurst Exponent + VPIN Directional Strategy for Hyperliquid BTC-USD-PERP. Based on nautilustrader tutorial: https://nautilustrader.io/docs/latest/tutorials/hurst_vpin_kraken/ Components: 1. HURST EXPONENT (dollar bars) — R/S analysis, >0.55 = trending 2. VPIN (Volume-synchronized Probability of Informed Trading) — buy/sell aggressor volume imbalance over dollar-bar buckets 3. QUOTE-DRIVEN ENTRY — both signals agree → place order on next tick Data: Real Hyperliquid API trade fills (aggressor side + size + price). Dollar bars: constant-notional $10,000 bars. Hurst window: 128 bars (~R/S needs ≥ 64). VPIN window: 50 buckets. """ import numpy as np from collections import deque import time, json, requests, os # ═══════════════════════════════════════════════════════════ # 1. Dollar Bar Construction # ═══════════════════════════════════════════════════════════ class DollarBarBuilder: """Accumulate trades until notional threshold reached → emit bar.""" def __init__(self, threshold: float = 10_000.0): self.threshold = threshold self.reset() def reset(self): self.accum_vol = 0.0 self.open = self.high = self.low = self.close = None self.buy_vol = 0.0 self.sell_vol = 0.0 def add(self, price: float, size: float, side: str): notional = price * size self.accum_vol += notional if side.upper() == "B": self.buy_vol += notional else: self.sell_vol += notional if self.open is None: self.open = self.high = self.low = price else: self.high = max(self.high, price) self.low = min(self.low, price) self.close = price def is_ready(self) -> bool: return self.accum_vol >= self.threshold def emit(self) -> dict: bar = { "open": self.open, "high": self.high, "low": self.low, "close": self.close, "buy_vol": self.buy_vol, "sell_vol": self.sell_vol, "total_vol": self.accum_vol, } self.reset() return bar # ═══════════════════════════════════════════════════════════ # 2. Hurst Exponent (R/S Rescaled Range) # ═══════════════════════════════════════════════════════════ def hurst_rs(log_returns: list, max_lag: int = None) -> float: """R/S Hurst exponent on log returns. H > 0.55 → persistent (trending) H < 0.50 → anti-persistent (mean-reverting) H ≈ 0.50 → random walk """ n = len(log_returns) if n < 32: return 0.50 # not enough data if max_lag is None: max_lag = min(n // 2, 64) lags = range(2, min(max_lag + 1, n // 2 + 1)) rs_vals = [] for lag in lags: if lag < 2: continue segments = n // lag if segments < 2: continue r_div_s = [] for s in range(segments): seg = log_returns[s * lag:(s + 1) * lag] mean = np.mean(seg) deviations = np.cumsum(seg - mean) r = np.max(deviations) - np.min(deviations) sd = np.std(seg, ddof=1) if sd > 1e-12: r_div_s.append(r / sd) if r_div_s: rs_vals.append(np.mean(r_div_s)) if len(rs_vals) < 4: return 0.50 # H = slope of log(R/S) vs log(lag) log_lags = np.log([l for l in lags if l >= 2][:len(rs_vals)]) log_rs = np.log(rs_vals) slope, _ = np.polyfit(log_lags, log_rs, 1) return min(max(slope, 0.20), 0.90) # ═══════════════════════════════════════════════════════════ # 3. VPIN (Volume-synchronized Probability of Informed Trading) # ═══════════════════════════════════════════════════════════ class VPINComputer: """VPIN on dollar-bar buckets. Each bucket = one dollar bar. VPIN = abs(buy_vol - sell_vol) / total_vol of bucket. Running average over `window` buckets. """ def __init__(self, window: int = 50): self.window = window self.buckets = deque(maxlen=window) def add_bucket(self, buy_vol: float, sell_vol: float): total = buy_vol + sell_vol if total < 1.0: self.buckets.append((0.0, 0.0)) else: vpin = abs(buy_vol - sell_vol) / total signed = (buy_vol - sell_vol) / total # + = net buying self.buckets.append((vpin, signed)) @property def vpin(self) -> float: if not self.buckets: return 0.0 return np.mean([b[0] for b in self.buckets]) @property def direction(self) -> float: """Signed net direction: +1 = strong buying, -1 = strong selling.""" if not self.buckets: return 0.0 return np.mean([b[1] for b in self.buckets]) @property def ready(self) -> bool: return len(self.buckets) >= self.window # ═══════════════════════════════════════════════════════════ # 4. Strategy Signal Generator # ═══════════════════════════════════════════════════════════ class HurstVPINSignal: def __init__(self, notional_threshold: float = 10_000.0, hurst_window: int = 128, vpin_window: int = 50, hurst_entry: float = 0.55, hurst_exit: float = 0.52, vpin_threshold: float = 0.25): self.builder = DollarBarBuilder(notional_threshold) self.vpin = VPINComputer(vpin_window) self.hurst_window = hurst_window self.hurst_entry = hurst_entry self.hurst_exit = hurst_exit self.vpin_threshold = vpin_threshold self.returns = deque(maxlen=hurst_window) # Current state self.hurst_val = 0.50 self.vpin_val = 0.0 self.vpin_dir = 0.0 self.position = 0 # -1 short, 0 flat, +1 long self.last_bar_close = 0.0 self.bar_count = 0 def add_trade(self, price: float, size: float, side: str): """Process a single trade tick.""" self.builder.add(price, size, side) if self.builder.is_ready(): bar = self.builder.emit() return self._process_bar(bar) return None def _process_bar(self, bar: dict) -> dict | None: self.bar_count += 1 # Update VPIN self.vpin.add_bucket(bar["buy_vol"], bar["sell_vol"]) self.vpin_val = self.vpin.vpin if self.vpin.ready else 0.0 self.vpin_dir = self.vpin.direction if self.vpin.ready else 0.0 # Update Hurst returns if self.last_bar_close > 0: log_ret = np.log(bar["close"] / self.last_bar_close) self.returns.append(log_ret) self.last_bar_close = bar["close"] # Compute Hurst if len(self.returns) >= self.hurst_window: self.hurst_val = hurst_rs(list(self.returns)) else: self.hurst_val = 0.50 # Signal logic signal = self._compute_signal() return { "bar": bar, "hurst": round(self.hurst_val, 4), "vpin": round(self.vpin_val, 4), "vpin_dir": round(self.vpin_dir, 4), "signal": signal, "position": self.position, "bar_count": self.bar_count, } def _compute_signal(self) -> str: trending = self.hurst_val >= self.hurst_entry high_vpin = self.vpin_val >= self.vpin_threshold exiting = self.hurst_val <= self.hurst_exit # Exit: Hurst decays below exit threshold if self.position != 0 and exiting: self.position = 0 return "EXIT" # Entry: both agree if self.position == 0 and trending and high_vpin: if self.vpin_dir > 0.02: self.position = 1 return "BUY" elif self.vpin_dir < -0.02: self.position = -1 return "SELL" return "HOLD" # ═══════════════════════════════════════════════════════════ # 5. Hyperliquid Data Fetcher # ═══════════════════════════════════════════════════════════ def fetch_recent_trades(user: str = None, limit: int = 500) -> list: """Fetch recent BTC-USD-PERP fills from Hyperliquid mainnet.""" url = "https://api.hyperliquid.xyz/info" payload = {"type": "userFills", "user": user} if user else { "type": "allMids"} if user: resp = requests.post(url, json=payload, timeout=10) fills = resp.json() return fills[:limit] if isinstance(fills, list) else [] return [] # ═══════════════════════════════════════════════════════════ # 6. Backtest Runner # ═══════════════════════════════════════════════════════════ def run_hurst_vpin(trades: list, starting_capital: float = 100.0, size: float = 0.0002) -> dict: signal_gen = HurstVPINSignal() equity = [{"t": 0, "v": starting_capital}] capital = starting_capital position = 0 entry_price = 0.0 all_trades = [] signals = [] for i, trade in enumerate(trades): price = float(trade.get("px", 0)) sz = float(trade.get("sz", 0)) side = trade.get("side", "B") result = signal_gen.add_trade(price, sz, side) if result: signals.append(result) # Execute signal sig = result["signal"] if sig in ("BUY", "SELL") and position == 0: entry_price = price direction = 1 if sig == "BUY" else -1 notional = price * size if capital >= notional: all_trades.append({ "i": i, "side": sig, "price": price, "size": size, "hurst": result["hurst"], "vpin": result["vpin"], "bar_count": result["bar_count"], }) position = direction elif sig == "EXIT" and position != 0: pnl_pct = (price / entry_price - 1) * position pnl = capital * pnl_pct * 0.01 # 1% of capital at risk capital += pnl all_trades[-1]["exit_price"] = price all_trades[-1]["pnl"] = round(pnl, 4) equity.append({"t": i, "v": round(capital, 4)}) position = 0 entry_price = 0.0 return { "total_trades": len(all_trades), "signals": len(signals), "final_equity": round(capital, 4), "pnl_pct": round((capital / starting_capital - 1) * 100, 2), "trades": all_trades, "signals_history": signals[-20:], } # ═══════════════════════════════════════════════════════════ # 7. Test # ═══════════════════════════════════════════════════════════ if __name__ == "__main__": # Simulated backtest with synthetic trades print("Hurst/VPIN Strategy — Hyperliquid BTC-USD") np.random.seed(42) n = 50000 prices = 64000 + np.cumsum(np.random.randn(n) * 50) sizes = np.abs(np.random.randn(n) * 0.01) + 0.001 sides = ["B" if np.random.random() > 0.5 else "A" for _ in range(n)] sim_trades = [{"px": p, "sz": s, "side": sd} for p, s, sd in zip(prices, sizes, sides)] result = run_hurst_vpin(sim_trades) print(f" Total trades: {result['total_trades']}") print(f" Signals generated: {result['signals']}") print(f" Final equity: ${result['final_equity']:.2f} ({result['pnl_pct']:+.2f}%)") print(f" Last signals:") for s in result["signals_history"][-5:]: print(f" H={s['hurst']:.3f} VPIN={s['vpin']:.3f} dir={s['vpin_dir']:+.3f} → {s['signal']}")