All resources
Tutorial · Quant 12 min read

Backtest a contrarian funding strategy in 40 lines of pandas.

Optiom Research · runnable code, tested against the live API

The classic funding trade: when annualized funding gets extreme, the crowd is leaning too far on one side — fade it and collect the funding payment while you wait. This tutorial builds the whole backtest from two API calls: funding history and hourly candles. On the available BTCUSDT history the naive version returns +42% while buy-and-hold lost 43% — read the caveats before you get excited.

The strategy and the code

Rule: if annualized funding is above +10%, short until the next funding interval; below -10%, long. Position return = price move in your direction plus the funding you receive for being on the unpopular side. Everything joins on funding timestamps, with the price forward-filled from 1h closes.

import pandas as pd
import requests

BASE = "https://www.getoptiom.com/api/tradinghub"
SYMBOL = "BTCUSDT"
THRESHOLD = 10  # annualized %, entry trigger

funding = pd.DataFrame(requests.get(
    f"{BASE}/funding", params={"symbol": SYMBOL, "max_rows": 20_000}, timeout=30
).json())
funding["dt"] = pd.to_datetime(funding["time"], unit="s", utc=True)
funding["annualized_pct"] = funding["value"] * 3 * 365 * 100
funding = funding.set_index("dt").sort_index()

candles = pd.DataFrame(requests.get(
    f"{BASE}/ohlcv", params={"symbol": SYMBOL, "timeframe": "1h", "limit": 20_000}, timeout=60
).json()["candles"])
candles["dt"] = pd.to_datetime(candles["time"], unit="s", utc=True)
closes = candles.set_index("dt")["close"].sort_index()

# price at each funding timestamp (last close at or before it)
funding["price"] = closes.reindex(funding.index, method="ffill")
funding = funding.dropna(subset=["price"])

# contrarian rule: crowded longs (funding very positive) -> short, and vice versa
funding["position"] = 0
funding.loc[funding["annualized_pct"] > THRESHOLD, "position"] = -1
funding.loc[funding["annualized_pct"] < -THRESHOLD, "position"] = 1

# hold each position for one funding interval; earn price move + funding carry
funding["price_ret"] = funding["price"].pct_change().shift(-1)
funding["carry"] = -funding["position"] * funding["value"]  # shorts receive positive funding
funding["strat_ret"] = funding["position"] * funding["price_ret"] + funding["carry"]

active = funding[funding["position"] != 0]
total = (1 + funding["strat_ret"].fillna(0)).prod() - 1
bh = funding["price"].iloc[-1] / funding["price"].iloc[0] - 1
print(f"Intervals: {len(funding)}, in position: {len(active)} ({len(active)/len(funding)*100:.0f}%)")
print(f"Strategy return: {total*100:+.1f}%  |  Buy & hold: {bh*100:+.1f}%")
print(f"Hit rate when active: {(active['strat_ret'] > 0).mean()*100:.0f}%")

Honest caveats

This is in-sample on a single symbol with no fees, no slippage, and only 15% time in the market — the aggregate number flatters small samples. Perp fees (roughly 4-8 bps per side taker) eat a meaningful part of each interval trade. Treat the result as evidence the signal is worth studying, not as a deployable system.

Where to take it next

Sweep the threshold instead of hard-coding 10%. Test per-symbol on the top 20 pairs. Add open interest as a filter (see the OI squeeze tutorial): fading extreme funding works best when leverage is also elevated. Pro accounts get 2 years of history, which turns this from anecdote into a sample.

Run it on your own data.

Create a free account for CSV exports, then move to API access as your workflow grows.