Sending an order is four lines. Everything difficult happens afterwards — the rejection you did not anticipate, the partial fill your position tracking ignored, the retry that doubled your size, and the restart that lost track of what you were holding.
This covers the whole path: placing, the parameters that actually matter, reading what happened, and the failure handling that separates a script from something you would leave running.
The minimum, and the parameters worth understanding
from kiteconnect import KiteConnect
kite = KiteConnect(api_key='your_api_key')
kite.set_access_token('your_access_token')
order_id = kite.place_order(
variety=kite.VARIETY_REGULAR,
exchange=kite.EXCHANGE_NSE,
tradingsymbol='RELIANCE',
transaction_type=kite.TRANSACTION_TYPE_BUY,
quantity=1,
product=kite.PRODUCT_CNC,
order_type=kite.ORDER_TYPE_LIMIT,
price=2450.50,
validity=kite.VALIDITY_DAY,
tag='sma20x50'
)
print(order_id)Three of those deserve attention.
product decides what the position is. CNC is delivery equity. MIS is intraday and will be squared off automatically by the broker near the close, whether or not your program intended that. NRML is for carrying derivatives positions. Choosing MIS when you meant NRML is a common and expensive slip, because the exit happens without you.
order_type is where a strategy's assumptions live. MARKET fills but not at a price you chose. LIMIT gets your price or nothing. SL and SL-M are the two stop variants, and the difference between them matters most in a fast market: SL becomes a limit order on trigger and can go unfilled precisely when you needed the exit.
tag costs nothing and is worth using on every order. It lets you reconcile the broker's records against your own log later, which you will want the first time something does not match.
The order id is not a confirmation
This trips almost everybody once. place_order returning an id means the order was accepted for processing. It does not mean it was accepted by the exchange, and it certainly does not mean it filled.
An order moves through statuses, and only one of them is final and good.
orders = kite.orders() # aaj ke saare orders
mine = [o for o in orders if o['order_id'] == order_id][0]
print(mine['status'], mine['filled_quantity'],
mine['average_price'], mine['status_message'])COMPLETE means filled. OPEN means resting in the book. REJECTED means it never got there, and status_message tells you why. CANCELLED means it was pulled, possibly by you and possibly by the broker at square-off time.
A program that places an order and moves on, assuming it worked, will eventually think it holds a position it does not. Check the status, or subscribe to postbacks so the broker tells you when it changes.
Rejections, and what they usually mean
Read status_message rather than guessing. The recurring causes:
- Insufficient funds or margin. The most common by far. Your risk calculation and the broker's margin calculation are different things, and the broker's is the one that decides.
- Price outside the circuit limit. A limit order placed too far from the current price is rejected outright.
- Tick size. A price of 2450.53 on an instrument with a five-paisa tick is invalid. Round to the tick before sending.
- Market closed, or the instrument not tradeable in that session.
- Freeze quantity exceeded in derivatives — a single order above the exchange's cap is rejected, and the fix is splitting it into several.
- Wrong product for the segment, such as CNC on a derivative.
Log every rejection with its message. A rejection at 9:16 that nobody recorded becomes a mystery at 4 pm.
Modifying and cancelling
A resting order can be changed rather than cancelled and replaced, which is usually what you want — it keeps the same order id and avoids a gap where you have no order in the market at all.
kite.modify_order(
variety=kite.VARIETY_REGULAR,
order_id=order_id,
quantity=2,
price=2455.00,
order_type=kite.ORDER_TYPE_LIMIT
)
kite.cancel_order(variety=kite.VARIETY_REGULAR,
order_id=order_id)Both fail if the order is no longer open — if it filled a moment before your modification arrived, you get an error rather than a silent no-op. Handle that case explicitly, because the correct response is usually to re-read your position rather than to retry.
The variety passed to modify and cancel must match what was used to place. Getting that wrong produces a confusing error that has nothing to do with the actual problem.
Partial fills, which break naive position tracking
You asked for 100 and 40 filled. Your program believes it holds 100. Every subsequent decision — the stop quantity, the exit, the risk calculation — is now wrong.
The rule that prevents this: never track position from what you requested. Track it from what the broker says you hold.
def actual_position(kite, symbol):
for p in kite.positions()['net']:
if p['tradingsymbol'] == symbol:
return p['quantity'], p['average_price']
return 0, 0.0Call that after every order and before every decision that depends on size. It is one extra request and it removes an entire category of bug.
The same discipline applies to your protective order. If 40 filled, the stop should be for 40. A stop sized for 100 against a 40 position leaves you short 60 the moment it triggers.
Retries, and why a naive one is dangerous
A network timeout on an order request is the worst failure mode in the whole system, because you do not know whether it reached the broker.
Retry blindly and you may place the order twice. Do not retry and you may have no position when your strategy thinks it has one. Neither guess is acceptable.
The safe pattern is to check before retrying:
def place_once(kite, tag, **params):
try:
return kite.place_order(tag=tag, **params)
except Exception as e:
log.warning('place_order failed: %s', e)
# Ho sakta hai order chala gaya ho - pehle dekho, phir dobara bhejo
for o in kite.orders():
if o.get('tag') == tag:
log.info('already placed: %s', o['order_id'])
return o['order_id']
return kite.place_order(tag=tag, **params)This is what the tag field is genuinely for. Give each intended order a unique tag, and an ambiguous failure becomes a question you can answer rather than a coin flip.
Restarts, and the state you must not assume
Your process dies at 11:40 holding a position and the supervisor restarts it at 11:41. What it must not do is start from a clean slate.
On startup, before evaluating anything: read your positions from the broker, read today's orders, and reconstruct what you are holding and what is resting in the book. Only then decide anything.
def rebuild_state(kite):
pos = {p['tradingsymbol']: p['quantity']
for p in kite.positions()['net'] if p['quantity']}
resting = [o for o in kite.orders() if o['status'] == 'OPEN']
log.info('resumed with %s positions, %s open orders',
len(pos), len(resting))
return pos, restingTwo things to check while you are there. Is there a protective order for every position? If the crash happened between entry and stop placement, there is not. And is any resting order now stale — a limit from a signal that is no longer valid?
All of this needs a live account with Kite Connect enabled, which supplies the order endpoints and the position data you are reconciling against.
Order placement needs a live account
Kite Connect is billed separately from the account. Account free to open
Order handling, reconciliation and safe restarts are covered as sections rather than as asides in our Algorithmic Trading with Python course at Rs 24,900 — one payment, permanent access, free demo on WhatsApp first.
We sell no tips and no signal group, we manage nobody's money, and we promise no returns. 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 place_order mean my order was executed?
No. It returns an order id meaning the request was accepted for processing. The order can still be rejected by the exchange or rest unfilled. Check the status through kite.orders() or subscribe to postbacks.
What is the difference between CNC, MIS and NRML?
CNC is delivery equity, MIS is intraday and will be squared off by the broker near the close whether or not your program intended it, and NRML carries derivatives positions. Choosing MIS when you meant NRML means the exit happens without you.
Why is my Kite Connect order rejected?
Read status_message rather than guessing. The usual causes are insufficient margin, a price outside the circuit limit, a price that is not a multiple of the tick size, freeze quantity exceeded in derivatives, or the wrong product for the segment.
How do I handle partial fills?
Never track position from what you requested. Read it back from kite.positions() after every order and before any decision that depends on size, and size your protective order to the quantity actually filled.
Is it safe to retry a failed order request?
Not blindly - a timeout does not tell you whether the order reached the broker. Give every intended order a unique tag, and on failure check the order book for that tag before placing again.
What should my program do after a crash and restart?
Read positions and today's orders from the broker and rebuild state before evaluating anything. Then check that every position has a protective order, since the crash may have happened between entry and stop placement.
Related Reading
- Kite Connect API in Python - step-by-step tutorial
- Live tick data with the Kite Connect WebSocket
- Zerodha historical data API in Python
- Order types: market, limit, SL and SL-M
- Is algo trading legal in India?
- Algorithmic Trading with Python - full syllabus
Disclaimer: TheFinBaba provides educational content only - this is not investment advice. Trading involves risk of loss.