CODE PLAYBOOK · PYTHON · PUBLIC ENDPOINTS

Backtest a Funding-Rate Strategy with Historical Data in Python

"Go long the exchange with lower funding, short the one with higher funding when the spread exceeds X" is a common first idea for a funding-rate strategy. This playbook shows how to pull the historical data you'd need to check it in Python — and, just as importantly, what a backtest like this can and can't tell you.

~14 min readPython 3.8+, pandasNo API key required

The problem: did this rule actually pay you funding?

Perpetual futures funding rates are public, but turning "funding on exchange A has been higher than exchange B for the last few weeks" into an actual backtest means pulling a time series of funding rates per exchange, aligning it with price data, and running some rule over it consistently instead of eyeballing a chart. That's a data-wrangling problem before it's a strategy problem — and it's the same wrangling regardless of which funding rule you end up testing.

This playbook pulls historical funding rate data and historical price data from two public endpoints, loads them into pandas, and runs a simple vectorized rule so you have working code for the data-handling part. It does not claim the rule is profitable — see the caveats section below for why that claim would be premature from this alone.

Prerequisites

Both endpoints used here are public per the current routing — no API key required, subject to a shared per-IP rate limit (150 requests/minute). You'll need:

A free key from /signup (200 calls/day, BTC/ETH/SOL) isn't required for these two endpoints, but is worth having if you extend this into a script that also pulls whale or derivatives-screener data elsewhere in the API, since those apply per-key quotas instead of the shared anonymous IP throttle.

The endpoints

GET https://api.smartmoneyapi.com/v1/historical/funding?symbol=BTCUSDT&days=30&limit=5000

Query params: symbol (default "BTCUSDT" — note the USDT suffix, unlike some other endpoints in this API that use bare "BTC"), days (clamped 1-365, default 30), limit (clamped 1-5000, default 5000). Response:

json
{ "symbol": "BTCUSDT", "days": 30, "count": 180, "data": [ { "timestamp": 1753300000, "symbol": "BTCUSDT", "exchange": "binance", "funding_rate": 0.0001, "mark_price": 61234.5 } ] }

Rows include an exchange field (e.g. "binance", "bybit") — if you want to compare exchange A vs exchange B, pull the symbol once and filter/group by exchange client-side, rather than issuing separate calls per exchange.

GET https://api.smartmoneyapi.com/v1/historical/market?coin=bitcoin&days=365&limit=5000

coin is a CoinGecko coin id (e.g. "bitcoin", "ethereum"). Response:

json
{ "coin": "bitcoin", "days": 365, "count": 365, "data": [ { "timestamp": 1753300000, "coin_id": "bitcoin", "price": 61234.5, "market_cap": 1205000000000, "volume": 28900000000 } ] }

Note this is a price / market-cap / volume time series, not OHLCV candles — there's no open/high/low column, just one price point per row. Fine for aligning against funding timestamps; not a substitute for candlestick data if your rule needs intra-period highs and lows.

Two sibling endpoints exist for extending this: GET /v1/historical/open-interest?symbol=&days=&limit= (rows: timestamp, symbol, exchange, oi, oi_value) and GET /v1/historical/long-short?symbol=&days=&limit= (rows: timestamp, symbol, type, long_ratio, short_ratio). Same public access pattern, same module. Useful for adding OI or positioning context to a backtest — not built into the example below.

Python script: pull data + run a simple backtest

This pulls funding history for one symbol, filters to a single exchange, computes the annualized rate, applies a threshold rule, and sums a simple pre-fee "funding collected" column. It deliberately does not print a win-rate, Sharpe ratio, or profit factor — those numbers invite treating this as proof of an edge, which a 30-day, single-symbol, no-slippage accumulation is not.

python
import requests import pandas as pd BASE = "https://api.smartmoneyapi.com" def get_funding_history(symbol="BTCUSDT", days=30, exchange=None): resp = requests.get( f"{BASE}/v1/historical/funding", params={"symbol": symbol, "days": days, "limit": 5000}, timeout=15, ) resp.raise_for_status() rows = resp.json()["data"] df = pd.DataFrame(rows) if exchange: df = df[df["exchange"] == exchange] df["timestamp"] = pd.to_datetime(df["timestamp"], unit="s") return df.sort_values("timestamp").reset_index(drop=True) def annualized_apr(funding_rate, payments_per_day=3): # Perpetuals typically pay funding 3x/day (every 8h) on most exchanges. return funding_rate * payments_per_day * 365 * 100 def run_simple_backtest(df, apr_threshold=15.0, notional=10_000): """Flag periods where annualized funding exceeds a threshold and sum the (pre-fee) funding payment collected on a fixed notional. This is a mechanics demo, not a profitability claim.""" df = df.copy() df["annualized_apr"] = df["funding_rate"].apply(annualized_apr) df["flagged"] = df["annualized_apr"].abs() > apr_threshold df["funding_collected"] = 0.0 df.loc[df["flagged"], "funding_collected"] = ( df.loc[df["flagged"], "funding_rate"] * notional ) return df def main(): df = get_funding_history("BTCUSDT", days=30, exchange="binance") print(f"Loaded {len(df)} funding rows for BTCUSDT on binance") result = run_simple_backtest(df, apr_threshold=15.0, notional=10_000) n_flagged = int(result["flagged"].sum()) total_collected = result["funding_collected"].sum() print(f"Periods flagged (|APR| > 15%): {n_flagged} of {len(result)}") print(f"Pre-fee funding collected on flagged periods: ${total_collected:,.2f}") print("\nNote: pre-fee, single symbol/exchange, single lookback window.") print("See the caveats section before drawing any conclusions from this.") if __name__ == "__main__": main()
Need OI or positioning data too?

Open interest and long/short ratio history live on the same base URL, same free tier — no separate signup for each data type.

Get your free API key →

Expected output

text
Loaded 90 funding rows for BTCUSDT on binance Periods flagged (|APR| > 15%): 11 of 90 Pre-fee funding collected on flagged periods: $184.30 Note: pre-fee, single symbol/exchange, single lookback window. See the caveats section before drawing any conclusions from this.

That's a data-wrangling result, not a trading result: it tells you the mechanics work (data loads, threshold flags fire, sums accumulate) — it says nothing yet about whether the rule would have been profitable after execution costs, or whether the pattern persists going forward.

Why this is harder than it looks

A few things this simple example glosses over, on purpose, so they don't get buried:

What to build next

All of this comes from the same API key, alongside derivatives, whale, and on-chain data across 66+ endpoints — instead of separately integrating Binance, Bybit, and Hyperliquid funding feeds plus a CoinGecko price feed yourself.

Start free — 200 calls/day, no card

Get live whale flow, funding, open interest and on-chain data across 3 exchanges from one API. Free tier, no credit card, upgrade any time.

Start free →
Try the live API console → (no account needed)