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:

  • side is +1 for a long (pays when rate > 0) or -1 for a short (pays when rate < 0), under the common “positive rate → longs pay shorts” convention
  • quantity is position size in base coin
  • mark_price is the venue mark used for valuation (we will parameterize it; live mark endpoints differ by venue)
  • funding_rate is 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

  1. Estimate vs settled rate. Prefer fundingRate / realizedRate from history over a pre-settlement forecast when reconciling a statement.
  2. 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.
  3. Contract multipliers. Some products are not qty × price notionals. Read the instrument spec.
  4. Sign conventions. Always confirm “positive rate ⇒ longs pay” for your venue before automating alerts.
  5. Geo / availability. Public endpoints can return region errors. Handle non-0 response 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:

  1. Interval set — same settlement timestamps?
  2. Rate field — forecast vs settled/realized?
  3. Notional base — mark at funding vs your assumed mark?
  4. Side / hedge mode — net position vs separate long/short legs?
  5. Units — percent display vs decimal rate (0.01 vs 0.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.