DEV Community

Greenhaven365
Greenhaven365

Posted on AI-assisted

Estimate isolated liquidation price from a public mark-price API (Python)

A liquidation price in a futures UI is an estimate. It moves when mark, assigned margin, maintenance tiers, or fees change, and it is usually not computed from the last trade on your chart. This tutorial pulls last, mark, and index for OKX BTC-USDT-SWAP from public HTTP endpoints (no API key), then applies OKX’s published isolated-margin formula for a linear USDT-margined contract to a set of declared position inputs.

The script will not match a live account unless your maintenance-margin rate, fee rate, margin balance, and contract multiplier equal the venue’s current values. Treat the output as a reconciliation helper.

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 compute

OKX documents estimated isolated liquidation prices for USDT-margined futures as follows (Futures PnL calculation rules):

long:
  P_liq = (WB - FV * |n| * P_e) / [FV * |n| * (mmr + fee - 1)]

short:
  P_liq = (WB + FV * |n| * P_e) / [FV * |n| * (mmr + fee + 1)]
Enter fullscreen mode Exit fullscreen mode

WB is isolated margin balance, FV is contract face value, n is the contract count, P_e is average entry, mmr is the maintenance-margin rate for the applicable tier, and fee is the fee rate the venue includes in the estimate.

On 10 November 2026, OKX’s public instruments endpoint described BTC-USDT-SWAP as linear, ctVal = 0.01 BTC per contract, ctMult = 1, settled in USDT. Ten contracts are therefore 0.10 BTC of base size (Q = FV * |n| * multiplier).

The teaching goal is to measure how far the current mark sits from P_liq, and why substituting last would be the wrong risk input even when the two prices almost agree.

Fetch last, mark and index

import json
import urllib.request
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

INST = "BTC-USDT-SWAP"
INDEX_INST = "BTC-USDT"
UA = {"User-Agent": "liq-estimate-demo/1.0"}
LOCAL_TZ = ZoneInfo("Asia/Shanghai")


def get_json(url: str) -> dict:
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=20) as resp:
        payload = json.loads(resp.read().decode())
    if str(payload.get("code")) != "0":
        raise RuntimeError(payload)
    return payload["data"][0]


def fetch_prices() -> dict:
    ticker = get_json(f"https://www.okx.com/api/v5/market/ticker?instId={INST}")
    mark = get_json(
        "https://www.okx.com/api/v5/public/mark-price"
        f"?instType=SWAP&instId={INST}"
    )
    index = get_json(
        "https://www.okx.com/api/v5/market/index-tickers?instId={INDEX_INST}"
    )
    ts = datetime.fromtimestamp(int(mark["ts"]) / 1000, tz=timezone.utc)
    return {
        "last": float(ticker["last"]),
        "bid": float(ticker["bidPx"]),
        "ask": float(ticker["askPx"]),
        "mark": float(mark["markPx"]),
        "index": float(index["idxPx"]),
        "when": ts.astimezone(LOCAL_TZ),
    }


if __name__ == "__main__":
    px = fetch_prices()
    mid = (px["bid"] + px["ask"]) / 2
    print(f"{INST} at {px['when'].isoformat()}")
    print(f"last={px['last']:.1f}  mid={mid:.2f}  "
          f"mark={px['mark']:.1f}  index={px['index']:.1f}")
    print(f"mark-last={px['mark']-px['last']:+.1f}  "
          f"mark-index={px['mark']-px['index']:+.1f}")
Enter fullscreen mode Exit fullscreen mode

On 10 November 2026 at 18:58:21–22 (Asia/Shanghai), a live pull of these endpoints returned approximately:

last = 74,216.8     mid = 74,216.75
mark = 74,217.1     index = 74,256.6
mark − last  = +0.3 USDT
mark − index = −39.5 USDT
Enter fullscreen mode Exit fullscreen mode

Last and mark were nearly identical in that window. Index was not. A liquidation engine that listens to mark will not care that your chart’s last price ticked 0.3 USDT the other way.

Apply the isolated-linear formula to declared inputs

Maintenance-margin rates are tiered and change. Do not hard-code a production mmr from a tutorial. Here mmr and fee are explicit assumptions so the algebra is testable.

from dataclasses import dataclass


@dataclass
class IsolatedLinear:
    face_value: float   # BTC per contract, e.g. 0.01
    contracts: float    # absolute contract count
    side: str           # "long" or "short"
    entry: float        # average entry (USDT)
    wallet: float       # isolated margin balance (USDT)
    mmr: float          # maintenance margin rate, e.g. 0.005
    fee: float          # fee rate used in the venue estimate
    multiplier: float = 1.0

    @property
    def qty(self) -> float:
        return self.face_value * abs(self.contracts) * self.multiplier


def est_isolated_liq(p: IsolatedLinear) -> float:
    q = p.qty
    if p.side == "long":
        return (p.wallet - q * p.entry) / (q * (p.mmr + p.fee - 1.0))
    if p.side == "short":
        return (p.wallet + q * p.entry) / (q * (p.mmr + p.fee + 1.0))
    raise ValueError("side must be 'long' or 'short'")


def distance_report(p: IsolatedLinear, mark: float, last: float) -> None:
    liq = est_isolated_liq(p)
    print(f"side={p.side} qty={p.qty} BTC entry={p.entry} wallet={p.wallet}")
    print(f"assumed mmr={p.mmr} fee={p.fee}")
    print(f"estimated isolated liq = {liq:.2f} USDT")
    print(f"mark {mark:.1f} is {mark - liq:+.2f} from that estimate")
    print(f"last {last:.1f} is {last - liq:+.2f} from that estimate")
    print("Use mark for the distance. Last is shown only to make the gap visible.")


# Illustrative isolated long: 10 contracts = 0.10 BTC, 10x-style margin at entry
# 0.10 * 74,217 / 10 ≈ 742.17 USDT. Algebra, not a trade recommendation.
# pos = IsolatedLinear(0.01, 10, "long", 74_217.0, 742.17, mmr=0.005, fee=0.0005)
# px = fetch_prices()
# distance_report(pos, mark=px["mark"], last=px["last"])
Enter fullscreen mode Exit fullscreen mode

Worked arithmetic for those declared inputs:

Q = 0.10 BTC    WB = 742.17    P_e = 74,217    mmr = 0.005    fee = 0.0005

P_liq = (742.17 − 0.10 × 74,217) / [0.10 × (0.005 + 0.0005 − 1)]
      = (−6,679.53) / (−0.09945)
      ≈ 67,164.7 USDT
Enter fullscreen mode Exit fullscreen mode

Against the snapshot mark of 74,217.1, the estimate sits roughly 7,052 USDT below mark. Against last (74,216.8) the distance is the same to the dollar in this quiet window. That agreement is a property of this minute, not a reason to wire last price into the solver. Raising mmr to 0.01, to simulate a higher maintenance tier, produces a closer long estimate with the same wallet and entry: a tier-schedule change can shrink the buffer without a new fill.

What this script does not cover

Do not reuse the function for cross margin (wallet-level maintenance and other-leg PnL belong in a different solver), for inverse contracts (those use 1/price), or as a substitute for a venue’s tier cum offset if one is published. Working orders can reserve margin, and a funding debit can move the estimate without a trade. If your UI size is already in BTC, do not also multiply by ctVal.

Fetching mark in the same process as the estimate makes a common mixing error visible: a spreadsheet that subtracts “distance to stop” from the chart’s last price is not measuring liquidation distance. Print both distances; keep only the mark distance next to a liquidation warning. A warning can also appear before a last-price stop triggers, because the two systems may watch different prices — see greenhaven365.com/crypto-futures-liquidation-warning-before-stop-triggers/.

Useful follow-ups: read live ctVal / ctMult from public/instruments, and add a test that your function reproduces a venue worked example before you trust a custom mmr table.

Sources: OKX public API market/ticker, public/mark-price, market/index-tickers, and public/instruments for BTC-USDT-SWAP, retrieved 2026-11-10 18:58 (Asia/Shanghai). Formula: OKX Help, Futures PnL calculation rules, isolated USDT-margined estimated liquidation price (https://www.okx.com/en-us/help/futures-pnl-calculation-rules).

AI disclosure: This article was created with AI assistance (drafting/editing). The numeric snapshot was checked against a live public API response on the date above; the liquidation example uses declared margin parameters, not a live account. Re-run the script and verify the venue’s current formula before relying on any figure.

Disclaimer: Not financial advice. Educational / developer content only.

Top comments (0)