Files
ftdt-quant-lab/strategies/funding_arb.py
T
ramseshk 70d43fefe0 Complete Funding Rate Arb: real API data for live + paper
New module: strategies/funding_arb.py
  - get_funding_rates(): fetches predicted funding from Hyperliquid
    Uses metaAndAssetCtxs (primary) + predictedFundings (fallback)
  - funding_arb_signal(): generates entry/exit signals
    Entry: |annual_rate| > threshold (3% testnet, 5% mainnet)
    Exit:  rate drops below 2% or flips sign
  - 30s cache to avoid rate-limiting

Live node:
  - Replaced proxy-based funding (20-period return) with real API
  - Calls get_funding_rates(use_testnet=True) every compute_signals()
  - Lowered threshold to 3% APR for testnet (lower liquidity)

Paper trader:
  - Replaced manual funding calc with unified funding_arb_signal()
  - Proper entry/exit logic with position tracking
  - 5% APR threshold for mainnet data

Current rates: BTC +0.87% APR, ETH -0.82% APR
(Arb fires when rates exceed threshold during volatility)
2026-08-05 07:09:29 +00:00

144 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Funding Rate Arb — Complete Implementation.
Strategy:
Funding rates on perpetual futures represent the cost of leverage.
When funding is positive (longs pay shorts), short the perp and collect.
When funding is negative (shorts pay longs), go long the perp and collect.
The Hyperliquid API provides predicted funding rates via:
- predictedFundings: current predicted rate for each interval
- metaAndAssetCtxs: asset context including current funding
Entry: |annualized_funding_rate| > threshold (5-10% APR)
Exit: |annualized_funding_rate| < threshold/2 or after N hours
Size: scales with rate — higher rate = larger size
"""
import requests
import time
import math
from typing import Optional
MAINNET_API = "https://api.hyperliquid.xyz/info"
TESTNET_API = "https://api.hyperliquid-testnet.xyz/info"
# Cache funding rates to avoid hitting API every tick
_funding_cache: dict = {}
_last_funding_fetch: float = 0
FUNDING_CACHE_TTL = 30 # seconds
def get_funding_rates(use_testnet: bool = False) -> dict[str, float]:
"""
Fetch current predicted funding rates for supported coins.
Uses Hyperliquid's predictedFundings endpoint which returns
the current projected funding rate for each perpetual.
Returns: {coin: funding_rate_annualized}
"""
global _funding_cache, _last_funding_fetch
now = time.time()
if now - _last_funding_fetch < FUNDING_CACHE_TTL and _funding_cache:
return _funding_cache
api = TESTNET_API if use_testnet else MAINNET_API
rates: dict[str, float] = {}
# Method 1: Try metaAndAssetCtxs (most reliable)
try:
r = requests.post(MAINNET_API, json={"type": "metaAndAssetCtxs"}, timeout=10)
data = r.json()
if isinstance(data, list) and len(data) >= 2:
universe = data[0].get("universe", [])
ctxs = data[1]
for i, u in enumerate(universe):
name = u.get("name", "")
if name in ("BTC", "ETH", "HYPE", "VVV", "SOL"):
try:
funding = float(ctxs[i].get("funding", 0))
# funding is the 8h rate; annualize: × 365 × (24/8) = × 1095
annual = funding * 1095
rates[name] = annual
except (IndexError, ValueError, TypeError):
pass
except Exception:
pass
# Method 2: Fallback to predictedFundings
if not rates:
try:
r = requests.post(MAINNET_API, json={"type": "predictedFundings"}, timeout=10)
data = r.json()
if isinstance(data, list):
for coin_entry in data:
coin = coin_entry[0]
if coin not in ("BTC", "ETH", "HYPE", "VVV", "SOL"):
continue
for venue_entry in coin_entry[1]:
venue = venue_entry[0]
info = venue_entry[1]
rate_str = info.get("fundingRate", "0")
try:
rate = float(rate_str)
except (ValueError, TypeError):
rate = 0.0
interval_hours = info.get("fundingIntervalHours", 8)
annual = rate * (365 * 24 / interval_hours)
if coin not in rates or "HlPerp" in venue:
rates[coin] = annual
except Exception:
pass
_funding_cache = rates
_last_funding_fetch = now
return rates
def funding_arb_signal(
coin: str = "BTC",
apr_threshold: float = 0.05, # 5% APR minimum
apr_exit: float = 0.02, # 2% APR to exit
current_position: int = 0,
) -> dict:
"""
Generate funding rate arbitrage signal.
Args:
coin: Ticker to check.
apr_threshold: Minimum annualized funding rate to enter (>0.05 = 5%).
apr_exit: Rate below which to exit position.
current_position: -1 (short), 0 (none), +1 (long).
Returns:
dict with signal, rate, annual_apr, reason.
"""
rates = get_funding_rates()
annual = rates.get(coin, 0)
rate_8h = annual / 1095 # de-annualize
signal = 0
reason = ""
if abs(annual) > apr_threshold and current_position == 0:
signal = -1 if annual > 0 else +1 # short if funding positive, long if negative
reason = f"funding_{annual*100:.1f}pct_apr"
elif current_position != 0:
# Exit condition: rate has dropped below exit threshold
if abs(annual) < apr_exit:
signal = -current_position
reason = f"exit_funding_{annual*100:.2f}pct_apr"
# Also exit if funding flips sign (we'd be paying instead of collecting)
elif (current_position == -1 and annual < 0) or (current_position == 1 and annual > 0):
signal = -current_position
reason = f"exit_funding_flipped_{annual*100:.2f}pct_apr"
return {
"signal": signal,
"rate_8h": rate_8h,
"annual_apr": annual,
"reason": reason,
}