Your API says "order filled." But the exchange never saw it. You place a stop-loss for a position that doesn't exist. Now you have a naked long losing real money.
You send the order. Broker forwards it to the exchange. Exchange matches it with a buyer. Broker tells you "complete." You place a stop-loss to protect the position.
Upstox has an internal Order Management System (OMS). When you place an order, it doesn't go straight to the exchange. It sits in the OMS first — validated, queued, then forwarded.
On BSE (SENSEX options), this queue can hold orders for 5+ minutes before forwarding them. The Upstox UI shows these as "Scheduled".
# Place a LIMIT SELL order result = broker.place_order(sell_order) # "order accepted" # Wait 5 seconds for it to fill time.sleep(5) # Ask the broker: did it fill? status = broker.get_order_status(order_id) if status == "complete": # Upstox says YES place_stop_loss(...) # We trust it blindly
The Upstox API can return status = "complete" while the order is still in the OMS queue. Most of the time the fill is real. But if the exchange later rejects the order, you've placed a stop-loss for a position that doesn't exist.
Today the fill was real — we got lucky. But the code had zero defense against a false positive. If the exchange had rejected the SELL, the SL BUY would have created a naked long position losing money with no hedge.
It doesn't lie on purpose. It's a timing mismatch between layers:
Asks the broker API: "Is my order filled?"
Knows the order was accepted and priced. Reports "complete" optimistically.
Might still be validating, or hasn't even seen the order yet. The "complete" refers to the OMS step, not the exchange step.
Now you have no short position (the SELL was rejected) but a live SL BUY order sitting on the exchange.
When the price hits the trigger, the SL BUY fires. You buy something you never sold. A naked long position, losing real money, with no hedge.
# After broker says "filled", ask a DIFFERENT question: # "Do I actually HOLD this position?" positions = broker.get_positions() for p in positions: if p.instrument_id == my_instrument: if p.sell_quantity >= expected_qty: return fill_result # Position confirmed — safe to place SL # Position not found — PHANTOM FILL! log.critical("PHANTOM FILL: order says filled but no position found") send_alert("PHANTOM FILL BLOCKED — verify manually!", severity="CRITICAL") return None # Do NOT place SL. Report failure to engine.
The engine receives None (no fill), so it never places an SL. The operator gets a CRITICAL Telegram alert and can verify manually.
When get_positions() itself throws an error (API is down, timeout, network issue).
Why: If we blocked every fill during API instability, we'd miss real trades on volatile days — exactly when trading matters most. The order API already said "filled" — it's probably right.
When get_positions() returns data successfully but the position is missing or quantity is wrong.
Why: The API worked fine — it just told us the position isn't there. That's a clear signal. Placing an SL now would create the exact naked position we're trying to prevent.
The broker's internal queue. Your order sits here before reaching the exchange. Upstox, Zerodha, everyone has one.
The API says "filled" but the exchange hasn't confirmed. The position might not exist. Trusting it blindly creates naked risk.
The broker's list of your real positions. It's updated after the exchange confirms. It's the truth, not the order status.
A BUY without a corresponding SELL (or vice versa). You're exposed to unlimited loss in one direction with no protection.
Check both before placing a stop-loss. Always.
← Back to course index