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.
1. Fetch funding and mark prices
Section titled “1. Fetch funding and mark prices”import pandas as pdfrom markethistory import Client
mh = Client() # reads MARKET_HISTORY_KEYspan = {"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.
2. Price each payment at the mark
Section titled “2. Price each payment at the mark”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 spotdf["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 yieldA 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.
3. A rule: hold only while funding pays
Section titled “3. A rule: hold only while funding pays”fee = 0.0005 # per leg, per trade, on notionaldf["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.
What this leaves out
Section titled “What this leaves out”- 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 inmark − indexover 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.