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.
This commit is contained in:
ramseshk
2026-08-06 17:23:49 +08:00
parent 08a95e8fe2
commit f5ffe4baee
16 changed files with 22419 additions and 20 deletions
+231
View File
@@ -0,0 +1,231 @@
"""
NautilusTrader event-driven backtest engine for Hyperliquid strategies.
Sets up a BacktestEngine with Hyperliquid venue, instruments, historical
bar data, and registered strategies. Runs event-driven simulation with
realistic fill emulation (maker/taker, slippage).
Slower but more realistic than VectorBT — intended for final validation
before paper/live deployment.
"""
from __future__ import annotations
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import numpy as np
import pandas as pd
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from nautilus_trader.backtest.engine import BacktestEngine, BacktestEngineConfig
from nautilus_trader.model.data import Bar, BarSpecification, BarType
from nautilus_trader.model.enums import BarAggregation, PriceType
from nautilus_trader.model.identifiers import InstrumentId, Venue
from nautilus_trader.model.instruments import CryptoPerpetual
from nautilus_trader.model.objects import Price, Quantity
from framework.data import HyperliquidDataProvider, INTERVAL_TO_SECONDS
from framework.instruments import HL_VENUE
logger = logging.getLogger(__name__)
INTERVAL_TO_AGG = {
"1m": (1, BarAggregation.MINUTE),
"5m": (5, BarAggregation.MINUTE),
"15m": (15, BarAggregation.MINUTE),
"30m": (30, BarAggregation.MINUTE),
"1h": (1, BarAggregation.HOUR),
"4h": (4, BarAggregation.HOUR),
"8h": (8, BarAggregation.HOUR),
"1d": (1, BarAggregation.DAY),
}
class NTBacktestRunner:
"""NautilusTrader backtest engine wrapper for Hyperliquid."""
def __init__(self):
pass
def run_backtest(
self,
strategy: str = "pairs",
interval: str = "1h",
instruments: dict[str, CryptoPerpetual] | None = None,
testnet: bool = False,
limit: int = 5000,
) -> dict[str, Any] | None:
"""Run event-driven backtest with NautilusTrader.
1. Set up BacktestEngine
2. Register Hyperliquid venue + instruments
3. Load historical bars from Hyperliquid
4. Add strategy and run
5. Return metrics
"""
step, agg = INTERVAL_TO_AGG.get(interval, (1, BarAggregation.HOUR))
config = BacktestEngineConfig()
engine = BacktestEngine(config=config)
engine.add_venue(HL_VENUE)
# Add instruments
if instruments:
for inst in instruments.values():
engine.add_instrument(inst)
coin = self._get_coin(strategy)
# Fetch real candles
provider = HyperliquidDataProvider(testnet=testnet)
df = provider.fetch_candles(coin, interval=interval, limit=limit)
if df.empty:
logger.error("No candles for %s", coin)
return None
# Build bars
inst_id = InstrumentId.from_str(f"{coin.upper()}-USD-PERP.HYPERLIQUID")
bars = self._df_to_bars(df, inst_id, step, agg)
# Add bars
engine.add_data(bars)
# Add strategy
strategy_class = self._resolve_strategy_class(strategy)
if strategy_class is None:
logger.error("No NT strategy class for %s", strategy)
return None
from framework.config import StrategyConfig
cfg = StrategyConfig(
name=strategy,
instrument=f"{coin}-USD-PERP",
asset=coin,
allocation=10000.0,
order_size=0.001,
)
nt_strategy = strategy_class(cfg)
engine.add_strategy(nt_strategy)
# Run
try:
result = engine.run()
except Exception as e:
logger.error("Backtest engine error: %s", e)
import traceback
traceback.print_exc()
return None
# Extract metrics
return self._extract_result(result, engine, strategy, interval, len(bars))
# ── Helpers ─────────────────────────────────────────────────
def _get_coin(self, strategy: str) -> str:
return {"pairs": "ETH", "hurst_vpin": "BTC", "as_mm": "BTC",
"obi": "BTC", "funding_arb": "BTC"}.get(strategy, "BTC")
def _df_to_bars(
self,
df: pd.DataFrame,
instrument_id: InstrumentId,
step: int,
aggregation: BarAggregation,
) -> list[Bar]:
spec = BarSpecification(step, aggregation, PriceType.LAST)
bar_type = BarType(instrument_id, spec)
bars = []
for idx, row in df.iterrows():
ts = int(idx.timestamp() * 1e9)
bar = Bar(
bar_type=bar_type,
open=Price.from_str(str(row["open"])),
high=Price.from_str(str(row["high"])),
low=Price.from_str(str(row["low"])),
close=Price.from_str(str(row["close"])),
volume=Quantity.from_str(str(row["volume"])),
ts_event=ts,
ts_init=ts,
)
bars.append(bar)
return bars
def _resolve_strategy_class(self, strategy: str):
import importlib
registry = {
"pairs": "strategies.nt.pairs_trading_nt.PairsTradingNT",
"hurst_vpin": "strategies.nt.hurst_vpin_nt.HurstVPINNT",
"as_mm": "strategies.nt.as_mm_nt.ASMarketMakingNT",
}
path = registry.get(strategy)
if not path:
return None
module_path, class_name = path.rsplit(".", 1)
mod = importlib.import_module(module_path)
return getattr(mod, class_name)
def _extract_result(
self,
result,
engine,
strategy: str,
interval: str,
n_bars: int,
) -> dict:
try:
pnl = float(sum(
a.pnl() for a in result.accounts if hasattr(a, 'pnl')
)) if hasattr(result, 'accounts') else 0.0
except Exception:
pnl = 0.0
try:
equity = result.equity_curve if hasattr(result, 'equity_curve') else None
except Exception:
equity = None
equity_vals = []
if equity is not None and hasattr(equity, '__iter__'):
equity_vals = [float(v) for v in equity] if equity is not None else []
total_return = (equity_vals[-1] / 10000.0 - 1) * 100 if equity_vals else 0.0
return {
"strategy": strategy,
"engine": "nautilus_trader",
"interval": interval,
"n_bars": n_bars,
"start_equity": 10000.0,
"end_equity": equity_vals[-1] if equity_vals else 10000.0,
"total_return_pct": round(total_return, 2),
"pnl": round(pnl, 2),
"sharpe": self._compute_sharpe(equity_vals),
"max_drawdown_pct": round(self._compute_max_dd(equity_vals) * 100, 2),
"generated_at": datetime.now(timezone.utc).isoformat(),
}
def _compute_sharpe(self, equity: list[float]) -> float:
if len(equity) < 2:
return 0.0
returns = [(equity[i] - equity[i - 1]) / equity[i - 1] for i in range(1, len(equity))]
mean_ret = np.mean(returns) if returns else 0.0
std_ret = np.std(returns, ddof=1) if returns else 0.0
return (mean_ret / std_ret) * np.sqrt(365 * 24) if std_ret > 0 else 0.0
def _compute_max_dd(self, equity: list[float]) -> float:
if not equity:
return 0.0
peak = equity[0]
worst = 0.0
for v in equity:
if v > peak:
peak = v
dd = (peak - v) / peak if peak > 0 else 0.0
worst = max(worst, dd)
return worst