Agentic Trading Index

Handling partial fills and rejections in trading code

Trading terminal showing order events for handling partial fills and rejections
Photo by Rafael Minguet Delgado on Pexels

Build around events, not acknowledgements

You see the same pattern in nearly every unattended trading setup: submit, acknowledgement, then an event stream that may arrive late or out of order. An acknowledgement means the broker received the order. It does not mean the requested quantity has traded, or that the order will remain live.

Partial fills and rejections change what the strategy can safely do next. If a 1,000-share buy fills 400 shares, the agent owns 400 shares even if the remaining 600 are still working. If the order is then rejected or cancelled, blindly resubmitting 1,000 can leave the account with 1,400 shares.

Keep three quantities separate: the strategy's target, the quantity filled, and the quantity still working. Derive the remaining target from a reconciled position, not from the last status message. Broker APIs use different event names and sequencing rules, so build an adapter that converts each feed into a consistent internal event model.

A cancel request is not a cancelled order. Keep the remaining quantity exposed until the broker confirms the cancel.

That distinction matters because trades can occur while a cancel is in flight. A solid agent records the request, waits for a terminal event or reconciles with the broker, and only then decides whether another child order is needed.

Walk through a 1,000-share order

Suppose an agent wants 1,000 shares of a liquid US-listed ETF, with a $50.00 limit price. It submits a DAY limit order. The broker acknowledges order ID A17. The next events report 400 shares filled at $49.99, then 250 at $50.00. The cumulative filled quantity is now 650, and 350 remain open.

The strategy sees the price move away and requests a cancel. Before the broker confirms it, another 25 shares fill at $50.00. The cancel confirmation reports 675 cumulative shares filled and 325 cancelled. The position change is 675 shares, not 650, and the agent must not treat the cancel request itself as proof that the order stopped trading.

Before replacing the remaining 325 shares, the agent checks its account state. A separate strategy has used some buying power, leaving $16,100 available. The replacement would require about $16,250 at the limit price, before any broker reserve for fees or other open orders. The broker rejects it for insufficient buying power. That rejection applies to the replacement order. It does not undo the 675-share fill or change the 325-share target shortfall.

Typical market ranges vary with instrument, venue, time of day and API. The figures below are planning ranges for liquid US equities in regular trading, not service guarantees or audited results.

Event or measureTypical rangeCode implication
Broker order acknowledgement50-500 msTrack the request as pending until an order ID or explicit failure arrives.
Cancel confirmation100 ms-2 sAllow for fills during the gap; do not release the full remaining quantity early.
Partial fill event deliveryMilliseconds to several secondsDeduplicate events and compare cumulative quantity with executions.
Displayed spread in a liquid ETF1-5 basis pointsUse a price limit and a staleness check rather than assuming a market order is costless.

For this example, retain the original intent of 1,000 shares and the actual 675-share position separately. If buying power remains insufficient, wait, reduce the target under a defined sizing rule, or stop and alert. Do not change the target merely to make the rejection disappear.

Represent order and intent separately

An order state machine tracks each broker order. A strategy intent tracks what the agent is trying to achieve.

One intent can create several orders, and each order can end as filled, cancelled, rejected or expired. Keep immutable order and execution identifiers so a reconnect does not turn a replayed message into a second fill.

Store both the last-fill quantity and the cumulative filled quantity when the broker supplies them. Apply an execution once, keyed by its execution ID. If the API reports cumulative quantity, reject regressions and reconcile discrepancies against the execution feed or order query. Never add cumulative values together as if each were a new fill.

  1. On submission, store the intent ID, client order ID, symbol, side, limit, requested quantity and risk decision.
  2. On acknowledgement, attach the broker order ID and mark the order working. Keep the requested quantity distinct from the filled quantity.
  3. On each fill, record the execution ID, quantity, price, timestamp and fees where available. Update the position and remaining quantity atomically.
  4. On cancel request, mark cancellation pending. Continue to accept fills until a terminal cancel, fill or rejection event arrives.
  5. On rejection, record the broker code and message, mark that order terminal, then recalculate the intent from position and other live orders before taking action.

Order-level states might include pending submission, working, partially filled, pending cancel, filled, cancelled and rejected. Avoid a single status field on the strategy intent that tries to represent all its child orders. For observability, log:

  • Intent ID, client order ID and broker order ID.
  • Event time, receive time, event sequence if supplied, and a unique execution ID.
  • Requested, cumulative filled, leaves quantity and average fill price.
  • Rejection code, cancel reason, available buying power and the risk check that allowed or blocked the next action.

Compare broker-specific event behaviour in an agentic paper trading environment before relying on it in production. Simulation can exercise state transitions, but it cannot prove that a live venue will deliver events with the same timing.

Reconcile before replacing or retrying

After a rejection, do not automatically submit the same order again. First decide whether the rejection is transient, correctable or terminal. Rate limits and temporary service errors may allow a bounded retry.

Invalid symbols, closed markets, price increments and insufficient buying power need a changed request or a changed portfolio state. Retrying unchanged input can create noise and consume request limits.

On a disconnect, local state may have missed a fill or terminal event. Query open orders, executions and positions, then reconcile by broker ID and execution ID.

Where the broker's position snapshot and execution history disagree, pause new orders for that instrument and investigate the discrepancy. Do not infer that an order vanished just because its ID is absent from one incomplete response.

A safe replacement sequence is:

  • Read current position, open orders and recent executions from the broker.
  • Calculate leaves quantity against the strategy target, accounting for every live order on that instrument and side.
  • Check buying power, exposure limits, price collars and order-rate limits on the proposed replacement.
  • Submit with a fresh client order ID, but retain the parent intent ID for audit and recovery.
  • If the response times out, query by client order ID before resubmitting. The request may have reached the broker even when the response did not.

Bound retries by both count and time. A typical implementation might allow one or two retries for a recognised transient error, with backoff, then halt that intent and alert. Set the actual limits from broker rules and strategy risk, not from a generic retry library. For examples of agent execution behaviour, test the order lifecycle in paper trading, including delayed messages and reconnects.

Test the failure paths and cap exposure

Unit tests should inject event sequences, not just final states. Replay the 400, 250, cancel, 25 and cancel-confirmation sequence above. Assert that the final filled quantity is 675, leaves quantity is zero for order A17, and a replacement for the remaining intent is calculated as 325, subject to a fresh risk check.

Then test a replacement rejection. The expected result is a terminal rejected child order, an unchanged 675-share filled position and a visible 325-share intent shortfall. A risk policy can leave that shortfall pending, reduce the target or mark the intent failed. The choice should be explicit and recorded, rather than buried in a generic exception handler.

Include at least these cases in integration tests:

  • A duplicate fill message, which must not increase position twice.
  • A fill arriving after a cancel request but before cancel confirmation.
  • A rejection followed by a delayed acknowledgement or execution event.
  • A network timeout after submission, where a broker-side order may already exist.
  • A restart with open orders, missing local events and a different broker position snapshot.

Use hard limits on total open quantity and position exposure across all child orders. If local and broker state disagree beyond a configured threshold, stop submissions for that symbol and raise an operational alert. A sensible paper-trading test also checks that fees, buying-power holds and partial executions affect the next sizing decision, rather than treating fills as an all-or-nothing result.

This approach does not apply unchanged to derivatives or other products whose orders can create contingent exposure, such as options spreads, where leg execution and margin rules require product-specific state and risk logic.

Frequently Asked Questions

What should a trading bot do after a partial fill?

Record the execution once, update the position and calculate the remaining target from reconciled account state. Do not resubmit the original quantity while the first order or its cancellation may still be live.

Can an order fill after a cancel request?

Yes. A fill can occur before the broker processes the cancellation. Keep the order exposed until a terminal cancel or fill event is confirmed, then reconcile executions and leaves quantity.

Should a bot retry a rejected order automatically?

Only for recognised transient errors, with a bounded retry count and a fresh risk check. Correctable errors need a changed request, while terminal errors should stop the intent and produce a clear record.

How should a bot handle a timeout after order submission?

Treat the order state as unknown rather than assuming submission failed. Query the broker using the client order ID or another supported lookup before resubmitting, to avoid creating a duplicate order.