""" 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)