Wire up real Hyperliquid integration and funding rate API

Replaced the placeholder live node with a proper NautilusTrader
TradingNode that connects to Hyperliquid Testnet using the
official adapter. Added:

- common/hyperliquid_api.py: direct REST calls to Hyperliquid's
  info endpoint for funding rates, predicted fundings, and
  asset contexts
- backtests/run_backtest.py: CLI runner for strategy backtests
- Updated funding_rate_arb.py to fetch real funding rates
  instead of using a hardcoded placeholder
- Added requests to requirements.txt
This commit is contained in:
ramseshk
2026-08-03 11:37:47 +00:00
parent b59dcc3629
commit c1da0cbe65
5 changed files with 366 additions and 25 deletions
+90
View File
@@ -0,0 +1,90 @@
"""
Quick backtest runner for strategy validation.
Runs any strategy against historical bar data to check basic
logic before deploying live. Uses NautilusTrader's BacktestEngine.
Usage:
python backtests/run_backtest.py --strategy ofi --bars data/BTC-1h.parquet
"""
import argparse
import asyncio
from pathlib import Path
from nautilus_trader.config import BacktestEngineConfig
from nautilus_trader.config import BacktestDataConfig
from nautilus_trader.config import BacktestVenueConfig
from nautilus_trader.model.data import BarType
from nautilus_trader.model.identifiers import InstrumentId, Venue
from nautilus_trader.backtest.node import BacktestNode
STRATEGIES = {
"ofi": "strategies.orderbook_imbalance:OrderBookImbalanceConfig",
"iceberg": "strategies.iceberg_detection:IcebergDetectorConfig",
"funding_arb": "strategies.funding_rate_arb:FundingRateArbConfig",
"pairs": "strategies.pairs_trading:PairsTradingConfig",
"avellaneda": "strategies.avellaneda_stoikov:AvellanedaStoikovConfig",
}
async def run_backtest(strategy_name: str, data_path: str) -> None:
"""Run a single strategy backtest."""
if strategy_name not in STRATEGIES:
print(f"Unknown strategy: {strategy_name}")
print(f"Options: {list(STRATEGIES.keys())}")
return
config_path = STRATEGIES[strategy_name]
# Basic backtest config — swap these for real data
engine_config = BacktestEngineConfig()
venue_config = BacktestVenueConfig(
name="HYPERLIQUID",
oms_type="NETTING",
account_type="MARGIN",
starting_balances=["100000 USDC"],
)
data_config = BacktestDataConfig(
catalog_path=str(Path(data_path).parent),
data_cls="nautilus_trader.model.data.Bar",
catalog_fs_protocol="file",
bar_type=BarType.from_str("BTC-USD-PERP-1-HOUR-LAST-INTERNAL"),
instrument_id=InstrumentId.from_str("BTC-USD-PERP.HYPERLIQUID"),
start_time=None,
end_time=None,
)
node = BacktestNode(
config=engine_config,
venue_configs=[venue_config],
data_configs=[data_config],
)
node.add_strategy(config_path=config_path)
await node.run()
node.dispose()
def main():
parser = argparse.ArgumentParser(description="FTDT Quant Lab - Backtest Runner")
parser.add_argument(
"--strategy", "-s",
choices=list(STRATEGIES.keys()),
required=True,
help="Strategy to backtest",
)
parser.add_argument(
"--data", "-d",
default="data/BTC-1h.parquet",
help="Path to bar data (parquet format)",
)
args = parser.parse_args()
asyncio.run(run_backtest(args.strategy, args.data))
if __name__ == "__main__":
main()
+75
View File
@@ -0,0 +1,75 @@
"""
Hyperliquid API utilities.
Direct REST calls to Hyperliquid info endpoint for data
not yet covered by the NautilusTrader adapter (funding rates,
predicted fundings, asset contexts).
"""
import requests
from typing import Any
TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
MAINNET_API = "https://api.hyperliquid.xyz/info"
def _post(api_url: str, payload: dict) -> Any:
resp = requests.post(api_url, json=payload, timeout=10)
resp.raise_for_status()
return resp.json()
def get_asset_contexts(testnet: bool = True) -> list[dict]:
"""
Fetch asset contexts including current funding rates.
Returns list of per-asset dicts with keys:
funding, openInterest, markPx, oraclePx, premium, dayNtlVlm, etc.
"""
api = TESTNET_API if testnet else MAINNET_API
data = _post(api, {"type": "metaAndAssetCtxs"})
# data[0] = universe, data[1] = asset contexts
if isinstance(data, list) and len(data) >= 2:
return data[1]
return []
def get_funding_rate(asset_name: str, testnet: bool = True) -> float | None:
"""
Get the current funding rate for a specific asset.
Funding is paid every 8 hours. Positive = longs pay shorts.
"""
ctxs = get_asset_contexts(testnet=testnet)
for ctx in ctxs:
if isinstance(ctx, dict) and ctx.get("name") == asset_name.upper():
funding_str = ctx.get("funding", "0")
return float(funding_str)
return None
def get_all_funding_rates(testnet: bool = True) -> dict[str, float]:
"""Get funding rates for all assets on Hyperliquid."""
ctxs = get_asset_contexts(testnet=testnet)
rates = {}
for ctx in ctxs:
if isinstance(ctx, dict):
name = ctx.get("name", "")
funding_str = ctx.get("funding", "0")
if name:
rates[name] = float(funding_str)
return rates
def get_predicted_funding(asset_name: str, testnet: bool = True) -> float | None:
"""
Get the predicted funding rate for the next interval.
Uses the predictedFundings endpoint.
"""
api = TESTNET_API if testnet else MAINNET_API
data = _post(api, {"type": "predictedFundings"})
if isinstance(data, list):
for item in data:
if isinstance(item, dict) and item.get("name") == asset_name.upper():
# Return the Hyperliquid-specific prediction
predicted = item.get("funding", "0")
return float(predicted)
return None
+145 -5
View File
@@ -1,17 +1,145 @@
""" """
Live trading node for Hyperliquid Testnet. Live trading node for Hyperliquid Testnet.
Runs all five strategies concurrently with shared risk management. Runs all five quant strategies against the Hyperliquid testnet
using NautilusTrader's event-driven architecture. Strategies share
a risk manager and portfolio tracker.
Usage:
export HYPERLIQUID_TESTNET_PK=0x...
python live/node.py
""" """
import asyncio
import os import os
import sys import sys
import asyncio
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
from nautilus_trader.adapters.hyperliquid.config import (
HyperliquidDataClientConfig,
HyperliquidExecClientConfig,
)
from nautilus_trader.adapters.hyperliquid.factories import (
HyperliquidLiveDataClientFactory,
HyperliquidLiveExecClientFactory,
)
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",
),
)
async def main(): async def main():
private_key = os.getenv("HYPERLIQUID_TESTNET_PK") private_key = os.getenv("HYPERLIQUID_TESTNET_PK")
if not private_key: if not private_key:
print("Set HYPERLIQUID_TESTNET_PK environment variable") print("ERROR: Set HYPERLIQUID_TESTNET_PK environment variable")
print(" export HYPERLIQUID_TESTNET_PK=0x...")
sys.exit(1) sys.exit(1)
print("=" * 55) print("=" * 55)
@@ -26,9 +154,21 @@ async def main():
print(" 4. Pairs Trading (BTC/ETH)") print(" 4. Pairs Trading (BTC/ETH)")
print(" 5. Avellaneda-Stoikov Market Making") print(" 5. Avellaneda-Stoikov Market Making")
print() print()
node = build_node(private_key)
register_strategies(node)
print("Connecting to Hyperliquid Testnet...") print("Connecting to Hyperliquid Testnet...")
# TODO: Full Nautilus TradingNode integration try:
print("Ready.") await node.start()
print("Node started. Running strategies...")
print("Press Ctrl+C to stop.")
await node.run_until_stopped()
except KeyboardInterrupt:
print("\nShutting down...")
finally:
await node.stop()
print("Node stopped. Goodbye.")
if __name__ == "__main__": if __name__ == "__main__":
+2 -3
View File
@@ -1,10 +1,9 @@
# Nautilus Trader # Core
nautilus-trader>=1.210.0 nautilus-trader>=1.210.0
# Data & Math
numpy>=1.24.0 numpy>=1.24.0
pandas>=2.0.0 pandas>=2.0.0
pyyaml>=6.0 pyyaml>=6.0
requests>=2.28.0
# Visualization # Visualization
matplotlib>=3.7.0 matplotlib>=3.7.0
+54 -17
View File
@@ -8,59 +8,95 @@ longs pay shorts. This strategy:
2. Goes SHORT perp (collects funding) 2. Goes SHORT perp (collects funding)
3. Maintains delta neutrality 3. Maintains delta neutrality
The profit comes from funding, not price direction. The profit comes from funding, not price direction. The strategy
fetches real funding rates from Hyperliquid's API every bar
and enters/exits based on the rate crossing configurable thresholds.
""" """
from nautilus_trader.trading.strategy import Strategy from nautilus_trader.trading.strategy import Strategy
from nautilus_trader.config import StrategyConfig from nautilus_trader.config import StrategyConfig
from common.hyperliquid_api import get_funding_rate, get_predicted_funding
class FundingRateArbConfig(StrategyConfig, frozen=True): class FundingRateArbConfig(StrategyConfig, frozen=True):
spot_instrument: str spot_instrument: str
perp_instrument: str perp_instrument: str
min_funding_rate: float = 0.0001 min_funding_rate: float = 0.0001 # 0.01% annualized ~ 10.95% APR
rebalance_threshold: float = 0.05 rebalance_threshold: float = 0.05 # 5% PnL deviation triggers rebalance
position_size: float = 0.01 position_size: float = 0.01 # BTC
use_predicted: bool = True # Use predicted funding rate
testnet: bool = True
class FundingRateArb(Strategy): class FundingRateArb(Strategy):
""" """
Delta-neutral funding rate carry trade. Delta-neutral funding rate carry trade.
Key idea: funding rate IS the edge. Stay neutral, collect Key concept: the funding rate IS the edge.
the payments. Direction doesn't matter — neutrality does.
Entry: when funding rate > min_funding_rate AND no position
Exit: when funding rate drops below half the entry threshold
""" """
def __init__(self, config: FundingRateArbConfig) -> None: def __init__(self, config: FundingRateArbConfig) -> None:
super().__init__(config) super().__init__(config)
self.config = config self.config = config
self.position_open = False self.position_open = False
self.bars_elapsed = 0
def on_start(self) -> None: def on_start(self) -> None:
bar_type = f"{self.config.perp_instrument}-1-MINUTE-LAST-INTERNAL" bar_type = f"{self.config.perp_instrument}-1-MINUTE-LAST-INTERNAL"
self.subscribe_bars(bar_type) self.subscribe_bars(bar_type)
self.log.info( self.log.info(
f"Funding arb: {self.config.spot_instrument} / {self.config.perp_instrument}" f"Funding arb started: "
f"{self.config.spot_instrument} / {self.config.perp_instrument} "
f"(min_rate={self.config.min_funding_rate:.4%}, "
f"size={self.config.position_size})"
) )
def on_bar(self, bar) -> None: def on_bar(self, bar) -> None:
funding_rate = self._get_funding_rate() # Check funding every 5 bars to avoid hammering the API
if funding_rate is None: self.bars_elapsed += 1
if self.bars_elapsed % 5 != 0:
return return
spot_pos = self.portfolio.net_position(self.config.spot_instrument) # Fetch real funding rate from Hyperliquid
asset = self._extract_asset(self.config.perp_instrument)
if self.config.use_predicted:
funding_rate = get_predicted_funding(asset, testnet=self.config.testnet)
else:
funding_rate = get_funding_rate(asset, testnet=self.config.testnet)
if funding_rate is None:
return # API call failed, skip this bar
spot_pos = float(self.portfolio.net_position(self.config.spot_instrument))
# Entry condition: funding rate is attractive and we have no position
if funding_rate > self.config.min_funding_rate and spot_pos == 0: if funding_rate > self.config.min_funding_rate and spot_pos == 0:
self._open() self.log.info(
f"Entering funding arb: rate={funding_rate:.6f} "
f"(>{self.config.min_funding_rate:.6f})"
)
self._open_arb()
self.position_open = True self.position_open = True
# Exit condition: funding rate no longer worth the risk
elif funding_rate < self.config.min_funding_rate / 2 and self.position_open: elif funding_rate < self.config.min_funding_rate / 2 and self.position_open:
self._close() self.log.info(
f"Closing funding arb: rate={funding_rate:.6f} "
f"(<{self.config.min_funding_rate / 2:.6f})"
)
self._close_arb()
self.position_open = False self.position_open = False
def _get_funding_rate(self) -> float | None: def _extract_asset(self, instrument: str) -> str:
# TODO: fetch from Hyperliquid API """Extract asset name from instrument ID (e.g. BTC-USD-PERP -> BTC)."""
return 0.0001 return instrument.split("-")[0]
def _open(self) -> None: def _open_arb(self) -> None:
"""Long spot, short perp — delta neutral."""
self.submit_order(self.order_factory.market( self.submit_order(self.order_factory.market(
instrument_id=self.config.spot_instrument, instrument_id=self.config.spot_instrument,
order_side="BUY", order_side="BUY",
@@ -72,6 +108,7 @@ class FundingRateArb(Strategy):
quantity=self.config.position_size, quantity=self.config.position_size,
)) ))
def _close(self) -> None: def _close_arb(self) -> None:
"""Close both legs."""
self.close_all_positions(self.config.spot_instrument) self.close_all_positions(self.config.spot_instrument)
self.close_all_positions(self.config.perp_instrument) self.close_all_positions(self.config.perp_instrument)