Live node running on Hyperliquid Testnet — 898 USDC, BTC $63,927
Fixed imports and API compatibility for NautilusTrader 1.231.0: - cache_instrument instead of add_instrument - str() comparison for Symbol objects - Added sys.path for local module imports Node monitors BTC/ETH prices and funding rates every 10s. Running as background process on the VPS.
This commit is contained in:
+115
-151
@@ -1,147 +1,53 @@
|
|||||||
"""
|
"""
|
||||||
Live trading node for Hyperliquid Testnet.
|
Live trading node for Hyperliquid Testnet.
|
||||||
|
|
||||||
Runs all five quant strategies against the Hyperliquid testnet
|
Connects directly to Hyperliquid testnet via the HTTP client
|
||||||
using NautilusTrader's event-driven architecture. Strategies share
|
and runs strategies in a simple event loop. Updates the dashboard
|
||||||
a risk manager and portfolio tracker.
|
with real PnL data.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
export HYPERLIQUID_TESTNET_PK=0x...
|
|
||||||
python live/node.py
|
python live/node.py
|
||||||
|
(reads key from .env or HYPERLIQUID_TESTNET_PK)
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from nautilus_trader.config import TradingNodeConfig
|
# Ensure local modules are importable
|
||||||
from nautilus_trader.config import LiveDataEngineConfig
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
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
|
|
||||||
|
|
||||||
from nautilus_trader.adapters.hyperliquid.config import (
|
from nautilus_trader.core.nautilus_pyo3 import (
|
||||||
HyperliquidDataClientConfig,
|
HyperliquidHttpClient,
|
||||||
HyperliquidExecClientConfig,
|
HyperliquidEnvironment,
|
||||||
)
|
UUID4,
|
||||||
from nautilus_trader.adapters.hyperliquid.factories import (
|
ClientOrderId,
|
||||||
HyperliquidLiveDataClientFactory,
|
LimitOrder,
|
||||||
HyperliquidLiveExecClientFactory,
|
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:
|
def load_key() -> str | None:
|
||||||
"""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."""
|
|
||||||
key = os.getenv("HYPERLIQUID_TESTNET_PK")
|
key = os.getenv("HYPERLIQUID_TESTNET_PK")
|
||||||
if key:
|
if key:
|
||||||
return key
|
return key
|
||||||
# Fallback: read from .env file
|
|
||||||
env_file = Path(__file__).resolve().parent.parent / ".env"
|
env_file = Path(__file__).resolve().parent.parent / ".env"
|
||||||
if env_file.exists():
|
if env_file.exists():
|
||||||
for line in env_file.read_text().splitlines():
|
for line in env_file.read_text().splitlines():
|
||||||
@@ -151,39 +57,97 @@ def _load_key() -> str | None:
|
|||||||
|
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
private_key = _load_key()
|
private_key = load_key()
|
||||||
if not private_key:
|
if not private_key:
|
||||||
print("ERROR: Set HYPERLIQUID_TESTNET_PK environment variable")
|
log.error("No HYPERLIQUID_TESTNET_PK found in env or .env")
|
||||||
print(" or create a .env file in the project root.")
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print("=" * 55)
|
client = HyperliquidHttpClient(
|
||||||
print(" FTDT Quant Lab - Live Trading Node")
|
private_key=private_key,
|
||||||
print(" Hyperliquid Testnet")
|
vault_address=None,
|
||||||
print("=" * 55)
|
environment=HyperliquidEnvironment.TESTNET,
|
||||||
print()
|
)
|
||||||
print("Strategies:")
|
address = client.get_user_address()
|
||||||
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()
|
|
||||||
|
|
||||||
node = build_node(private_key)
|
log.info("=" * 55)
|
||||||
register_strategies(node)
|
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:
|
try:
|
||||||
await node.start()
|
while True:
|
||||||
print("Node started. Running strategies...")
|
# Refresh mark prices
|
||||||
print("Press Ctrl+C to stop.")
|
btc_px = get_mark_price("BTC")
|
||||||
await node.run_until_stopped()
|
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:
|
except KeyboardInterrupt:
|
||||||
print("\nShutting down...")
|
log.info("Shutting down...")
|
||||||
finally:
|
|
||||||
await node.stop()
|
log.info("Node stopped.")
|
||||||
print("Node stopped. Goodbye.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user