Short answer: A partial fill means only part of an order's quantity has executed; the rest is still working or has been canceled or expired. Venues report each fill plus a cumulative total, and trading software should sum the fills itself, keyed by the venue's fill ID, so a repeated, late or corrected event is never counted twice.
Partial fills are routine; the bugs come from code that assumes one order means one fill. Below: how venues report fills and a Python tracker tested on a local mock.
This is a technical guide, not a recommendation of any venue or strategy; automated trading can lose money, and rules depend on your country and each venue's terms. For readers in Turkey: Article 99/A of Capital Markets Law No. 6362 treats activity by foreign crypto-asset platforms directed at Turkish residents as unauthorized.
What does partially filled mean?
A partially filled order has a filled part and an unfilled remainder, which may or may not still be working.
- Resting limit order: the rest waits until it fills, you cancel it or its time in force ends.
- Market or IOC order: the rest ends at once. Binance's enum page, read 6 November 2026, uses
EXPIREDfor limit IOC and market orders that partially fill, so an expired order can hold a position. - Cancel after a fill: the filled part stays.
Hypothetical example. A bot buys 0.02 BTC with a 100,000 USDT limit. Fills of 0.006 at 99,990, 0.01 at 100,000, 0.00196 at 99,995 and 0.002 at 100,000 arrive, then it cancels: 0.01996 BTC filled at an average of 99,996.50, and 0.00004 BTC left, no longer working. The code below reuses these invented numbers.
How exchanges report a partial fill: statuses and fields
Every venue reports single fills and order totals under different names, so a parser for one venue misreads another.
| Venue and feed | Partly filled shows as | One fill | Order totals | Fill ID; fee |
|---|---|---|---|---|
Binance Spot, executionReport | X = PARTIALLY_FILLED | x = TRADE, l, L | z; Z in quote, so average = Z / z | t; n ("Commission amount"), N |
Bybit V5, order and execution | PartiallyFilled; spot only: PartiallyFilledCanceled | execQty, execPrice | cumExecQty, avgPrice, leavesQty | execId; execFee, feeCurrency |
Kraken Spot WebSocket v2, executions | partially_filled | exec_type = trade, last_qty, last_price | cum_qty, cum_cost, avg_price | exec_id; fees |
Coinbase Advanced Trade, user channel and List Fills | None in the status list | List Fills only: size, price | cumulative_quantity, leaves_quantity, avg_price | entry_id; commission |
Alpaca, trade_updates | Event partial_fill | qty, price | The order object; examples show filled_qty, filled_avg_price | Not documented |
| Interactive Brokers TWS API | orderStatus with amounts filled and pending | execDetails | orderStatus | execID; commissionReport |
| FIX Execution Report (35=8) | OrdStatus (39) = 1 | LastQty (32), LastPx (31) | CumQty (14), LeavesQty (151), AvgPx (6) | ExecID (17) |
Coinbase gives cumulative_quantity in base currency but leaves_quantity in the currency the order was placed in. Kraken currently expresses fees in quote currency only; Binance and Bybit name a fee asset per fill, so sum fees per asset. For Binance events, see the user data stream guide.
Partially filled orders in stocks
Stock broker APIs report partial fills the same way, plus repeated statuses and corrected executions.
- Alpaca (streaming page updated 24 September 2025):
partial_fillandfillevents carry that fill'spriceandqtyand the resultingposition_qty; examples show anexecution_idthe field list omits, so confirm it in paper trading before keying on it. - Interactive Brokers: its older TWS API pages, now marked deprecated, call repeated identical
orderStatusmessages typical and advise watchingexecDetailstoo, where a correction arrives with anexecIDthat differs only after the final period.
Rules, account types and API access vary by broker and country.
What bot code must track after a partial fill
Track five things per order, all from the fills.
- Filled quantity: your sum of fills, checked against the venue's cumulative field; a higher cumulative means a fill you missed.
- Average price: from the fills, not the limit price.
- Fees by asset: a fee taken in the asset you bought leaves you holding less than the filled quantity.
- Remainder: it may be too small to place again. Binance's
exchangeInfofor BTCUSDT, read 6 November 2026, setLOT_SIZEminQty and stepSize at 0.00001 andNOTIONALminNotional at 5 USDT, applied to market orders too. - Protective orders: a stop sized on the order quantity can fail for lack of balance on spot or, on derivatives without reduce only, open an opposite position.
Duplicate and out-of-order fill events
Expect a fill to arrive twice or late: some venues say so, and none rule it out.
- Bybit: a cancel accepted alongside a fill can produce two
Filledorder messages, one execution message can carry several fills for an order, and simultaneous fills share oneseq, which can repeat across symbols. - Coinbase:
trade_idis not unique across adjusted fills (REVERSAL,CORRECTION,SYNTHETIC), so key onentry_id; WebSocket messages carry a per-connectionsequence_numto detect drops and reordering. - Kraken: the executions page contradicts itself on whether the snapshot includes the latest 50 fills by default, so set
snap_tradesexplicitly. - Binance: the user data stream page says nothing either way.
The rules: key fills on the venue's fill ID, scoped by symbol; refuse empty IDs (Binance's example has t = -1 on a non-trade event); treat a known ID with new contents as a break, not an update; never lower a cumulative or reopen a final order. After a reconnect, or a final status with a gap, query the order and read the trade list: Binance's myTrades costs 5 weight with orderId and returns 500 trades by default, 1,000 at most, per its REST page read 6 November 2026.
A tested partial fill tracker in Python
The tracker keys fills by trade ID, keeps numbers in Decimal (a float turns 1e-400 into 0.0, so parse JSON with parse_float=Decimal), checks its sum against the venue's cumulative quantity, reads the order and its trade rows when a final status leaves a gap, and raises Reconcile on anything it cannot explain, such as an error body instead of a trade list. The tracker assumes n is per fill; check that on the Spot testnet.
Setup: Python 3.11.3 on Windows 11, standard library only, run on 6 November 2026 against invented events with Binance's executionReport field names and an in-memory list shaped like myTrades; no exchange or key was used.
from decimal import Decimal as D
FINAL = {"FILLED", "CANCELED", "EXPIRED", "EXPIRED_IN_MATCH", "REJECTED"}
class Reconcile(Exception):
pass
def num(raw, name, zero_ok=False):
try:
v = D(str(raw)) # Decimal, never float
except ArithmeticError:
v = D("NaN")
if not v.is_finite() or v < 0 or (v == 0 and not zero_ok):
raise ValueError(f"{name}={repr(raw)} rejected")
return v
class Order:
def __init__(self, oid, qty):
self.oid, self.qty, self.status, self.fills, self.cum = oid, num(qty, "qty"), "NEW", {}, D(0)
def filled(self):
return sum((f[0] for f in self.fills.values()), D(0))
def add_fill(self, fid, qty, price, fee, asset):
if str(fid).strip() in ("None", "", "-1"):
raise ValueError("fill has no ID")
new = (num(qty, "qty"), num(price, "price"), num(fee, "fee", True), asset)
old = self.fills.get(str(fid))
if old == new:
return "duplicate ignored"
if old:
raise Reconcile(f"fill {fid} repeated with different data")
if self.filled() + new[0] > self.qty:
raise Reconcile(f"fill {fid} would overfill {self.qty}")
self.fills[str(fid)] = new
return "applied"
def on_report(self, e): # one Binance Spot executionReport
done = "no fill"
if e.get("x") == "TRADE":
done = self.add_fill(e.get("t"), e.get("l"), e.get("L"), e.get("n"), e.get("N"))
if self.status in FINAL: # never reopen a final order
done += f", late {e.get('X')} ignored"
else:
self.status, self.cum = e.get("X"), max(self.cum, num(e.get("z"), "z", True))
done += f", {self.status}"
if self.filled() > self.cum:
raise Reconcile(f"fills exceed the venue's {self.cum}")
return f"t={e.get('t')} {done}, gap {self.cum - self.filled()}"
def reconcile(self, executed, rows): # GET /api/v3/order, then myTrades
if not isinstance(rows, list):
raise Reconcile(f"no trade list: {rows}")
self.cum = max(self.cum, num(executed, "executedQty", True))
for t in rows:
if t.get("orderId") == self.oid:
self.add_fill(t.get("id"), t.get("qty"), t.get("price"),
t.get("commission"), t.get("commissionAsset"))
if self.cum - self.filled():
raise Reconcile(f"still {self.cum - self.filled()} apart")
return "trade list read, gap 0"
F = {11: ("0.00600", "99990.00", "0.00000600", "BTC"),
12: ("0.01000", "100000.00", "0.00720000", "BNB"),
13: ("0.00196", "99995.00", "0.00141100", "BNB"),
14: ("0.00200", "100000.00", "0.00144000", "BNB")}
TRADES = [dict(id=i, orderId=7, qty=q, price=p, commission=n, commissionAsset=a)
for i, (q, p, n, a) in F.items()]
fill = lambda t, z: dict(x="TRADE", X="PARTIALLY_FILLED", t=t, z=z, **dict(zip("lLnN", F[t])))
o = Order(7, "0.02000")
for e in [fill(11, "0.00600"), fill(12, "0.01600"), fill(12, "0.01600"), # 12 replayed
fill(14, "0.01996"), dict(x="CANCELED", X="CANCELED", t=-1, z="0.01996"),
fill(13, "0.01796")]: # 13 arrives last
print(o.on_report(e))
if o.status in FINAL and o.cum - o.filled():
print(o.reconcile("0.01996", TRADES))
fees = {}
for q, p, n, a in o.fills.values():
fees[a] = fees.get(a, D(0)) + n
avg = sum(q * p for q, p, _, _ in o.fills.values()) / o.filled()
left, px = o.qty - o.filled(), D("100000")
print(f"{o.status}: filled {o.filled()}, unfilled {left}, average {avg.quantize(D('0.01'))}")
print("fees", {a: str(n) for a, n in fees.items()}, "BTC held", o.filled() - fees.get("BTC", 0))
ok = left >= D("0.00001") and left % D("0.00001") == 0 and left * px >= 5
print(f"unfilled {left} at {px} is {left * px} USDT, can re-place: {ok}")
for name, e in [("blank trade ID", fill(13, "0.01997") | {"t": " "}),
("infinite price", fill(13, "0.01997") | {"t": 15, "L": "Infinity"}),
("same ID, other size", fill(12, "0.01600") | {"l": "0.00900"}),
("overfill", fill(12, "0.02096") | {"t": 16, "l": "0.00100"})]:
try:
o.on_report(e)
except (ValueError, Reconcile) as err:
print(f"{name}: {type(err).__name__}: {err}")
try:
o.reconcile("0.01996", {"code": -2015})
except Reconcile as err:
print("error body:", err)Output:
t=11 applied, PARTIALLY_FILLED, gap 0.00000
t=12 applied, PARTIALLY_FILLED, gap 0.00000
t=12 duplicate ignored, PARTIALLY_FILLED, gap 0.00000
t=14 applied, PARTIALLY_FILLED, gap 0.00196
t=-1 no fill, CANCELED, gap 0.00196
trade list read, gap 0
t=13 duplicate ignored, late PARTIALLY_FILLED ignored, gap 0.00000
CANCELED: filled 0.01996, unfilled 0.00004, average 99996.50
fees {'BTC': '0.00000600', 'BNB': '0.01005100'} BTC held 0.01995400
unfilled 0.00004 at 100000 is 4.00000 USDT, can re-place: False
blank trade ID: ValueError: fill has no ID
infinite price: ValueError: price='Infinity' rejected
same ID, other size: Reconcile: fill 12 repeated with different data
overfill: Reconcile: fill 16 would overfill 0.02000
error body: no trade list: {'code': -2015}Trade 13 came from the trade list first, so its late event changes nothing; 0.00004 BTC at the hypothetical price is 4 USDT, under the 5 USDT minimum. When fetching, check the HTTP status first, so a rejected key is not retried as a network error. Persistence and client order IDs are left out; see the order management system guide.
Partial fill failure cases to test before going live
Run each case on a testnet or mock first.
| Case | What goes wrong | Test |
|---|---|---|
| IOC or market order partly filled | Ends EXPIRED (Binance) or Cancelled (Bybit derivatives) and is read as no trade | Final event with a cumulative above 0 |
| Remainder below the minimum | Placing it again fails in a loop | Check lot size and notional first |
| Fill after a cancel request | Position larger than the bot believes | Deliver a fill between cancel and confirmation |
| Fee in the bought asset | A sell for the filled quantity fails on balance | Compare the net amount with the balance event |
| Restart mid-order | Fills during downtime are missed | Restart between two fills, then rebuild from the trade list |
| Corrected or reversed fill | Counted as a second fill | Inject a new ID that refers to an old one, as FIX ExecRefID (19) does |
If the bot and the venue still disagree, stop and follow position reconciliation. For integration work in general, see Fincode's exchange API integration page; naming Binance, Bybit, Kraken, Coinbase, Alpaca or Interactive Brokers here does not mean Fincode integrates them or has built on them.
Frequently asked questions
Is PARTIALLY_FILLED a final status?
No. On Binance the rest may still trade; the order usually ends as FILLED, CANCELED or EXPIRED, and the last two can still hold filled quantity.
Do you pay a fee on each partial fill?
Crypto venues report a fee with each fill, such as Bybit's execFee and the commission on each Binance myTrades row. Whether a stock broker charges per execution or per order depends on its fee schedule.
How do I calculate the average price of a partially filled order?
Divide the sum of price times quantity over all fills by the filled quantity. On Binance, Z divided by z gives the same figure.
Can you avoid a partial fill?
A fill-or-kill (FOK) order cannot partly fill: Binance's enum page says it expires if the full quantity cannot fill on execution, so it may not fill at all.
Sources
- Binance Spot API documentation: User Data Stream (executionReport fields) (opens in a new tab)
- Binance Spot API documentation: Enum definitions (order status, execution types) (opens in a new tab)
- Binance Spot API documentation: REST API, Account trade list (myTrades) (opens in a new tab)
- Binance Spot exchangeInfo for BTCUSDT, live LOT_SIZE and NOTIONAL filters (opens in a new tab)
- Bybit V5 API: WebSocket execution topic (opens in a new tab)
- Bybit V5 API: WebSocket order topic (opens in a new tab)
- Bybit V5 API: Enums Definitions (orderStatus) (opens in a new tab)
- Kraken API: WebSocket v2 executions channel (opens in a new tab)
- Coinbase Advanced Trade WebSocket AsyncAPI specification (user channel order fields) (opens in a new tab)
- Coinbase Advanced Trade API: List Fills (opens in a new tab)
- Alpaca: WebSocket Streaming, trade_updates events (updated 24 September 2025) (opens in a new tab)
- Interactive Brokers TWS API documentation, older GitHub-hosted version: Placing Orders (orderStatus) (opens in a new tab)
- Interactive Brokers TWS API documentation, older GitHub-hosted version: Executions and Commissions (opens in a new tab)
- FIX Latest on Orchimate, where FIX Trading Community FIXimate links now redirect: ExecutionReport (opens in a new tab)
- FIX Latest on Orchimate: OrdStatusCodeSet (opens in a new tab)
This guide is general information about software and markets, not investment, legal or tax advice. Automated trading can lose money, past or backtested performance does not guarantee future results, and the rules that apply depend on your jurisdiction and your exchange's terms. Sources, checks and corrections: see the editorial policy.