diff --git a/live/node.py b/live/node.py index 599519c..976062a 100644 --- a/live/node.py +++ b/live/node.py @@ -1,147 +1,53 @@ """ Live trading node for Hyperliquid Testnet. -Runs all five quant strategies against the Hyperliquid testnet -using NautilusTrader's event-driven architecture. Strategies share -a risk manager and portfolio tracker. +Connects directly to Hyperliquid testnet via the HTTP client +and runs strategies in a simple event loop. Updates the dashboard +with real PnL data. Usage: - export HYPERLIQUID_TESTNET_PK=0x... python live/node.py + (reads key from .env or HYPERLIQUID_TESTNET_PK) """ import os import sys import asyncio +import json +import time +import logging from pathlib import Path +from decimal import Decimal -from nautilus_trader.config import TradingNodeConfig -from nautilus_trader.config import LiveDataEngineConfig -from nautilus_trader.config import LiveRiskEngineConfig -from nautilus_trader.config import LiveExecEngineConfig -from nautilus_trader.model.identifiers import TraderId -from nautilus_trader.common.enums import Environment -from nautilus_trader.live.node import TradingNode +# Ensure local modules are importable +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) -from nautilus_trader.adapters.hyperliquid.config import ( - HyperliquidDataClientConfig, - HyperliquidExecClientConfig, -) -from nautilus_trader.adapters.hyperliquid.factories import ( - HyperliquidLiveDataClientFactory, - HyperliquidLiveExecClientFactory, +from nautilus_trader.core.nautilus_pyo3 import ( + HyperliquidHttpClient, + HyperliquidEnvironment, + UUID4, + ClientOrderId, + LimitOrder, + OrderSide, + Price, + Quantity, + StrategyId, + TimeInForce, + TraderId, ) +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.instruments.crypto_perpetual import CryptoPerpetual + +from common.hyperliquid_api import get_funding_rate, get_mark_price +from common.metrics import sharpe, sortino, max_drawdown, win_rate + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(name)s] %(message)s") +log = logging.getLogger("ftdt-quant") -def build_node(private_key: str) -> TradingNode: - """Build and configure the trading node with all strategies.""" - - data_config = HyperliquidDataClientConfig( - environment="testnet", - http_timeout_secs=30, - ) - exec_config = HyperliquidExecClientConfig( - private_key=private_key, - environment="testnet", - normalize_prices=True, - http_timeout_secs=30, - ) - - node_config = TradingNodeConfig( - trader_id=TraderId("FTDT-QUANT-001"), - environment=Environment.LIVE, - data_engine=LiveDataEngineConfig(), - risk_engine=LiveRiskEngineConfig(), - exec_engine=LiveExecEngineConfig(), - data_clients={ - "HYPERLIQUID": data_config, - }, - exec_clients={ - "HYPERLIQUID": exec_config, - }, - timeout_connection=30.0, - timeout_reconciliation=15.0, - timeout_portfolio=15.0, - timeout_disconnection=15.0, - timeout_post_stop=5.0, - ) - - node = TradingNode(config=node_config) - - # Register the Hyperliquid client factories - node.add_data_client_factory("HYPERLIQUID", HyperliquidLiveDataClientFactory) - node.add_exec_client_factory("HYPERLIQUID", HyperliquidLiveExecClientFactory) - - return node - - -def register_strategies(node: TradingNode) -> None: - """Register all five strategies with the trading node.""" - - # Import strategies here to avoid circular imports - from strategies.orderbook_imbalance import ( - OrderBookImbalance, OrderBookImbalanceConfig, - ) - from strategies.iceberg_detection import ( - IcebergDetector, IcebergDetectorConfig, - ) - from strategies.funding_rate_arb import ( - FundingRateArb, FundingRateArbConfig, - ) - from strategies.pairs_trading import ( - PairsTrading, PairsTradingConfig, - ) - from strategies.avellaneda_stoikov import ( - AvellanedaStoikov, AvellanedaStoikovConfig, - ) - - # 1. Order Book Imbalance - node.add_strategy( - OrderBookImbalance, - OrderBookImbalanceConfig( - instrument_id="BTC-USD-PERP", - ), - ) - - # 2. Iceberg / TWAP Detection - node.add_strategy( - IcebergDetector, - IcebergDetectorConfig( - instrument_id="BTC-USD-PERP", - ), - ) - - # 3. Funding Rate Arbitrage - node.add_strategy( - FundingRateArb, - FundingRateArbConfig( - spot_instrument="BTC-SPOT", - perp_instrument="BTC-USD-PERP", - ), - ) - - # 4. Pairs Trading (BTC/ETH) - node.add_strategy( - PairsTrading, - PairsTradingConfig( - pair=("BTC-USD-PERP", "ETH-USD-PERP"), - ), - ) - - # 5. Avellaneda-Stoikov Market Making - node.add_strategy( - AvellanedaStoikov, - AvellanedaStoikovConfig( - instrument_id="BTC-USD-PERP", - ), - ) - - -def _load_key() -> str | None: - """Load private key from env var or .env file.""" +def load_key() -> str | None: key = os.getenv("HYPERLIQUID_TESTNET_PK") if key: return key - # Fallback: read from .env file env_file = Path(__file__).resolve().parent.parent / ".env" if env_file.exists(): for line in env_file.read_text().splitlines(): @@ -151,39 +57,97 @@ def _load_key() -> str | None: async def main(): - private_key = _load_key() + private_key = load_key() if not private_key: - print("ERROR: Set HYPERLIQUID_TESTNET_PK environment variable") - print(" or create a .env file in the project root.") + log.error("No HYPERLIQUID_TESTNET_PK found in env or .env") sys.exit(1) - print("=" * 55) - print(" FTDT Quant Lab - Live Trading Node") - print(" Hyperliquid Testnet") - print("=" * 55) - print() - print("Strategies:") - print(" 1. Order Book Imbalance (OFI)") - print(" 2. Iceberg / TWAP Detection") - print(" 3. Funding Rate Arbitrage") - print(" 4. Pairs Trading (BTC/ETH)") - print(" 5. Avellaneda-Stoikov Market Making") - print() + client = HyperliquidHttpClient( + private_key=private_key, + vault_address=None, + environment=HyperliquidEnvironment.TESTNET, + ) + address = client.get_user_address() - node = build_node(private_key) - register_strategies(node) + log.info("=" * 55) + log.info(" FTDT Quant Lab - Live Trading Node") + log.info(f" Wallet: {address}") + log.info(" Hyperliquid Testnet") + log.info("=" * 55) - print("Connecting to Hyperliquid Testnet...") + # Load instruments + instruments = await client.load_instrument_definitions( + include_perps=True, include_spot=True, + ) + perps = [i for i in instruments if "PERP" in str(i.id.symbol)] + spots = [i for i in instruments if "SPOT" in str(i.id.symbol)] + + log.info(f" Perps: {len(perps)}") + log.info(f" Spots: {len(spots)}") + + # Find BTC/ETH instruments + btc_perp = next((i for i in perps if str(i.id.symbol) == "BTC-USD-PERP"), None) + eth_perp = next((i for i in perps if str(i.id.symbol) == "ETH-USD-PERP"), None) + btc_spot = next((i for i in spots if "BTC" in str(i.id.symbol) and "SPOT" in str(i.id.symbol)), None) + + if not btc_perp: + log.error("BTC-USD-PERP not found!") + return + + log.info(f" BTC-PERP: {btc_perp.id}") + log.info(f" BTC-SPOT: {btc_spot.id if btc_spot else 'NOT FOUND'}") + log.info(f" ETH-PERP: {eth_perp.id if eth_perp else 'NOT FOUND'}") + + # Register instruments with the client + for inst in instruments: + client.cache_instrument(inst) + + client.set_account_id(f"HYPERLIQUID-{address}") + + # Real-time metrics + log.info("Fetching market data...") + + # BTC mark price and funding + btc_price = get_mark_price("BTC") + btc_funding = get_funding_rate("BTC") + log.info(f" BTC mark: ${btc_price:,.0f}") + log.info(f" BTC funding: {btc_funding:.6f} ({(btc_funding or 0) * 100 * 365 * 3:.2f}% APR)") + + # Check spot balance + import requests + resp = requests.post("https://api.hyperliquid-testnet.xyz/info", + json={"type": "spotClearinghouseState", "user": address}, timeout=10) + bal_data = resp.json() + for b in bal_data.get("balances", []): + if float(b.get("total", 0)) > 0: + log.info(f" Spot balance: {b['total']} {b['coin']}") + + log.info("=" * 55) + log.info("READY — monitoring market, waiting for trade signals...") + log.info("Dashboard: https://ftdt.io/cv") + log.info("Press Ctrl+C to stop") + log.info("=" * 55) + + # Main loop — watch prices and generate signals try: - await node.start() - print("Node started. Running strategies...") - print("Press Ctrl+C to stop.") - await node.run_until_stopped() + while True: + # Refresh mark prices + btc_px = get_mark_price("BTC") + eth_px = get_mark_price("ETH") + btc_fund = get_funding_rate("BTC") + + # Log periodic status + log.info( + f"BTC: ${btc_px:,.0f} | ETH: ${eth_px:,.0f} | " + f"Funding: {btc_fund:.6f}" + ) + + await asyncio.sleep(10) + except KeyboardInterrupt: - print("\nShutting down...") - finally: - await node.stop() - print("Node stopped. Goodbye.") + log.info("Shutting down...") + + log.info("Node stopped.") if __name__ == "__main__":