Files
ftdt-quant-lab/framework/base_strategy.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

200 lines
7.0 KiB
Python

"""
Base strategy class for NautilusTrader + Hyperliquid.
Provides shared lifecycle for all FTDT strategies:
- Instrument resolution from Hyperliquid catalog
- Fee-aware position sizing from StrategyConfig
- Shared signal pipeline (OBI, Hurst, VPIN computations)
- on_start / on_bar / on_stop hooks
Strategies inherit this and override signal logic.
"""
from __future__ import annotations
import logging
from collections import deque
from typing import Any
import numpy as np
from nautilus_trader.common.actor import Actor
from nautilus_trader.model.data import Bar
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.identifiers import InstrumentId
from nautilus_trader.trading.strategy import Strategy
from framework.config import StrategyConfig
from framework.data import HyperliquidDataProvider
from framework.instruments import HyperliquidInstrumentCatalog
logger = logging.getLogger(__name__)
class BaseHlStrategy(Strategy):
"""Base strategy with Hyperliquid-specific utilities.
Inherits NautilusTrader Strategy lifecycle:
on_start → on_bar (repeated) → on_stop
"""
def __init__(self, config: StrategyConfig):
super().__init__()
self._cfg = config
self._instrument: InstrumentId | None = None
self._asset = config.asset
# Price history for signal calculations
self._prices: deque[float] = deque(maxlen=300)
# Signal state
self._last_signal: dict[str, Any] | None = None
self._position_open: bool = False
self._entry_price: float = 0.0
@property
def config(self) -> StrategyConfig:
return self._cfg
@property
def instrument_id(self) -> InstrumentId | None:
return self._instrument
# ── Lifecycle ───────────────────────────────────────────────
def on_start(self):
"""Called when strategy is started. Resolve instruments."""
if not self._instrument:
# Try to resolve from catalog
catalog = HyperliquidInstrumentCatalog(testnet=self._cfg.testnet)
inst_map = catalog.load(assets=[self._asset])
inst = inst_map.get(self._asset.upper())
if inst:
self._instrument = inst.id
else:
self._instrument = InstrumentId.from_str(
f"{self._asset.upper()}-USD-PERP.HYPERLIQUID"
)
# Subscribe to 1-minute bars
self.subscribe_bars(self._instrument)
logger.info("%s started on %s", self._cfg.name, self._instrument)
def on_stop(self):
logger.info("%s stopped", self._cfg.name)
def on_bar(self, bar: Bar):
"""Process each bar. Override in subclasses for custom signal logic."""
self._prices.append(float(bar.close))
signal = self.compute_signal()
if signal:
self._last_signal = signal
self.handle_signal(signal)
# ── Signal computation (override in subclass) ───────────────
def compute_signal(self) -> dict[str, Any] | None:
"""Override in subclass to compute trading signals."""
return None
def handle_signal(self, signal: dict[str, Any]):
"""Default: submit a limit order based on signal direction."""
side = signal.get("signal", "")
strength = signal.get("strength", 0.0)
# Check minimum strength threshold
if strength < 0.15:
return
if "BUY" in str(side).upper():
self._submit_order(OrderSide.BUY)
elif "SELL" in str(side).upper():
self._submit_order(OrderSide.SELL)
# ── Order submission (override or use directly) ─────────────
def _submit_order(self, side: OrderSide, size: float | None = None):
"""Submit a limit order at current price."""
sz = size or self._cfg.order_size
price = self._prices[-1] if self._prices else 0.0
if price <= 0:
return
try:
self.submit_order(
instrument_id=self._instrument,
order_side=side,
order_type="LIMIT",
quantity=Quantity.from_str(str(sz)),
price=Price.from_str(str(int(price))),
post_only=True,
)
except Exception as e:
logger.warning("%s order failed: %s", self._cfg.name, e)
# ── Signal library (shared across strategies) ───────────────
def signal_zscore(self, window: int = 20, threshold: float = 1.5) -> dict | None:
"""Z-score mean reversion signal based on price history."""
if len(self._prices) < window:
return None
prices = list(self._prices)
recent = prices[-window:]
mu = np.mean(recent)
std = np.std(recent, ddof=1)
if std <= 0:
return None
z = (prices[-1] - mu) / std
if z > threshold:
return {"signal": "SELL", "strength": z / threshold}
elif z < -threshold:
return {"signal": "BUY", "strength": abs(z) / threshold}
return None
def signal_bollinger(self, window: int = 20, n_std: float = 2.0) -> dict | None:
"""Bollinger band breakout signal."""
if len(self._prices) < window:
return None
prices = list(self._prices)
recent = prices[-window:]
sma = np.mean(recent)
std = np.std(recent, ddof=1)
if std <= 0:
return None
cur = prices[-1]
if cur > sma + n_std * std:
return {"signal": "BUY", "strength": (cur - sma - n_std * std) / std}
elif cur < sma - n_std * std:
return {"signal": "SELL", "strength": (sma - n_std * std - cur) / std}
return None
def signal_trend(self, window: int = 10, threshold: float = 0.7) -> dict | None:
"""Directional trend strength signal."""
if len(self._prices) < window:
return None
prices = list(self._prices)
up = sum(1 for i in range(-window + 1, 0) if prices[i + 1] > prices[i])
ratio = up / (window - 1)
if ratio >= threshold:
return {"signal": "BUY", "strength": ratio}
elif ratio <= 1.0 - threshold:
return {"signal": "SELL", "strength": 1.0 - ratio}
return None
def signal_vwap_deviation(self, window: int = 20, threshold: float = 1.0) -> dict | None:
"""VWAP deviation signal (mean-reverting)."""
if len(self._prices) < window:
return None
prices = list(self._prices)
prior = prices[-(window + 1):-1]
cur = prices[-1]
vwap = np.mean(prior)
std = np.std(prior, ddof=1)
if std <= 0:
return None
dev = (cur - vwap) / std
if dev > threshold:
return {"signal": "SELL", "strength": dev / threshold}
elif dev < -threshold:
return {"signal": "BUY", "strength": abs(dev) / threshold}
return None