DEV Community

Greenhaven365
Greenhaven365

Posted on AI-assisted

Compute crypto perpetual funding cost from a public API (Python)

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)