""" Hyperliquid execution provider — live and paper trading via NautilusTrader. Live mode: Submits real orders to Hyperliquid testnet/mainnet via REST. Paper mode: Tracks virtual positions, simulates fills with realistic slippage. Uses the hyperliquid-python-sdk for signed order submission. """ from __future__ import annotations import asyncio import logging import time from dataclasses import dataclass, field import requests from nautilus_trader.model.enums import OrderSide, OrderType, TimeInForce from nautilus_trader.model.identifiers import ClientOrderId, InstrumentId, VenueOrderId from nautilus_trader.model.objects import Price, Quantity logger = logging.getLogger(__name__) TESTNET_API = "https://api.hyperliquid-testnet.xyz/info" MAINNET_API = "https://api.hyperliquid.xyz/info" @dataclass class SimulatedPosition: coin: str quantity: float entry_price: float side: str # BUY or SELL fee_paid: float = 0.0 pnl: float = 0.0 @dataclass class SimulatedOrder: cloid: str coin: str side: str quantity: float price: float timestamp: float = field(default_factory=time.time) filled: bool = False fill_price: float = 0.0 fee: float = 0.0 pnl: float = 0.0 class HyperliquidExecutionProvider: """Live trading via Hyperliquid SDK + REST API.""" def __init__( self, private_key: str, testnet: bool = True, vault_address: str | None = None, ): self._pk = private_key self._vault = vault_address self._testnet = testnet self._api_url = TESTNET_API if testnet else MAINNET_API self._exchange = None self._info = None self._address: str | None = None def _ensure_sdk(self): if self._exchange is None: from hyperliquid.exchange import Exchange from hyperliquid.info import Info self._info = Info(self._api_url, skip_ws=True) self._exchange = Exchange( wallet=self._info, private_key=self._pk, vault_address=self._vault, account_address=None, is_testnet=self._testnet, ) meta = self._info.meta() if meta and "universe" in meta: logger.info("HL SDK initialized: %d assets", len(meta.get("universe", []))) @property def address(self) -> str | None: if not self._address: self._ensure_sdk() if self._exchange: self._address = self._exchange.wallet.address return self._address def submit_limit_order( self, coin: str, side: str, # "BUY" or "SELL" size: float, price: float, post_only: bool = True, reduce_only: bool = False, ) -> dict | None: """Submit a limit order. Returns order response or None on failure.""" self._ensure_sdk() try: is_buy = side.upper() == "BUY" result = self._exchange.order( name=coin, is_buy=is_buy, sz=size, limit_px=price, order_type={"limit": {"tif": "Gtc" if post_only else "Ioc"}}, reduce_only=reduce_only, ) logger.info("Order submitted: %s %s %.6f @ %.1f → %s", side, coin, size, price, result) return result except Exception as e: logger.error("Order failed: %s %s: %s", side, coin, e) return None def cancel_order(self, coin: str, cloid: str) -> bool: """Cancel an order by client order ID.""" self._ensure_sdk() try: self._exchange.cancel(coin, cloid) return True except Exception as e: logger.warning("Cancel failed for %s/%s: %s", coin, cloid, e) return False def cancel_all(self, coin: str | None = None): """Cancel all open orders, optionally filtered by coin.""" self._ensure_sdk() try: self._exchange.cancel_all(coin) except Exception as e: logger.warning("Cancel all failed: %s", e) def get_positions(self) -> list[dict]: """Get open positions for the wallet.""" if not self.address: return [] resp = requests.post( self._api_url, json={"type": "clearinghouseState", "user": self.address}, timeout=10, ) if resp.status_code != 200: return [] data = resp.json() positions = [] for pos in data.get("assetPositions", []): pos_type = pos.get("position", {}) if pos_type: coin = pos_type.get("coin", "") szi = float(pos_type.get("szi", 0)) if coin and abs(szi) > 0: positions.append({ "coin": coin, "size": szi, "entry_px": float(pos_type.get("entryPx", 0)), "unrealized_pnl": float(pos_type.get("unrealizedPnl", 0)), }) return positions def get_open_orders(self) -> list[dict]: if not self.address: return [] resp = requests.post( self._api_url, json={"type": "openOrders", "user": self.address}, timeout=10, ) if resp.status_code != 200: return [] return resp.json() class PaperExecutionProvider: """Paper trading — simulated fills against real Hyperliquid mark prices.""" def __init__( self, maker_fee: float = 0.0002, taker_fee: float = 0.0005, slippage_bps: float = 1.0, ): self.maker_fee = maker_fee self.taker_fee = taker_fee self.slippage_bps = slippage_bps self.positions: dict[str, SimulatedPosition] = {} self.orders: dict[str, SimulatedOrder] = {} self.trades: list[dict] = [] self._counter = 0 def submit( self, coin: str, side: str, size: float, price: float, fee_model: str = "taker", mark_price: float | None = None, ) -> str: """Submit a simulated order. Returns client order ID.""" self._counter += 1 cloid = f"paper-{self._counter}" order = SimulatedOrder(cloid=cloid, coin=coin, side=side, quantity=size, price=price) self.orders[cloid] = order # Simulate immediate fill at mark price or limit price fill_price = mark_price if mark_price and mark_price > 0 else price fee_rate = self.maker_fee if fee_model == "maker" else self.taker_fee # Apply slippage slip = fill_price * self.slippage_bps / 10000 effective_px = fill_price + slip if side.upper() == "BUY" else fill_price - slip fee = size * effective_px * fee_rate order.filled = True order.fill_price = effective_px order.fee = fee # Update position pos = self.positions.get(coin) if pos and pos.side != side: # Closing trade — calculate PnL pnl = (effective_px - pos.entry_price) * min(size, abs(pos.quantity)) if pos.side == "SELL": pnl = -pnl order.pnl = pnl pos.quantity -= size pos.fee_paid += fee pos.pnl += pnl if abs(pos.quantity) < 1e-8: del self.positions[coin] else: # Opening or adding to position if coin not in self.positions: self.positions[coin] = SimulatedPosition( coin=coin, quantity=size, entry_price=effective_px, side=side ) else: pos.quantity += size pos.entry_price = (pos.entry_price * (pos.quantity - size) + effective_px * size) / pos.quantity trade = { "cloid": cloid, "coin": coin, "side": side, "size": size, "price": effective_px, "fee": round(fee, 6), "pnl": round(order.pnl, 4), "timestamp": time.time(), } self.trades.append(trade) logger.debug("Paper fill: %s %s %.6f @ %.1f | pnl=%.4f fee=%.6f", side, coin, size, effective_px, order.pnl, fee) return cloid def cancel(self, cloid: str) -> bool: if cloid in self.orders and not self.orders[cloid].filled: del self.orders[cloid] return True return False def get_pnl(self) -> float: return sum(p.pnl for p in self.positions.values()) + sum( t.get("pnl", 0) for t in self.trades if t.get("pnl", 0) > 0 )