A one-tick spread is not a fill. On a crypto perpetual, a marketable order walks the displayed book until its size is consumed, and the average fill (VWAP) can sit several basis points away from mid even when the best bid and best ask are a single tick apart. This tutorial pulls a public depth snapshot for OKX BTC-USDT-SWAP (no API key), converts contract size to BTC with the live face value, and walks both sides for a set of declared sizes.
The script estimates slippage from visible depth at one timestamp. It does not reserve liquidity, it does not see hidden orders, and it is not a promise of a live fill.
Not financial advice. This is a programming / market-structure exercise. Derivatives trading can liquidate your margin. Verify contract units and order-book fields against the venue you use.
What we will compute
For a marketable buy, walk the ask side from best ask upward until the requested BTC size is filled (or the snapshot is exhausted). For a marketable sell, walk the bids downward. Then:
mid = (best_bid + best_ask) / 2 vwap = sum(price × filled_btc) / filled_btc slip_vs_mid = (vwap − mid) / mid (in basis points) slip_vs_best = (vwap − best) / best (in basis points) where best is the best ask for a buy and the best bid for a sell. A buy that walks up prints a positive slip versus mid; a sell that walks down prints a negative slip versus mid. Both are costs relative to mid if you read the sign as “away from mid.”
Two unit choices are easy to get wrong, and this script makes them explicit:
- Size in the book is contracts, not BTC. On this instrument
ctValis 0.01 BTC andctMultis 1, so BTC at a level iscontracts × 0.01. - The depth request is capped.
sz=400asks for up to 400 price levels per side, not for the entire book. Depth figures at wide basis-point bands can be truncated by that cap.
A market order that fills at several prices is normal book mechanics; a short conceptual companion is at greenhaven365.com/crypto-futures-market-order-multiple-prices/. Why a tight spread still does not describe size is at greenhaven365.com/why-crypto-futures-spread-does-not-show-depth/.
Fetch the instrument and the book
import json import urllib.request from datetime import datetime, timezone from zoneinfo import ZoneInfo INST = "BTC-USDT-SWAP" UA = {"User-Agent": "slippage-demo/1.0"} LOCAL_TZ = ZoneInfo("Asia/Shanghai") DEPTH_SZ = 400 SIZES_BTC = (0.10, 0.50, 1.0, 5.0, 10.0, 25.0, 50.0) BANDS_BPS = (1, 2, 5, 10, 25, 50) def get_payload(url: str) -> list: 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"] def parse_ts(ms: str | int) -> datetime: return datetime.fromtimestamp(int(ms) / 1000, tz=timezone.utc).astimezone(LOCAL_TZ) def load_book() -> tuple[float, list, list, datetime]: inst = get_payload( "https://www.okx.com/api/v5/public/instruments" f"?instType=SWAP&instId={INST}" )[0] if inst.get("ctType") != "linear" or inst.get("ctValCcy") != "BTC": raise RuntimeError(f"unexpected spec: {inst}") ct_val = float(inst["ctVal"]) ct_mult = float(inst["ctMult"]) book = get_payload( f"https://www.okx.com/api/v5/market/books?instId={INST}&sz={DEPTH_SZ}" )[0] def levels(raw: list) -> list[tuple[float, float, float, int]]: # each row: [price, size_contracts, deprecated, n_orders] out = [] for row in raw: px = float(row[0]) contracts = float(row[1]) btc = contracts * ct_val * ct_mult n_orders = int(float(row[3])) out.append((px, contracts, btc, n_orders)) return out asks = levels(book["asks"]) # increasing price bids = levels(book["bids"]) # decreasing price when = parse_ts(book["ts"]) print( f"{INST} ctVal={ct_val} BTC ctMult={ct_mult} " f"book ts={when.isoformat()} " f"levels bid={len(bids)} ask={len(asks)} (cap {DEPTH_SZ})" ) return ct_val, bids, asks, when get_payload sends a User-Agent and refuses a non-zero code before indexing data. The f prefixes on the instrument and book URLs matter: without them, {INST} and {DEPTH_SZ} are sent literally.
Walk the book and print slippage
def walk(levels, target_btc: float, mid: float, best: float) -> dict: remaining = target_btc notional = 0.0 filled = 0.0 levels_used = 0 last_px = None for px, _contracts, btc, _n in levels: if remaining <= 0: break take = min(btc, remaining) notional += take * px filled += take remaining -= take levels_used += 1 last_px = px vwap = notional / filled if filled else None return { "filled_btc": filled, "unfilled_btc": remaining, "exhausted": remaining > 1e-12, "vwap": vwap, "last_px": last_px, "levels_used": levels_used, "slip_vs_mid_bps": (vwap - mid) / mid * 1e4 if vwap else None, "slip_vs_best_bps": (vwap - best) / best * 1e4 if vwap else None, } def depth_within(levels, mid: float, bps: float, is_ask: bool) -> float: cap = mid * (1 + bps / 1e4) if is_ask else mid * (1 - bps / 1e4) total = 0.0 for px, _c, btc, _n in levels: if is_ask and px > cap + 1e-12: break if (not is_ask) and px < cap - 1e-12: break total += btc return total def report() -> None: _ct_val, bids, asks, _when = load_book() best_bid, best_ask = bids[0][0], asks[0][0] mid = (best_bid + best_ask) / 2.0 spread_bps = (best_ask - best_bid) / mid * 1e4 print( f"best bid={best_bid} best ask={best_ask} mid={mid:.2f} " f"spread={best_ask - best_bid:.1f} ({spread_bps:.4f} bps)" ) print( f"top bid depth={bids[0][2]:.4f} BTC " f"({bids[0][1]} cts, {bids[0][3]} orders) " f"top ask depth={asks[0][2]:.4f} BTC " f"({asks[0][1]} cts, {asks[0][3]} orders)" ) print( f"{'side':<4} {'size':>7} {'filled':>8} {'vwap':>10} {'last':>10} " f"{'lvls':>5} {'vs_mid':>9} {'vs_best':>9} exhausted" ) for size in SIZES_BTC: for side, levels, best in (("buy", asks, best_ask), ("sell", bids, best_bid)): r = walk(levels, size, mid, best) print( f"{side:<4} {size:7.2f} {r['filled_btc']:8.4f} " f"{r['vwap']:10.2f} {r['last_px']:10.1f} {r['levels_used']:5d} " f"{r['slip_vs_mid_bps']:8.4f} {r['slip_vs_best_bps']:8.4f} " f"{r['exhausted']}" ) print("Displayed BTC depth within X bps of mid (truncated by the 400-level cap):") for bps in BANDS_BPS: d_bid = depth_within(bids, mid, bps, is_ask=False) d_ask = depth_within(asks, mid, bps, is_ask=True) print(f" {bps:5.1f} bps bid {d_bid:10.4f} ask {d_ask:10.4f}") if __name__ == "__main__": report() On 15 November 2026 at 01:41:40 (Asia/Shanghai), a live run printed:
BTC-USDT-SWAP ctVal=0.01 BTC ctMult=1.0 book ts=2026-11-15T01:41:40.759000+08:00 levels bid=400 ask=400 (cap 400) best bid=65874.2 best ask=65874.3 mid=65874.25 spread=0.1 (0.0152 bps) top bid depth=3.3968 BTC (339.68 cts, 24 orders) top ask depth=0.9975 BTC (99.75 cts, 10 orders) side size filled vwap last lvls vs_mid vs_best exhausted buy 0.10 0.1000 65874.30 65874.3 1 0.0076 0.0000 False sell 0.10 0.1000 65874.20 65874.2 1 -0.0076 0.0000 False buy 0.50 0.5000 65874.30 65874.3 1 0.0076 0.0000 False sell 0.50 0.5000 65874.20 65874.2 1 -0.0076 0.0000 False buy 1.00 1.0000 65874.30 65877.4 12 0.0079 0.0003 False sell 1.00 1.0000 65874.20 65874.2 1 -0.0076 0.0000 False buy 5.00 5.0000 65881.91 65887.3 73 1.1623 1.1547 False sell 5.00 5.0000 65874.17 65874.1 2 -0.0125 -0.0049 False buy 10.00 10.0000 65884.64 65887.4 74 1.5777 1.5701 False sell 10.00 10.0000 65872.96 65869.4 32 -0.1953 -0.1877 False buy 25.00 25.0000 65886.95 65889.2 89 1.9278 1.9202 False sell 25.00 25.0000 65869.65 65866.0 50 -0.6984 -0.6909 False buy 50.00 50.0000 65889.63 65897.0 143 2.3344 2.3268 False sell 50.00 50.0000 65865.52 65857.9 104 -1.3246 -1.3170 False Displayed BTC depth within X bps of mid (truncated by the 400-level cap): 1.0 bps bid 15.2596 ask 1.1047 2.0 bps bid 42.8455 ask 12.1259 5.0 bps bid 95.9370 ask 71.1524 10.0 bps bid 166.0377 ask 135.2186 25.0 bps bid 166.0377 ask 135.2186 50.0 bps bid 166.0377 ask 135.2186 A few readings of this snapshot, none of them a rule about later books:
- The spread was 0.1 USDT, 0.0152 bps of mid — a single tick. Sizes of 0.10 and 0.50 BTC bought or sold entirely at the inside, so slippage versus mid was just half the spread.
- A 1 BTC buy needed 12 ask levels because the top ask held only 0.9975 BTC. The VWAP barely moved (0.0079 bps versus mid). The last price touched, 65,877.4, looks worse than the average, which is the usual gap between a worst print and a size-weighted fill.
- A 5 BTC buy walked 73 levels and printed 1.16 bps of slip versus mid — about seventy-five times the spread. The same-size sell walked only two levels and printed 0.01 bps. In this minute the bid side was heavier near mid; that asymmetry can reverse on the next request.
- Displayed depth stopped increasing between the 10 bp and 25 bp bands. That is the 400-level cap, not a claim that the venue has no book beyond 10 bps. Do not quote the 25 bp or 50 bp columns as measured depth.
What this script does not cover
Do not treat the VWAP as a guaranteed fill. The book moves, the request is delayed, and hidden or reserved liquidity is invisible here. Fees are excluded. The walk is a single snapshot, not a queue-position model.
Do not switch the size unit mid-script, and do not compare this linear book to an inverse contract without converting face value. If you lower DEPTH_SZ, say so in the depth table, because truncation arrives sooner.
Useful follow-ups: pull books twice a few seconds apart and compare VWAPs for the same size; or walk a documented impact notional and compare it to the venue’s premium-index definition, without treating that number as a trading signal.
Sources: OKX public API public/instruments and market/books for BTC-USDT-SWAP, retrieved 2026-11-15 01:41 (Asia/Shanghai).
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. Re-run the script and verify the venue’s current contract specifications before relying on any figure.
Disclaimer: Not financial advice. Educational / developer content only.

