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

125 lines
4.3 KiB
Python

"""
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())