Trading bot partial fills: reconciliation without overtrading
An order is not a yes-or-no event. It can be accepted, partly filled at several prices, cancelled with some exposure remaining, or completed while your acknowledgement is lost. A production bot must treat execution as a state machine. This guide shows how to calculate the real fill, resize protection, prevent duplicate retries, and recover the truth after a restart.
What a partial fill changes
A partial fill means the venue executed less than the requested quantity. If a bot sends a limit buy for 100 shares and receives fills of 30 at $50.00 and 20 at $50.04, it owns 50 shares, not zero and not 100. The remaining 50 may still be live. The bot's decisions must use confirmed exposure rather than the original request.
The volume-weighted average price is:
fill mathaverage price = Σ(fill quantity × fill price) / Σ(fill quantity)
(30 × $50.00 + 20 × $50.04) / 50 = $50.016
Fees can arrive per fill and may be charged in the quote currency, base asset, or another token. Store the raw fill records, not just a rounded average. They are needed for exact position cost, P&L, and later audits. Read market vs limit orders for why liquidity and order type change fill behavior.
Model the order lifecycle explicitly
A local boolean such as order_complete cannot describe reality. Keep the venue order ID and your own unique client order ID, then track requested quantity, cumulative executed quantity, remaining quantity, average price, fees, and the latest venue status.
| State | What it means | Safe bot behavior |
|---|---|---|
| Pending submit | Request sent; outcome not yet known | Do not send a replacement until queried by client ID |
| Open | Accepted with no confirmed fill | Monitor events and expiry policy |
| Partially filled | Some exposure exists; remainder may be live | Protect actual exposure and track the remainder |
| Filled | Requested quantity completed | Finalize cost and protection |
| Cancelled/expired | No further fills expected, but earlier fills remain | Manage the executed quantity; never assume flat |
| Unknown | Local and venue evidence disagree or is incomplete | Block new risk and reconcile |
The venue may accept an order and the response may be lost on the network. Retrying the submit can double the position. Give each intent a deterministic client order ID, look it up after an ambiguous result, and only create a new ID after proving the first intent was not accepted.
WebSocket execution events are fast but not infallible: connections drop, messages can be delayed, and clients can restart. REST snapshots are slower but useful for repair. A robust bot consumes streaming events for normal operation and periodically checks a venue snapshot to find gaps.
Resize stops and bracket orders to confirmed exposure
Partial entries create a protection problem. If 40% of an entry fills, a stop for the full intended quantity may sell more than the bot owns and open an unintended short. Waiting for a complete fill before placing any stop leaves the first 40% unprotected. The safest policy depends on venue features, but the invariant is simple: protective order quantity should never exceed confirmed reducible exposure.
- Receive a fill and update cumulative entry quantity.
- Fetch or calculate the authoritative net position.
- Create or resize reduce-only stop and take-profit orders for that quantity.
- Confirm the new protection before removing the old protection where atomic amendment is unavailable.
- When an exit partly fills, reduce its sibling order so the combined exits cannot exceed the remaining position.
Some venues offer native bracket, OCO, or attached orders; their behavior on partial fills differs. Test the exact broker implementation rather than assuming the labels mean the same thing everywhere. The concepts in order types explained and order book depth help predict when partial execution is likely.
Build reconciliation around authoritative facts
Reconciliation asks, “What does the venue say exists right now?” and compares that with local expectations. Run it on startup, on a timer, after reconnecting a stream, after an ambiguous submit or cancel, and before clearing a kill switch.
python · conceptual reconciliationdef reconcile(venue, local):
remote_orders = venue.fetch_open_orders()
remote_fills = venue.fetch_recent_fills()
positions = venue.fetch_positions()
snapshot = merge_by_venue_and_client_id(
remote_orders, remote_fills, positions
)
differences = compare(local, snapshot)
for diff in differences:
if diff.increases_unknown_risk:
safety_gate.trip("reconciliation_mismatch")
else:
record_and_repair(diff)
Compare at least these invariants:
- every local live order has a matching venue order or terminal fill/cancel event;
- every venue order belongs to a known strategy, manual workflow, or quarantine list;
- the sum of fills agrees with the venue position after fees and transfers;
- protective exit quantity does not exceed the position it can reduce;
- no supposedly closed trade still has a resting order;
- no event page is incomplete because pagination stopped early.
Do not “repair” uncertain state by automatically cancelling everything. First determine whether the snapshot is complete and current. A partial API result is not proof that an order disappeared. Retry read-only queries with bounded rate-limit-aware backoff, then escalate if the evidence remains inconsistent.
Failure tests worth running
| Injected failure | Expected result |
|---|---|
| Entry fills 30%, then expires | Bot manages 30%; it does not wait for or record 100% |
| Submit response times out after acceptance | Client ID lookup finds the original; no duplicate is sent |
| Exit partly fills while stop remains live | Stop quantity shrinks to the true remaining position |
| WebSocket drops one fill event | Periodic snapshot repairs cumulative fill and average price |
| Process restarts with a live order | Startup reconciliation reconstructs it before signals resume |
| Trader changes the account manually | Difference is quarantined or adopted by an explicit policy |
Log every state transition with IDs and raw venue status. An order journal makes a race condition reproducible and supports the broader practices in trading bot logging and monitoring. Run the full lifecycle in a sandbox, then at the smallest possible live size; paper venues often simulate fills more cleanly than real books.
Frequently asked questions
What is a partial fill in automated trading?
A partial fill occurs when only part of an order executes. The remaining quantity may continue resting, expire, or be cancelled depending on the order's time-in-force. A bot must track cumulative filled quantity, remaining quantity, average fill price, fees, and final status.
How should a trading bot protect a partially filled position?
Protection should match confirmed exposure. After each fill, calculate the actual net position and create or resize the stop and take-profit quantities without leaving a gap or exceeding the position. Confirm the replacement before cancelling old protection whenever the venue's order model allows it.
Can retrying an order create a duplicate trade?
Yes. A request can time out after the venue accepted it, so blindly submitting again may create a second order. Use a unique client order ID, query the original ID after ambiguous failures, and make order submission idempotent before any retry.
What is trading bot order reconciliation?
Reconciliation compares the bot's local records with authoritative venue data: open orders, recent fills, positions, and balances. Differences are classified and repaired or escalated. It should run at startup, periodically, after uncertain requests, and before a halted bot resumes.