Files
ramseshk c1da0cbe65 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
2026-08-03 11:37:47 +00:00

115 lines
4.2 KiB
Python

"""
Funding Rate Arbitrage strategy.
Hyperliquid pays funding every 8 hours. When the rate is positive,
longs pay shorts. This strategy:
1. Goes LONG spot (no funding payments)
2. Goes SHORT perp (collects funding)
3. Maintains delta neutrality
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 # 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 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 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:
# Check funding every 5 bars to avoid hammering the API
self.bars_elapsed += 1
if self.bars_elapsed % 5 != 0:
return
# 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.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.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 _extract_asset(self, instrument: str) -> str:
"""Extract asset name from instrument ID (e.g. BTC-USD-PERP -> BTC)."""
return instrument.split("-")[0]
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",
quantity=self.config.position_size,
))
self.submit_order(self.order_factory.market(
instrument_id=self.config.perp_instrument,
order_side="SELL",
quantity=self.config.position_size,
))
def _close_arb(self) -> None:
"""Close both legs."""
self.close_all_positions(self.config.spot_instrument)
self.close_all_positions(self.config.perp_instrument)