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:
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Hyperliquid instrument catalog — loads perpetual contracts as NT CryptoPerpetual.
|
||||
|
||||
Fetches exchange metadata (universe + asset contexts) from Hyperliquid info API
|
||||
and builds NautilusTrader CryptoPerpetual instrument definitions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
import requests
|
||||
from nautilus_trader.model.identifiers import InstrumentId, Symbol, Venue
|
||||
from nautilus_trader.model.instruments import CryptoPerpetual
|
||||
from nautilus_trader.model.objects import Currency, Price, Quantity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HL_VENUE = Venue("HYPERLIQUID")
|
||||
TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
|
||||
MAINNET_API = "https://api.hyperliquid.xyz/info"
|
||||
|
||||
|
||||
def _hl_meta(testnet: bool = True) -> dict:
|
||||
url = TESTNET_API if testnet else MAINNET_API
|
||||
resp = requests.post(url, json={"type": "metaAndAssetCtxs"}, timeout=15)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
if not isinstance(data, list) or len(data) < 2:
|
||||
raise ValueError("Invalid metaAndAssetCtxs response")
|
||||
return {"universe": data[0].get("universe", []), "contexts": data[1]}
|
||||
|
||||
|
||||
def _to_instrument(asset: dict, ctx: dict | None) -> CryptoPerpetual | None:
|
||||
name = asset.get("name", "")
|
||||
if not name:
|
||||
return None
|
||||
|
||||
symbol_str = f"{name}-USD-PERP"
|
||||
inst_id = InstrumentId(Symbol(symbol_str), HL_VENUE)
|
||||
|
||||
px_ctx = ctx if ctx else {}
|
||||
mark_px = float(px_ctx.get("markPx", 0) or 0)
|
||||
|
||||
step_size = asset.get("szDecimals", 5)
|
||||
size_increment_val = 10 ** -step_size
|
||||
tick_size = asset.get("pxDecimals", 1)
|
||||
price_increment_val = 10 ** -tick_size
|
||||
|
||||
now_ns = int(datetime.now(timezone.utc).timestamp() * 1e9)
|
||||
|
||||
return CryptoPerpetual(
|
||||
instrument_id=inst_id,
|
||||
raw_symbol=Symbol(symbol_str),
|
||||
base_currency=Currency.from_str(name),
|
||||
quote_currency=Currency.from_str("USD"),
|
||||
settlement_currency=Currency.from_str("USD"),
|
||||
is_inverse=False,
|
||||
price_precision=tick_size,
|
||||
size_precision=step_size,
|
||||
price_increment=Price.from_str(str(price_increment_val)),
|
||||
size_increment=Quantity.from_str(str(size_increment_val)),
|
||||
multiplier=Quantity.from_str("1.0"),
|
||||
maker_fee=Decimal("0.0002"),
|
||||
taker_fee=Decimal("0.0005"),
|
||||
max_quantity=Quantity.from_str("10000.0"),
|
||||
min_quantity=Quantity.from_str(str(size_increment_val)),
|
||||
max_notional=None,
|
||||
min_notional=None,
|
||||
max_price=Price.from_str(str(int(mark_px * 10)) if mark_px > 0 else "10000000.0"),
|
||||
min_price=Price.from_str("0.01"),
|
||||
margin_init=Decimal("0.02"),
|
||||
margin_maint=Decimal("0.01"),
|
||||
ts_event=now_ns,
|
||||
ts_init=now_ns,
|
||||
)
|
||||
|
||||
|
||||
class HyperliquidInstrumentCatalog:
|
||||
"""Fetches and caches Hyperliquid perpetual instrument definitions."""
|
||||
|
||||
def __init__(self, testnet: bool = True):
|
||||
self._testnet = testnet
|
||||
self._instruments: dict[str, CryptoPerpetual] = {}
|
||||
|
||||
@property
|
||||
def venue(self) -> Venue:
|
||||
return HL_VENUE
|
||||
|
||||
def load(self, assets: list[str] | None = None) -> dict[str, CryptoPerpetual]:
|
||||
"""Fetch all perps, returning dict keyed by base currency name."""
|
||||
meta = _hl_meta(testnet=self._testnet)
|
||||
universe = meta["universe"]
|
||||
contexts = meta["contexts"]
|
||||
|
||||
for i, asset_info in enumerate(universe):
|
||||
name = asset_info.get("name", "")
|
||||
if not name:
|
||||
continue
|
||||
if assets and name.upper() not in [a.upper() for a in assets]:
|
||||
continue
|
||||
ctx = contexts[i] if i < len(contexts) else None
|
||||
try:
|
||||
inst = _to_instrument(asset_info, ctx)
|
||||
if inst:
|
||||
self._instruments[name] = inst
|
||||
except Exception as e:
|
||||
logger.warning("Skipped instrument %s: %s", name, e)
|
||||
|
||||
logger.info("Loaded %d Hyperliquid instruments", len(self._instruments))
|
||||
return self._instruments
|
||||
|
||||
def get(self, name: str) -> CryptoPerpetual | None:
|
||||
return self._instruments.get(name.upper())
|
||||
|
||||
def all_ids(self) -> list[InstrumentId]:
|
||||
return [inst.id for inst in self._instruments.values()]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._instruments)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._instruments.values())
|
||||
Reference in New Issue
Block a user