Perpetual futures charge (or pay) funding on a schedule so the contract stays near spot. The UI usually shows a percentage. What you actually need for risk is a dollar amount on your notional—and a way to rebuild that number from public data when a screenshot is not enough.
This tutorial pulls recent funding history for OKX BTC-USDT-SWAP from a public HTTP API, converts rates into cash-flow on a linear USDT notional, and prints a small holding-cost report. No exchange account or API key is required for the public endpoints used here.
Not financial advice. This is a programming / market-structure exercise. Derivatives trading can liquidate your margin. Verify every formula against the venue you use.
What we will calculate
For a linear (USDT-margined) perpetual, a practical first approximation is:
payment_usd ≈ side × quantity × mark_price × funding_rate
Where:
-
sideis+1for a long (pays when rate > 0) or-1for a short (pays when rate < 0), under the common “positive rate → longs pay shorts” convention -
quantityis position size in base coin -
mark_priceis the venue mark used for valuation (we will parameterize it; live mark endpoints differ by venue) -
funding_rateis the settled rate for that interval (notional fraction, not percent)
Venue documentation can add multipliers, interest/premium components, or fee floors. Treat the script as a reconciliation helper, not a replacement for the official statement.
Fetch public funding history (OKX)
OKX exposes funding history without authentication:
GET https://www.okx.com/api/v5/public/funding-rate-history?instId=BTC-USDT-SWAP&limit=24
Example response fields we care about: fundingRate, fundingTime, realizedRate.
import json
import urllib.request
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
INST = "BTC-USDT-SWAP"
URL = (
"https://www.okx.com/api/v5/public/funding-rate-history"
f"?instId={INST}&limit=24"
)
LOCAL_TZ = ZoneInfo("Asia/Shanghai")
def fetch_funding_history():
req = urllib.request.Request(URL, headers={"User-Agent": "funding-cost-demo/1.0"})
with urllib.request.urlopen(req, timeout=20) as resp:
payload = json.loads(resp.read().decode())
if payload.get("code") != "0":
raise RuntimeError(payload)
return payload["data"]
def main():
rows = fetch_funding_history()
print(f"Fetched {len(rows)} intervals for {INST}")
for row in rows[:5]:
ts = datetime.fromtimestamp(int(row["fundingTime"]) / 1000, tz=timezone.utc)
local = ts.astimezone(LOCAL_TZ)
rate = float(row["fundingRate"])
print(f"{local.isoformat()} rate={rate:.8f} ({rate * 100:.4f}% of notional)")
if __name__ == "__main__":
main()
On 9 November 2026 (Asia/Shanghai), a live pull of this endpoint returned recent BTC-USDT-SWAP settlements such as:
- 2026-11-09 08:00 CST → rate ≈ 0.00002100 (0.0021% of notional)
- 2026-11-09 00:00 CST → rate ≈ 0.00001280
- 2026-11-08 16:00 CST → rate ≈ 0.00003358
Sum of those three ≈ 0.00006738 of notional over that 24h window (source: OKX public API, retrieved the same day).
Turn rates into a holding-cost report
Add position parameters and accumulate payments. Here mark_price is an input you supply (from your flat file, websocket, or another public ticker). Keeping mark as an argument makes the cash-flow math testable without coupling to a second API.
from dataclasses import dataclass
@dataclass
class Position:
quantity: float # base coin size, e.g. 0.5 BTC
side: int # +1 long, -1 short
mark_price: float # USDT mark used for notional
def funding_payment_usd(pos: Position, funding_rate: float) -> float:
notional = pos.quantity * pos.mark_price
# Positive rate: longs pay shorts.
return pos.side * notional * funding_rate
def holding_cost_report(pos: Position, rows: list[dict], last_n: int = 3) -> None:
selected = rows[:last_n]
total = 0.0
print(f"Position: side={pos.side:+d}, qty={pos.quantity}, mark={pos.mark_price}")
print(f"Notional ≈ {pos.quantity * pos.mark_price:,.2f} USDT")
for row in selected:
rate = float(row["fundingRate"])
pay = funding_payment_usd(pos, rate)
total += pay
ts = datetime.fromtimestamp(int(row["fundingTime"]) / 1000, tz=LOCAL_TZ)
print(f"{ts.strftime('%Y-%m-%d %H:%M %Z')} rate={rate:.8f} cashflow={pay:+.4f} USDT")
print(f"Sum over {last_n} intervals: {total:+.4f} USDT")
print("Positive cashflow = you paid; negative = you received (given side convention).")
# Example: long 0.5 BTC at an assumed mark of 90,000 USDT
# holding_cost_report(Position(0.5, +1, 90_000.0), fetch_funding_history(), last_n=3)
Illustrative output math for that example on the three intervals above:
notional ≈ 45,000
sum_rates ≈ 0.00006738
long pays ≈ 45,000 × 0.00006738 ≈ 3.03 USDT over those three settlements
Change side to -1 to flip the sign. Change last_n to 9 for a rough 72-hour window when the venue uses 8-hour intervals.
Edge cases the script does not hide
-
Estimate vs settled rate. Prefer
fundingRate/realizedRatefrom history over a pre-settlement forecast when reconciling a statement. - Mark moves between intervals. Using one assumed mark understates path dependence. For accounting, use the mark the venue applied at each funding event if you have it.
-
Contract multipliers. Some products are not
qty × pricenotionals. Read the instrument spec. - Sign conventions. Always confirm “positive rate ⇒ longs pay” for your venue before automating alerts.
-
Geo / availability. Public endpoints can return region errors. Handle non-
0response codes explicitly.
Why this belongs in a trading journal tool
Funding is easy to ignore because each print is small. Automation helps for a boring reason: it turns “0.01%” into “$X on my notional over my hold.” Pair it with fee totals and stop-fill quality and you get a cost stack you can actually review.
If you also size positions from a fixed dollar risk budget (stop distance + fees), the funding module becomes one more line in the same ledger. A worked sizing approach is documented here: greenhaven365.com/calculate-crypto-futures-position-size-fixed-risk-budget/.
Reconciling against an exchange statement
When your script and the venue statement disagree, isolate the difference in this order:
- Interval set — same settlement timestamps?
- Rate field — forecast vs settled/realized?
- Notional base — mark at funding vs your assumed mark?
- Side / hedge mode — net position vs separate long/short legs?
-
Units — percent display vs decimal rate (
0.01vs0.0001)?
A one-line assert helps catch unit bugs early:
assert abs(float(row["fundingRate"])) < 0.05, "rate looks like a percent, not a decimal"
Most venues publish rates as decimals of notional (for example 0.0001 for 0.01%). If you accidentally treat 0.01 as a decimal, your cash-flow will be ~100× too large—an expensive way to learn about displays.
Next improvements
- Pull
funding-rate(current period) and alert when|rate|exceeds a threshold on open notional - Join funding cash-flow to a CSV of fills for true all-in PnL
- Add a venue adapter interface (
OkxPublic,OtherVenue) instead of hard-coding one URL
Sources: OKX Public API funding-rate-history for BTC-USDT-SWAP, retrieved 2026-11-09 (Asia/Shanghai).
AI disclosure: This article was created with AI assistance (drafting/editing). The code paths and numeric examples were checked against a live public API response on the date above; you should re-run the script yourself before relying on any figure.
Disclaimer: Not financial advice. Educational / developer content only.
Top comments (0)