Code Playbook · Python

Position Sizing From Live Crowding Data in Python

Standard position sizing divides a risk budget by a stop distance and stops there. That arithmetic quietly assumes the tail risk around your stop is the same from one trade to the next, and on leveraged perpetuals it is not. This playbook wires one public endpoint into a sizing function so that a one-sided book scales your notional down — and never up.

The problem: constant sizing, non-constant tail

A fixed-fractional sizing rule — risk 1% of equity, stop 2% away, therefore notional is half of equity — is the right starting point and it is not up for debate here. The issue is what it leaves out. That rule produces the same size whether the leveraged book is balanced or whether nearly all of it is stacked on the side you are about to join.

Those are not the same trade. When leveraged positioning is one-sided, the forced exits available to the market are all in one direction: liquidations of the crowded side push price the way that triggered them, which reaches the next cluster of liquidation levels, which triggers again. Your stop distance did not change. The probability that price traverses it in a single violent leg did.

Derivatives venues publish enough to measure that one-sidedness directly. This playbook reads those fields from one endpoint and folds them into a sizing function as a haircut. The conceptual background — what each ratio counts and why the balanced-notional objection does not apply to account ratios — is written up in this guide to crowding and open interest if you want the reasoning before the code.

Prerequisites

The screener endpoint

One GET returns the cross-venue derivatives state per symbol, aggregated across Bybit, Binance and Hyperliquid:

bash
GET https://api.smartmoneyapi.com/v1/derivatives/screener?limit=10&sort_by=oi_usd

Without a key the response is capped at the top ten symbols by open interest and carries "limited": true; total_count still reports how many symbols are tracked in full (518 at the time of writing). Sending an X-API-Key header on a Trader tier or above returns the whole list.

json
{ "symbols": [ { "symbol": "BTC", "oi_usd": 15607718574.39, "oi_change_1h_pct": -1.39, "oi_change_24h_pct": null, "funding_rate": 7.3e-05, "funding_annualized": 7.99, "long_short_ratio": 0.9482, "top_trader_lsr": 2.0816, "taker_ratio": 1.0999, "funding_state": "neutral", "crowding_state": "neutral", "crowding_severity": "normal", "ts": 1787586348 } ], "total_count": 518, "sort_by": "oi_usd", "updated": 1787587941, "public": true, "limited": true }

Which fields measure what

These are four different measurements and conflating them is the usual source of bad conclusions. Every perpetual has exactly as much long notional open as short notional open, so none of the ratios below can mean “more longs than shorts” in a notional sense — each one is counting something narrower.

FieldCountsRead as
oi_usdOpen notional, USDHow much leveraged capital can be force-closed
oi_change_1h_pctChange in that notionalLarge negatives are the footprint of positions being closed en masse
long_short_ratioAccounts, not sizeRetail headcount lean; weights a small account like a large one
top_trader_lsrLargest accounts onlyClosest public proxy for where size sits; smaller, noisier sample
taker_ratioAggressive buy vs sell flowRecent urgency over a window, not standing exposure
funding_annualizedCost of holding, % / yearThe market’s own price for sitting on the crowded side
The two ratios disagreeing is informative in itself. In a snapshot taken 24 August 2026, BTC showed long_short_ratio 0.95 (headcount essentially balanced) alongside top_trader_lsr 2.08 (large accounts leaning long). That is an ambiguous book, and a crowding argument in either direction is weak there. The sizing rule below deliberately requires both measures to agree before it takes anything off.

Python: crowding-adjusted sizing

The structure is: compute the baseline unconditionally, score the book, then apply a haircut that is clamped so it can only reduce. The scoring thresholds are risk-management judgement calls, not fitted parameters — they are there to be edited to your own tolerance.

python
import requests BASE = "https://api.smartmoneyapi.com" TIMEOUT = 15 def fetch_screener(limit=10): """Public view: top `limit` symbols by open interest, no key required.""" r = requests.get( f"{BASE}/v1/derivatives/screener", params={"limit": limit, "sort_by": "oi_usd"}, timeout=TIMEOUT, ) r.raise_for_status() return {row["symbol"]: row for row in r.json().get("symbols", [])} def crowding_score(row, side): """0.0 = balanced book, 1.0 = one-sided against you on every measure. Only counts evidence that you are joining the CROWD. Being on the uncrowded side scores 0 -- it is not treated as an advantage, because positioning is not a directional forecast. """ assert side in ("long", "short") score, notes = 0.0, [] acct = row.get("long_short_ratio") top = row.get("top_trader_lsr") fund = row.get("funding_annualized") oi_1h = row.get("oi_change_1h_pct") # Lean is expressed the same way for both sides: >1 means long-heavy. def leans_with_me(ratio): if ratio is None: return None return ratio > 1.0 if side == "long" else ratio < 1.0 def magnitude(ratio): # Distance from balanced, symmetric in log-ish terms. return abs(ratio - 1.0) if ratio >= 1.0 else abs(1.0 / ratio - 1.0) # Require BOTH populations to lean your way before charging for crowding. # Headcount and size disagreeing is an ambiguous book, not a crowded one. if leans_with_me(acct) and leans_with_me(top): m = min(magnitude(acct), magnitude(top)) if m >= 1.0: score += 0.40 notes.append(f"both ratios lean {side} hard (acct {acct:.2f}, top {top:.2f})") elif m >= 0.5: score += 0.20 notes.append(f"both ratios lean {side} (acct {acct:.2f}, top {top:.2f})") # Funding charging your side is a direct, ongoing cost of being the crowd. if fund is not None: paying = fund > 0 if side == "long" else fund < 0 if paying and abs(fund) >= 20: score += 0.30 notes.append(f"funding {fund:.1f}%/yr charged to {side}s") elif paying and abs(fund) >= 8: score += 0.15 notes.append(f"funding {fund:.1f}%/yr charged to {side}s") # A big one-hour drop in open interest means a large slice of the # leveraged book was just removed, usually not voluntarily. if oi_1h is not None and oi_1h <= -10: score += 0.30 notes.append(f"open interest {oi_1h:.1f}% in 1h -- book just got flushed") return min(score, 1.0), notes def size_position(equity, risk_pct, entry, stop, row, side, max_haircut=0.50): """Baseline fixed-fractional size, then a downward-only crowding haircut.""" stop_dist = abs(entry - stop) / entry if stop_dist <= 0: raise ValueError("stop must differ from entry") baseline_notional = (equity * risk_pct) / stop_dist score, notes = crowding_score(row, side) haircut = score * max_haircut # score 1.0 -> half size adjusted = baseline_notional * (1 - haircut) return { "baseline_notional": baseline_notional, "adjusted_notional": adjusted, "crowding_score": score, "haircut_pct": haircut * 100, "notes": notes, } if __name__ == "__main__": rows = fetch_screener() # Example: a long on the symbol you were going to trade anyway. symbol, side = "DOGE", "long" row = rows.get(symbol) if row is None: raise SystemExit(f"{symbol} not in the public top-10 view -- use a key for the full list") result = size_position( equity=25_000, risk_pct=0.01, # 1% of equity at risk entry=0.2100, stop=0.2016, # 4% away row=row, side=side, ) print(f"{symbol} {side}") print(f" baseline notional : ${result['baseline_notional']:,.0f}") print(f" crowding score : {result['crowding_score']:.2f}") print(f" haircut : {result['haircut_pct']:.0f}%") print(f" sized notional : ${result['adjusted_notional']:,.0f}") for n in result["notes"]: print(f" - {n}")

Expected output

Against a balanced book the script prints a zero score and hands back the baseline unchanged, which is the point — most of the time it should do nothing. Against a one-sided one it prints the reasons alongside the reduction:

text
DOGE long baseline notional : $6,250 crowding score : 0.85 haircut : 43% sized notional : $3,594 - both ratios lean long hard (acct 2.92, top 4.75) - funding 10.9%/yr charged to longs - open interest -16.8% in 1h -- book just got flushed

Those figures come from feeding the function a real screener row captured at 15:45 UTC on 24 August 2026, in which DOGE carried the hardest long lean of the top ten on both ratios, the highest annualized funding of the group at about 11%, and had shed 16.8% of its open interest in the preceding hour. Three independent reasons stack to a 0.85 score and a 43% reduction. Those are historical observations from one moment, not a forecast, and the numbers will be different when you run it live.

The contrast is the useful part. The same function on the BTC row from that snapshot — account ratio 0.95 against a top-trader ratio of 2.08, funding at 8.0%, open interest down 1.4% — scores 0.0 and returns the baseline untouched, because headcount and size disagreed and the both-ratios gate never opened. Most of the time that is what you want this to do: nothing.

What this does not do

It does not predict direction, and it is built so that it cannot accidentally start to. The haircut is clamped to be non-positive: there is no branch that increases size for being on the uncrowded side. That asymmetry is deliberate. Crowded positioning is a statement about the variance of the trade, not its expected return — it can persist for weeks in a trending market while every contrarian who faded it is stopped out.

We ran pre-registered studies on whether data of this family forecasts direction and published them, including the ones that found no durable edge. Do not convert this script into an entry trigger. It sizes a trade you had already decided to take for your own reasons.

Two further limits worth stating plainly. The ratios are venue-specific and aggregation across venues smooths but does not eliminate that — for anything touching your own funding and liquidation, use the venue you are actually trading on. And a smaller position at the same leverage has exactly the same liquidation price, so a haircut does not widen your buffer; it only reduces what you lose if the buffer fails.

What to build next

Sensible extensions once the basic function is wired in:

See the full API documentation for every field the screener returns, or browse the rest of the code playbooks hub for more integration examples.

Beyond the public top ten

The public view covers the ten largest symbols by open interest. A free key raises your daily call cap on the endpoints it covers; a Trader key returns the full tracked symbol list from the same base URL.

Create a free account
Start free — 200 calls/day, no card

Get live whale flow, funding, open interest and on-chain data across 3 exchanges from one API. Free tier, no credit card, upgrade any time.

Start free →
Try the live API console → (no account needed)