Skip to main content

Order Rejections and Partial Fills

FB THEFINBABA

Strategy logic is the interesting part of an automated system and it is almost never where the failures are. The failures are in the gap between placing an order and knowing what happened to it.

Almost every expensive live bug reduces to one assumption: that an order which was sent is an order that was filled, in full, at the price expected.

An order has a lifecycle, not an outcome

The mental model most beginners carry is that placing an order is like calling a function that returns a result. It is not. Placing an order starts a process whose outcome arrives later and asynchronously.

An order that has been accepted by your broker can subsequently be rejected by the exchange. An order that is open can be partially filled and remain open for the balance. An order can be filled in several pieces at several prices. An order can be cancelled by the broker rather than by you. And an order can sit open indefinitely because the price never came back.

So the response you get when you place an order tells you one thing only: the request was accepted for processing. It does not tell you that anything was bought or sold. Treating that response as confirmation is the single most common structural error in retail automated trading, and it produces the whole family of bugs below.

The correct model is a state machine. An order occupies a state — pending, open, partially filled, complete, rejected, cancelled — and your system's job is to track transitions between them rather than to assume an end state. Every path out of every state should be something your code has an answer for, including the paths you consider unlikely.

The failures that cost real money

Four specific ones, each of which has emptied accounts belonging to people who understood their strategy perfectly.

The unprotected entry. The system places an entry order and then places a stop-loss. The entry fills; the stop is rejected, perhaps for a margin reason or a price band. Nobody checks. There is now a live position with no protection, and the system believes it is protected. This is the worst common failure because the account can be exposed for hours with the code reporting normal operation.

The double entry. An order times out or the response is not received. The system retries. Both orders were in fact accepted, and the position is twice the intended size. Every order should carry a client-side unique identifier so a retry can be recognised as a retry rather than treated as a new instruction.

The mismatched exit. An entry for a large quantity fills partially. The system exits the full intended quantity anyway, which either fails or, worse, succeeds and opens a position in the opposite direction. Exit quantity must be derived from what was actually filled, never from what was intended.

The silent drift. Small discrepancies accumulate across a session — a partial here, a rejection there — and the internal position gradually stops matching the broker's. Every subsequent decision is then taken on a wrong number, and the error compounds quietly until something large goes wrong.

The broker is the source of truth, always

The single rule that prevents most of the above: your internal record of a position is a cache, and the broker's record is the truth. When they disagree, the broker is right and your code is wrong.

Three habits follow from that.

Reconcile on a schedule, not on suspicion. Fetch positions and holdings from the broker periodically through the session and compare against your internal state. If they differ, stop trading and alert rather than attempting an automatic correction, because a system that is already confused is not the thing you want deciding how to fix itself.

Reconcile at start-up before doing anything else. A system restarting after a crash has no idea what happened while it was down. It must ask the broker what positions and open orders exist and build its state from that answer, never from a file it wrote before it died.

Handle the update stream and the polled state as different things. Order updates arriving through a stream can be missed, delivered out of order, or duplicated. Treat them as hints that something changed, then confirm the actual state by querying. A system that trusts the stream alone is correct until the one session where it is not.

Also worth logging deliberately: every request sent, every response received, and every state transition, with timestamps. When something does go wrong, the difference between a ten-minute diagnosis and an unsolvable mystery is entirely whether that log exists.

Rejections you should expect, and what to do

Rejections are not exceptional events. A live system meets them regularly, and each type deserves a defined response rather than a generic retry.

Insufficient margin. Do not retry. The reason will still be true a second later, and a retry loop against a margin rejection can generate hundreds of requests in a minute. Alert, and stop attempting new positions.

Price outside the permitted band. A limit price beyond the circuit limit, or a stop placed at an invalid level relative to the current price. Recompute against current market data rather than resubmitting the same value.

Rate limiting. Brokers cap how many requests per second and per period you may send. Back off progressively rather than retrying immediately, and design the system not to approach the limit in normal operation so that headroom exists when something unusual happens.

Instrument or session issues. A contract that has expired, a symbol whose token changed after a corporate action, an order placed outside market hours, or a share moved to a restricted settlement category. These are configuration and data problems rather than transient ones, and retrying will never fix them.

Authentication failures. Access tokens expire daily on Indian broker APIs. A rejection at nine in the morning is far more likely to be a stale token than anything to do with the order.

The general rule: retry only what is genuinely transient, with a limit and a backoff, and alert on everything else. An automated system that retries indefinitely is not resilient; it is a way of turning one problem into many.

Order states come from the API

An account with a full order book

Every state transition, queryable, for reconciliation. Free to open

Broker API behaviour, order states, rate limits and rejection reasons differ between brokers and are revised from time to time — read your broker's current documentation rather than relying on this description. This is educational material, not advice. We run no tips group and no signal service, we manage nobody's money, and no return is promised. Automated trading carries a real risk of loss.

Disclosure: the account-opening link on this page is under Atul Shrivastava's Zerodha Authorised Person registration (NSE AP Reg: AP2516003481; Zerodha Broking Ltd. SEBI Reg: INZ000031633) and earns a revenue share. TheFinBaba is not a SEBI-registered Investment Adviser — this content is educational, not investment advice.

Frequently Asked Questions

Does a successful order placement mean the trade happened?

No. It means the request was accepted for processing. The order can still be rejected by the exchange, filled partially, filled in several pieces at different prices, or remain open indefinitely. Treating the placement response as confirmation is the most common structural error in retail automation.

How do I handle a partial fill?

Derive every subsequent action from the quantity actually filled rather than the quantity intended. Exiting the intended amount after a partial entry either fails or opens a position in the opposite direction, which is considerably worse than failing.

Should my bot retry a rejected order?

Only for genuinely transient reasons, with a retry limit and a backoff. Margin rejections, invalid prices, expired instruments and authentication failures will not resolve themselves, and retrying them generates a flood of requests that can hit rate limits and create new problems.

How often should I reconcile positions with the broker?

On a schedule through the session, and always at start-up before placing anything. Treat the broker's record as the truth and your internal state as a cache. On a mismatch, stop and alert rather than attempting an automatic correction.

What is the most dangerous failure in a live system?

An entry that fills while its stop-loss order is rejected. The position is live and unprotected while the system reports normal operation. Always verify that protective orders were accepted, and treat a missing stop as a reason to close the position rather than to continue.

Disclaimer: TheFinBaba provides educational content only - this is not investment advice. Trading involves risk of loss.

Atul Shrivastava
Written by

Atul Shrivastava

Founder & Lead Trainer, TheFinBaba

16+ years in the markets. 8+ years teaching Python algo trading.

Full profile

Found this useful? Share it:

WhatsApp Share

Disclaimer: TheFinBaba provides educational content only. Nothing in this article is investment advice or a recommendation to buy or sell any security. Trading in financial markets carries risk of loss — make every decision based on your own research and risk capacity.

Free Account Opening

Open a Free Demat & Trading Account in 5 Minutes

Start trading stocks, F&O, IPOs, bonds & ETFs with Zerodha — India's most trusted discount broker. Zero account-opening fee, paperless Aadhaar KYC, and the best APIs (Kite Connect) for Python algo trading.

  • Zero account-opening fee
  • ₹0 brokerage on equity delivery
  • Kite Connect API — best for Python algo traders
  • Paperless Aadhaar e-KYC in 5 minutes
Open Free Demat Account

* Zerodha is SEBI-registered. Account opening subject to KYC approval. Atul Shrivastava is an Authorised Person (AP) — Reg AP2516003481.

Trusted by 1.6 Cr+ Indian Investors

Zerodha is India's largest stock broker by active clients (NSE data, 2026).

₹0
Delivery
₹20
Intraday
5 min
Opening