Fix historical cards + Hurst/VPIN strategy
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
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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']}")
|
||||
Reference in New Issue
Block a user