Skip to content
Market History

Funding-rate carry on perpetuals

A perpetual’s funding moves cash between longs and shorts at every funding event. When the rate is positive, longs pay shorts. Short one BTC on the perpetual, buy one BTC spot, and price moves cancel: what is left is the funding, plus the drift between the two prices. This guide measures the funding leg over one quarter, in USDT, priced at the mark like the payment itself.

You need the Python client with the pandas extra, and a key with bought bars. One quarter of BTCUSDT is about 270 funding rows and 131,000 one-minute mark prices: about 131,500 bars, 66 cents at the first band’s rate.

import pandas as pd
from markethistory import Client
mh = Client() # reads MARKET_HISTORY_KEY
span = {"start": "2024-01-01", "end": "2024-03-31"}
funding = pd.DataFrame(mh.each(mh.funding, "BTCUSDT", limit=100_000, **span))
mark = pd.DataFrame(mh.each(mh.prices, "BTCUSDT", "mark", limit=100_000, **span))
for df in (funding, mark):
df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True, format="ISO8601")

each() follows the cursor to the end of the range. Funding rows are timestamp, rate (a fraction: 0.0001 is 0.01%) and interval_h; mark rows are one-minute OHLC.

mark = mark[["timestamp", "open"]].rename(columns={"open": "mark"})
df = pd.merge_asof(funding.sort_values("timestamp"), mark, on="timestamp")
qty = 1.0 # BTC short on the perpetual, long in spot
df["income"] = (
qty * df["mark"] * df["rate"]
) # USDT received by the short (paid when < 0)
df["apr"] = df["rate"] * 365 * 24 / df["interval_h"] # the rate as a yearly yield

A funding row is stamped a few milliseconds after the hour, so merge_asof (backward by default) pairs it with the minute that opened on the hour, and that minute’s open is the mark at the payment. interval_h turns each rate into a yearly figure even when a contract changes its funding interval.

fee = 0.0005 # per leg, per trade, on notional
df["hold"] = (df["rate"].rolling(3).mean().shift(1) > 0).astype(float)
switches = df["hold"].diff().abs().fillna(df["hold"])
df["pnl"] = df["hold"] * df["income"] - switches * 2 * fee * qty * df["mark"]
days = (df["timestamp"].iloc[-1] - df["timestamp"].iloc[0]) / pd.Timedelta(days=1)
notional = qty * df["mark"].mean()
print(f"mean funding, yearly: {df['apr'].mean():.1%}")
print(f"always hedged: {df['income'].sum():,.0f} USDT")
print(
f"rule: {df['pnl'].sum():,.0f} USDT, {df['pnl'].sum() / notional * 365 / days:.1%} a year"
)

The decision for each payment uses the three rates before it (shift(1)), never the one being paid. Every switch in or out trades both legs, hence 2 * fee.

  • The basis. The hedge earns or loses the change in the gap between the perpetual and spot. The index price is the spot reference: fetch it like the mark (mh.prices(..., "index")) and add the change in mark − index over each holding period.
  • Margin. The short needs collateral that earns nothing here.
  • Execution. Mark and index are reference prices: nothing trades at them, so the fills of the two legs differ from them by the spread.