# Partial fill: what it means and how to track it in code

URL: https://fincodelabs.com/en/blog/partial-fill-handling · Publisher: Fincode (Fincode Yazılım ve Teknoloji A.Ş.), Istanbul, Turkey · Category: Exchange APIs · Published: 2026-11-06 · Updated: 2026-11-06

A partial fill executes only part of an order. How Binance, Bybit, Kraken, Coinbase, Alpaca, IBKR and FIX report fills (6 Nov 2026), with tested Python.

**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](https://fincodelabs.com/en/blog/time-in-force) ends.
- **Market or IOC order:** the rest ends at once. Binance's enum page, read 6 November 2026, uses `EXPIRED` for 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.

**Fill reporting in official API documents, read 6 November 2026**

| 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](https://fincodelabs.com/en/blog/binance-user-data-stream).

## 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_fill` and `fill` events carry that fill's `price` and `qty` and the resulting `position_qty`; examples show an `execution_id` the 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 `orderStatus` messages typical and advise watching `execDetails` too, where a correction arrives with an `execID` that 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 `exchangeInfo` for BTCUSDT, read 6 November 2026, set `LOT_SIZE` minQty and stepSize at 0.00001 and `NOTIONAL` minNotional 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](https://fincodelabs.com/en/blog/reduce-only-order), 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 `Filled` order messages, one execution message can carry several fills for an order, and simultaneous fills share one `seq`, which can repeat across symbols.
- **Coinbase**: `trade_id` is not unique across adjusted fills (`REVERSAL`, `CORRECTION`, `SYNTHETIC`), so key on `entry_id`; WebSocket messages carry a per-connection `sequence_num` to detect drops and reordering.
- **Kraken**: the executions page contradicts itself on whether the snapshot includes the latest 50 fills by default, so set `snap_trades` explicitly.
- **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](https://fincodelabs.com/en/blog/websocket-heartbeat-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.

```python
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](https://fincodelabs.com/en/blog/client-order-id) are left out; see the [order management system guide](https://fincodelabs.com/en/blog/crypto-order-management-system).

## Partial fill failure cases to test before going live

Run each case on a testnet or mock first.

**Failure cases and how to test them**

| 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](https://fincodelabs.com/en/blog/position-reconciliation). For integration work in general, see Fincode's [exchange API integration](https://fincodelabs.com/en/services/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)](https://github.com/binance/binance-spot-api-docs/blob/master/user-data-stream.md)
- [Binance Spot API documentation: Enum definitions (order status, execution types)](https://github.com/binance/binance-spot-api-docs/blob/master/enums.md)
- [Binance Spot API documentation: REST API, Account trade list (myTrades)](https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md)
- [Binance Spot exchangeInfo for BTCUSDT, live LOT_SIZE and NOTIONAL filters](https://api.binance.com/api/v3/exchangeInfo?symbol=BTCUSDT)
- [Bybit V5 API: WebSocket execution topic](https://bybit-exchange.github.io/docs/v5/websocket/private/execution)
- [Bybit V5 API: WebSocket order topic](https://bybit-exchange.github.io/docs/v5/websocket/private/order)
- [Bybit V5 API: Enums Definitions (orderStatus)](https://bybit-exchange.github.io/docs/v5/enum)
- [Kraken API: WebSocket v2 executions channel](https://docs.kraken.com/exchange/api-reference/spot-websocket-v2/executions)
- [Coinbase Advanced Trade WebSocket AsyncAPI specification (user channel order fields)](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/advanced-trade-asyncapi.json)
- [Coinbase Advanced Trade API: List Fills](https://docs.cdp.coinbase.com/api-reference/advanced-trade-api/rest-api/orders/list-fills)
- [Alpaca: WebSocket Streaming, trade_updates events (updated 24 September 2025)](https://docs.alpaca.markets/us/docs/websocket-streaming)
- [Interactive Brokers TWS API documentation, older GitHub-hosted version: Placing Orders (orderStatus)](https://interactivebrokers.github.io/tws-api/order_submission.html)
- [Interactive Brokers TWS API documentation, older GitHub-hosted version: Executions and Commissions](https://interactivebrokers.github.io/tws-api/executions_commissions.html)
- [FIX Latest on Orchimate, where FIX Trading Community FIXimate links now redirect: ExecutionReport](https://orchimate.org/fixtrading/fix-latest/messages/ExecutionReport)
- [FIX Latest on Orchimate: OrdStatusCodeSet](https://orchimate.org/fixtrading/fix-latest/codeSets/OrdStatusCodeSet)

> 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](https://fincodelabs.com/en/editorial-policy).

---

Published by Fincode (Fincode Yazılım ve Teknoloji A.Ş.), a software company in Istanbul, Turkey: https://fincodelabs.com/en/blog/partial-fill-handling

Cite as: Fincode, "Partial fill: what it means and how to track it in code", updated November 6, 2026, https://fincodelabs.com/en/blog/partial-fill-handling
