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
+54 -17
View File
@@ -8,59 +8,95 @@ longs pay shorts. This strategy:
2. Goes SHORT perp (collects funding)
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.config import StrategyConfig
from common.hyperliquid_api import get_funding_rate, get_predicted_funding
class FundingRateArbConfig(StrategyConfig, frozen=True):
spot_instrument: str
perp_instrument: str
min_funding_rate: float = 0.0001
rebalance_threshold: float = 0.05
position_size: float = 0.01
min_funding_rate: float = 0.0001 # 0.01% annualized ~ 10.95% APR
rebalance_threshold: float = 0.05 # 5% PnL deviation triggers rebalance
position_size: float = 0.01 # BTC
use_predicted: bool = True # Use predicted funding rate
testnet: bool = True
class FundingRateArb(Strategy):
"""
Delta-neutral funding rate carry trade.
Key idea: funding rate IS the edge. Stay neutral, collect
the payments.
Key concept: the funding rate IS the edge.
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:
super().__init__(config)
self.config = config
self.position_open = False
self.bars_elapsed = 0
def on_start(self) -> None:
bar_type = f"{self.config.perp_instrument}-1-MINUTE-LAST-INTERNAL"
self.subscribe_bars(bar_type)
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:
funding_rate = self._get_funding_rate()
if funding_rate is None:
# Check funding every 5 bars to avoid hammering the API
self.bars_elapsed += 1
if self.bars_elapsed % 5 != 0:
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:
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
# Exit condition: funding rate no longer worth the risk
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
def _get_funding_rate(self) -> float | None:
# TODO: fetch from Hyperliquid API
return 0.0001
def _extract_asset(self, instrument: str) -> str:
"""Extract asset name from instrument ID (e.g. BTC-USD-PERP -> BTC)."""
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(
instrument_id=self.config.spot_instrument,
order_side="BUY",
@@ -72,6 +108,7 @@ class FundingRateArb(Strategy):
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.perp_instrument)