From c98681c13093f5baa2fc3d5387ea06a6540794d3 Mon Sep 17 00:00:00 2001 From: ramseshk Date: Thu, 6 Aug 2026 04:49:00 +0000 Subject: [PATCH] Fix historical cards + Hurst/VPIN strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Historical tab fix: - StrategyCard: handle BacktestSummary type (not Strategy) - Pass coin/badge/stats/pnlPct/status props for historical - Historical cards now show proper data Hurst/VPIN directional strategy (Hyperliquid BTC-USD): - Dollar bars (constant-notional 0K) - Hurst exponent R/S analysis on 128-bar window - VPIN on 50-bucket volume imbalance - Quote-driven entry: both signals agree → BUY/SELL - Exit: Hurst decays below exit threshold --- dashboard/static/index.html | 4 +- strategies/hurst_vpin.py | 332 ++++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 strategies/hurst_vpin.py diff --git a/dashboard/static/index.html b/dashboard/static/index.html index f93359a..de6052c 100644 --- a/dashboard/static/index.html +++ b/dashboard/static/index.html @@ -1,4 +1,4 @@ -Quant Dashboard
Live Testnet
OFFLINE · ···
\ No newline at end of file + text-[#6e7381] hover:text-[#1a1c23]">Historical
\ No newline at end of file diff --git a/strategies/hurst_vpin.py b/strategies/hurst_vpin.py new file mode 100644 index 0000000..f266ee3 --- /dev/null +++ b/strategies/hurst_vpin.py @@ -0,0 +1,332 @@ +""" +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']}")