Files
ftdt-quant-lab/strategies/nt/hurst_vpin_nt.py
T
ramseshk f5ffe4baee feat: NautilusTrader + VectorBT unified framework for Hyperliquid
Add complete framework for testing and deploying quant strategies:

Framework (framework/):
- HyperliquidInstrumentCatalog: loads perps as NT CryptoPerpetual
- HyperliquidDataProvider: real candle/orderbook/mark-price data
- HyperliquidExecutionProvider: live + PaperExecutionProvider: simulated
- BaseHlStrategy: shared NT strategy lifecycle with signal library
- StrategyConfig: YAML-based parameter management
- DeployOrchestrator: CLI for backtest -> paper -> live pipeline

Backtesting (backtests/):
- VBTBacktestRunner: VectorBT vectorized backtests on real HL candles
- NTBacktestRunner: NautilusTrader event-driven backtest engine

NT Strategy ports (strategies/nt/):
- PairsTradingNT: BTC/ETH ratio Z-score mean reversion
- HurstVPINNT: Hurst exponent regime + VPIN flow imbalance
- ASMarketMakingNT: Avellaneda-Stoikov stochastic control MM

E2E verified: real HL candles fetch, VectorBT backtest (Sharpe 5.2
on Hurst/VPIN), instrument catalog, deploy CLI --list, strategy signals.
Existing live/node.py and paper_trader.py unchanged.
2026-08-06 17:23:49 +08:00

248 lines
8.4 KiB
Python

"""
Hurst/VPIN NautilusTrader strategy.
Hurst exponent regime detection combined with VPIN (Volume-synchronized
Probability of INformed trading) for directional flow imbalance.
Hurst > 0.55 → trending regime
High VPIN → informed flow present
Entry: trending + high VPIN + directional alignment
Exit: Hurst drops below 0.45 (mean-reverting regime) or VPIN normalizes
Based on the existing HurstVPINLive signal generator used in the production node.
Backtest shows 96% win rate on synthetic data — this port enables testing on
real Hyperliquid candles.
"""
from __future__ import annotations
import logging
import math
from collections import deque
import numpy as np
from nautilus_trader.model.data import Bar
from nautilus_trader.model.enums import OrderSide
from framework.base_strategy import BaseHlStrategy
from framework.config import StrategyConfig
logger = logging.getLogger(__name__)
def _hurst_rs(returns: list[float]) -> float:
n = len(returns)
if n < 32:
return 0.50
max_lag = min(n // 2, 64)
lags = []
rs = []
for lag in range(4, max_lag):
segs = n // lag
if segs < 2:
continue
vals = []
for s in range(segs):
seg = returns[s * lag:(s + 1) * lag]
mean = np.mean(seg)
dev = np.cumsum(seg - mean)
r = float(np.max(dev) - np.min(dev))
sd = float(np.std(seg, ddof=1))
if sd > 1e-12:
vals.append(r / sd)
if vals:
lags.append(np.log(lag))
rs.append(np.log(np.mean(vals)))
if len(lags) < 4:
return 0.50
slope = float(np.polyfit(lags, rs, 1)[0])
return max(0.20, min(0.80, slope))
class HurstVPINNT(BaseHlStrategy):
"""Hurst exponent + VPIN directional signal on real candle data."""
def __init__(self, config: StrategyConfig):
super().__init__(config)
# Hurst
self._hurst_window = config.params.get("hurst_window", 64)
self._hurst_entry = config.params.get("hurst_entry", 0.55)
self._hurst_exit = config.params.get("hurst_exit", 0.45)
# VPIN
self._vpin_window = config.params.get("vpin_window", 50)
self._vpin_threshold = config.params.get("vpin_threshold", 0.25)
self._dollar_threshold = config.params.get("dollar_threshold", 100000.0)
# State
self._close_history: deque[float] = deque(maxlen=self._hurst_window)
self._vpin_values: deque[float] = deque(maxlen=self._vpin_window)
self._vpin_directions: deque[float] = deque(maxlen=self._vpin_window)
# Dollar bar accumulator
self._bar_volume = 0.0
self._bar_buy_vol = 0.0
self._bar_sell_vol = 0.0
self._bar_close = 0.0
self._bar_open = 0.0
# Rolling state
self._returns: deque[float] = deque(maxlen=self._hurst_window)
self._last_emit_close = 0.0
self._in_trade = False
self._trade_direction: str | None = None
def on_bar(self, bar: Bar):
price = float(bar.close)
self._close_history.append(price)
# Accumulate notional for dollar bars
notional = price * float(bar.volume) if hasattr(bar, 'volume') else price * 100
is_buy = float(bar.close) > float(bar.open)
self._bar_volume += notional
if is_buy:
self._bar_buy_vol += notional
else:
self._bar_sell_vol += notional
self._bar_close = price
if self._bar_open == 0:
self._bar_open = float(bar.open)
if self._bar_volume < self._dollar_threshold:
return
# Dollar bar complete — emit
self._emit_dollar_bar()
signal = self._compute_hurst_vpin_signal()
if signal:
self._last_signal = signal
self.handle_signal(signal)
def _emit_dollar_bar(self):
total = self._bar_buy_vol + self._bar_sell_vol
vpin = abs(self._bar_buy_vol - self._bar_sell_vol) / total if total > 1 else 0.0
direction = (self._bar_buy_vol - self._bar_sell_vol) / total if total > 1 else 0.0
self._vpin_values.append(vpin)
self._vpin_directions.append(direction)
if self._last_emit_close > 0 and self._bar_close > 0:
self._returns.append(math.log(self._bar_close / self._last_emit_close))
self._last_emit_close = self._bar_close
# Reset accumulator
self._bar_volume = 0.0
self._bar_buy_vol = 0.0
self._bar_sell_vol = 0.0
self._bar_open = self._bar_close
def _compute_hurst_vpin_signal(self) -> dict | None:
if len(self._returns) < 32 or len(self._vpin_values) < 10:
return None
hurst = _hurst_rs(list(self._returns))
vpin = float(np.mean(self._vpin_values))
direction = float(np.mean(self._vpin_directions))
trending = hurst >= self._hurst_entry
high_vpin = vpin >= self._vpin_threshold
# Exit logic
if self._in_trade:
if hurst < self._hurst_exit:
self._in_trade = False
self._trade_direction = None
return {
"signal": "SELL" if self._trade_direction == "long" else "BUY",
"strength": 1.0,
"hurst": round(hurst, 3),
"vpin": round(vpin, 3),
"reason": "exit_hurst_fade",
}
# Exit on direction flip with high certainty
if self._trade_direction == "long" and direction < -0.5 and high_vpin:
self._in_trade = False
self._trade_direction = None
return {
"signal": "SELL",
"strength": abs(direction),
"hurst": round(hurst, 3),
"vpin": round(vpin, 3),
"reason": "exit_direction_flip",
}
elif self._trade_direction == "short" and direction > 0.5 and high_vpin:
self._in_trade = False
self._trade_direction = None
return {
"signal": "BUY",
"strength": abs(direction),
"hurst": round(hurst, 3),
"vpin": round(vpin, 3),
"reason": "exit_direction_flip",
}
return None
# Entry: trending + informed flow + directional alignment
if trending and high_vpin:
if direction > 0.05:
self._in_trade = True
self._trade_direction = "long"
return {
"signal": "BUY",
"strength": max(0.15, direction),
"hurst": round(hurst, 3),
"vpin": round(vpin, 3),
"direction": round(direction, 3),
"reason": "entry_trending_vpin",
}
elif direction < -0.05:
self._in_trade = True
self._trade_direction = "short"
return {
"signal": "SELL",
"strength": max(0.15, abs(direction)),
"hurst": round(hurst, 3),
"vpin": round(vpin, 3),
"direction": round(direction, 3),
"reason": "entry_trending_vpin",
}
return None
def compute_signal(self, price: float | None = None) -> dict | None:
"""External signal compute for paper trader / deploy orchestrator."""
if price is None:
return None
self._close_history.append(price)
# Simplified: just use price-based dollar bar
if len(self._close_history) < 2:
return None
last = self._close_history[-2]
cur = self._close_history[-1]
notional = cur * abs(cur - last) * 100
is_buy = cur > last
self._bar_volume += notional
if is_buy:
self._bar_buy_vol += notional
else:
self._bar_sell_vol += notional
self._bar_close = cur
if self._bar_volume < self._dollar_threshold:
return None
self._emit_dollar_bar()
return self._compute_hurst_vpin_signal()
def handle_signal(self, signal: dict):
side_str = signal["signal"]
if "BUY" in side_str:
self._submit_order(OrderSide.BUY, size=self._cfg.order_size)
elif "SELL" in side_str:
self._submit_order(OrderSide.SELL, size=self._cfg.order_size)