Partial fills: the quiet way trading bots lose money
A limit order that fills halfway leaves you with a position nobody planned, a stop sized for a position you do not have, and an average price your code has not recomputed.
Vizanix engineering · about the author
- SECTION
- Bybit API
- PUBLISHED
- 2026-08-28
- CHAPTERS
- 6
- READ NEXT
- 3
- LANGUAGE
- written in English
Ask a developer how their bot handles a partial fill and you learn most of what you need to know about it. The answers cluster into three groups: those who have never thought about it, those who wait for the order's time-to-live to expire, and those who have been burned.
What actually happens
You place a limit buy for 10 units. The market touches your price, takes 3, and moves away. You now hold 3 units and have a resting order for 7. Three things are true at once, and each one breaks a naive bot:
- You have an unplanned position. The strategy sized risk for 10 units. You have 3. Your stop distance, computed from the intended size, is now wrong in the safe direction — but your take-profit legs, sized as percentages of 10, are wrong in a way that will try to sell units you never bought.
- You have exposure to the remainder. The 7 can fill at any moment, including at a price where the setup is no longer valid.
- Your average price is not your limit price. If the remainder fills later at a different level, the position's average moves, and every level computed from entry — stop, targets, break-even — has to move with it.
The wrong fixes
Waiting for the TTL. The most common approach: let the order live out its time-to-live and deal with whatever position exists at the end. This leaves the filled portion completely unprotected for the whole window. If the TTL is one candle, that is one candle of naked risk on every partial fill.
Cancelling and forgetting. Cancelling the remainder is right; forgetting to bracket the filled part is how a position ends up with no stop at all. This is the failure mode that shows up as “the bot had a position I did not know about”.
Sizing the bracket from the intended quantity. If the take-profit legs were computed as fixed quantities from the planned 10 units, submitting them against a 3-unit position produces reduce-only rejections at best and, on a venue that allows it, an unintended short at worst.
What we do
In our Bybit executor the rule is explicit: a partially filled entry cancels its remainder immediately, without waiting for the TTL. The filled portion is then handled by configuration — either closed at market, or bracketed.
Closing at market sounds wasteful and often is not. If your setup depended on getting size at a level and you got a third of it, the trade you are now in is not the trade the strategy chose. Paying a taker fee to exit a position you did not want is cheaper than carrying it with a stop that was designed for something else.
When the filled portion is kept, everything downstream is recomputed from the actual filled quantity and the actual average price — never from the intended ones. Take-profit legs are shares of what exists, not of what was planned.
def on_partial(order, position):
# 1. Kill the remainder first. Nothing else is safe while it can still fill.
exchange.cancel(order.id)
filled, avg = order.cum_exec_qty, order.avg_price
if filled <= 0:
return
if config.reject_partial_entry:
exchange.close_market(position.symbol, filled) # not the trade we chose
return
# 2. Bracket from what exists, never from what was intended.
stop = level_from(avg, config.sl_k)
legs = [(share * filled, level_from(avg, k)) for k, share in config.tp_legs]
exchange.place_bracket(position.symbol, filled, stop, legs)GTC or FOK
Fill-or-kill removes the problem by refusing anything less than the full quantity. That is a real option and it has a real cost: you pay taker fees, and you miss trades where the full size was not available at your price but most of it was.
| GTC limit | FOK | |
|---|---|---|
| Partial fills | Possible — must be handled | Impossible by construction |
| Fee side | Maker on the resting portion | Taker on the whole order |
| Missed entries | Fewer | More — all-or-nothing |
| Code complexity | Higher | Lower |
| Suits | Patient entries, wider spreads | Tight windows, small size |
There is no universally right answer. There is a right answer per strategy, and it should be a configuration flag rather than an assumption baked into the execution layer.
Reconciliation catches what the stream missed
Fill notifications arrive over the private execution stream. If that stream dropped — see WebSocket and reconnect — you can hold a partially filled position and not know it.
This is why reconciliation after every reconnect is not optional. Compare the exchange's position and open orders against your own, and treat any difference as authoritative in the exchange's favour. The most expensive partial fill is the one your bot never heard about.
A short checklist
- Cancel the remainder immediately on partial fill — do not wait out the TTL.
- Recompute average price and quantity from execution reports, not from intent.
- Size every exit from the filled quantity.
- Decide explicitly whether a partial entry is kept or rejected, per strategy.
- Reconcile against the exchange after every disconnect.
- Log the partial-fill rate. A rising rate means your limits are sitting too far out.
This article describes engineering practice. It is not investment advice. Vizanix develops software and does not promise trading returns.