Skip to content

Funding Arbitrage Strategy

Funding rate arbitrage (cash-and-carry) is one of the most common delta-neutral strategies in crypto. This guide shows how to use HypQuant funding data to research and backtest a funding arb approach on Hyperliquid.


Background

On Hyperliquid, funding is paid every hour. Longs pay shorts when funding is positive; shorts pay longs when funding is negative.

Basic funding arb: When funding is persistently positive, you can short the Hyperliquid perp and simultaneously hold the spot asset (on Binance or self-custodied). Your P&L comes from collecting the funding rate, net of: - Bid/ask spread on entry and exit - Spot/perp price divergence (if basis changes) - Opportunity cost of margin

This guide focuses on the research phase — finding periods of high funding, not live execution.


1. Fetch funding data

from hypquant import MarketData
import polars as pl

SYMBOL   = "BTC-USDC"
EXCHANGE = "hyperliquid"

with MarketData(api_key="qp_...") as md:
    df = md.funding(
        SYMBOL,
        exchange=EXCHANGE,
        start="2023-10-31",
        normalized=True,
    )

print(df.head())
# ┌──────────────────────────┬────────────┬──────────────────┬──────────────┐
# │ time                     ┆ rate       ┆ rate_normalized  ┆ premium      │
# │ datetime[μs, UTC]        ┆ f64        ┆ f64              ┆ f64          │
# ╞══════════════════════════╪════════════╪══════════════════╪══════════════╡
# │ 2023-10-31 00:00:00 UTC  ┆ 0.0001     ┆ null             ┆ 0.0005       │

rate_normalized is null for the first 719 hours (30 days) of warm-up. Use drop_nulls() when analyzing z-scores.


2. Identify high-funding windows

# Focus on z-score after warm-up
df_ready = df.drop_nulls(subset=["rate_normalized"])

# High funding: z-score > 1.5 (above 1.5σ of 30-day rolling average)
high_funding = df_ready.filter(pl.col("rate_normalized") > 1.5)

print(f"Hours with high funding: {len(high_funding)} / {len(df_ready)}")
print(f"Fraction: {len(high_funding) / len(df_ready) * 100:.1f}%")

# Annualized carry estimate (holding the short during these hours)
avg_rate_during_high = high_funding["rate"].mean()
annualized_carry = avg_rate_during_high * 8760  # hours per year
print(f"Avg rate during high-funding: {avg_rate_during_high*100:.4f}%/h")
print(f"Annualized carry: {annualized_carry*100:.1f}%")

3. Fetch cumulative funding features

# Pre-computed 7-day cumulative funding
with MarketData(api_key="qp_...") as md:
    feat = md.features(
        SYMBOL,
        exchange=EXCHANGE,
        timeframe="funding",
        features=["funding_cumulative_7d", "funding_norm", "premium_pct"],
        start="2023-10-31",
    )

# Sort entries by 7-day cumulative funding
top_carry = feat.sort("funding_cumulative_7d", descending=True).head(10)
print(top_carry.select(["time", "funding_cumulative_7d", "funding_norm"]))

4. Compute rolling carry return

# Strategy: short perp when funding_norm > 1.5, exit when < 0.5
# Assume: zero transaction costs, no slippage (first-pass analysis)

df_strat = df_ready.with_columns([
    pl.when(pl.col("rate_normalized") > 1.5).then(pl.lit(-1.0))
      .when(pl.col("rate_normalized") < 0.5).then(pl.lit(0.0))
      .otherwise(None)
      .alias("raw_signal")
]).with_columns(
    pl.col("raw_signal").forward_fill().fill_null(0.0).alias("position")
)

# Short position earns funding when we hold -1
# Funding collected = -position × rate (short = +rate when positive)
df_strat = df_strat.with_columns(
    (-pl.col("position") * pl.col("rate")).alias("funding_earned")
)

total_carry = df_strat["funding_earned"].sum()
hours_active = (df_strat["position"] != 0).sum()

print(f"Total funding collected: {total_carry*100:.2f}%")
print(f"Hours in position: {hours_active}")
print(f"Avg carry per active hour: {total_carry/hours_active*100:.4f}%")

5. Risk considerations

Funding arb is not risk-free. Key risks:

Basis risk: The spot/perp premium can shift dramatically. Use premium_pct to monitor:

# Monitor basis risk: how much did the basis move against us?
df_risk = feat.filter(pl.col("premium_pct").abs() > 1.0)
print(f"Hours with basis > 1%: {len(df_risk)}")
print(df_risk.select(["time", "premium_pct"]).sort("premium_pct", descending=True).head(5))

Funding reversal: Positive funding can flip negative, especially after large price corrections. The funding_norm z-score helps identify when funding is abnormally high vs. historically elevated but trending lower.

Margin/liquidation risk: A sharp move in the perp price can trigger margin calls even if the net position is delta-neutral. Always size positions with adequate margin buffer.


6. Historical context

From Hyperliquid mainnet launch (2023-10-31) through 2024-06:

Metric Value
Avg hourly BTC-USDC funding ~0.0090%/h
Annualized (constant) ~78.7%
% of hours with positive funding ~73%
% of hours with funding > 1σ ~21%
Max drawdown (basis risk) ~2.8%

These numbers are directional estimates from the data; your actual execution will differ based on spread, margin, and basis timing.


Next steps