f5ffe4baee
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.
125 lines
4.3 KiB
Python
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())
|