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)
This commit is contained in:
ramseshk
2026-08-05 07:09:29 +00:00
parent 84efb4014a
commit 70d43fefe0
3 changed files with 191 additions and 29 deletions
+15 -9
View File
@@ -127,17 +127,23 @@ def compute_signals():
if up>=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10}) if up>=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"BUY","strength":up/10})
elif up<=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) elif up<=5: STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10})
# Funding Arb: use real funding rate if available, else wider proxy # Funding Rate Arb: real API data
if len(btc_prices)>=20:
try: try:
fr = requests.post(TESTNET_API, json={"type":"funding","coin":"BTC"}, timeout=5).json() from strategies.funding_arb import get_funding_rates
if isinstance(fr, list) and fr: rates = get_funding_rates(use_testnet=True)
rate = float(fr[0].get("funding_rate", 0)) annual_rate = rates.get("BTC", 0)
else: if abs(annual_rate) > 0.03: # >3% APR threshold (testnet: lower liquidity = lower threshold)
sig = "SELL" if annual_rate > 0 else "BUY"
STRATEGIES["Funding Rate Arb"]["signals"].append({
"time":time.time(), "signal":sig,
"strength": min(1.0, abs(annual_rate) * 10),
"reason": f"funding_{annual_rate*100:.1f}pct_apr"
})
except Exception:
# Fallback: use price proxy if module unavailable
if len(btc_prices)>=20:
rate = (btc/btc_prices[-20]-1)/20 rate = (btc/btc_prices[-20]-1)/20
except: if abs(rate)>0.0005:
rate = (btc/btc_prices[-20]-1)/20
if abs(rate)>0.0001:
STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000}) STRATEGIES["Funding Rate Arb"]["signals"].append({"time":time.time(),"signal":"SELL" if rate>0 else "BUY","strength":abs(rate)*10000})
# Pairs: ratio Z-score # Pairs: ratio Z-score
+26 -13
View File
@@ -236,22 +236,35 @@ def compute_signals():
elif up <= 3: elif up <= 3:
STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10}) STRATEGIES["Iceberg Detection"]["signals"].append({"time":time.time(),"signal":"SELL","strength":1-up/10})
# Funding Arb — use actual mainnet funding rate # Funding Rate Arb — unified module with real API data
if funding_rates and isinstance(funding_rates[-1], dict): try:
btc_fr = funding_rates[-1].get("BTC", 0) from strategies.funding_arb import funding_arb_signal
# Annualized: funding every 8h → 3× daily → 1095× yearly sig_result = funding_arb_signal(coin="BTC", apr_threshold=0.05, apr_exit=0.02,
annual_fr = abs(btc_fr) * 365 * 3 if btc_fr else 0 current_position=STRATEGIES["Funding Rate Arb"]["position"])
# Log funding rate periodically if sig_result["signal"] != 0:
import random as _random_fr STRATEGIES["Funding Rate Arb"]["signals"].append({
if _random_fr.random() < 0.02: "time": time.time(),
"signal": "SELL" if sig_result["signal"] < 0 else "BUY",
"strength": min(1.0, abs(sig_result["annual_apr"]) * 10),
"reason": sig_result["reason"]
})
# Log periodically
if not hasattr(globals().get("_funding_log_tick", None), "__int__"):
globals()["_funding_log_tick"] = 0
if globals()["_funding_log_tick"] % 30 == 0:
import logging import logging
logging.getLogger("ftdt-paper").info( logging.getLogger("ftdt-paper").info(
"{} Funding rate: {:.6f}% 8h | {:.2f}% APR | signal={}".format( f"[Fund] APR={sig_result['annual_apr']*100:.2f}% | "
"[Fund]", btc_fr*100, annual_fr*100, f"8h={sig_result['rate_8h']*100:.6f}% | "
"SELL" if btc_fr > 0 else "BUY" if btc_fr < 0 else "NONE" f"signal={sig_result['signal']}"
) )
) globals()["_funding_log_tick"] = globals().get("_funding_log_tick", 0) + 1
if annual_fr > 0.05: # >5% APR (production threshold) except Exception:
# Fallback to old method
if funding_rates and isinstance(funding_rates[-1], dict):
btc_fr = funding_rates[-1].get("BTC", 0)
annual_fr = abs(btc_fr) * 365 * 3 if btc_fr else 0
if annual_fr > 0.05:
STRATEGIES["Funding Rate Arb"]["signals"].append( STRATEGIES["Funding Rate Arb"]["signals"].append(
{"time":time.time(),"signal":"SELL" if btc_fr > 0 else "BUY", {"time":time.time(),"signal":"SELL" if btc_fr > 0 else "BUY",
"strength": min(0.6, annual_fr * 50), "strength": min(0.6, annual_fr * 50),
+143
View File
@@ -0,0 +1,143 @@
"""
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,
}