You wanted to sell 100. The broker sold 80 and called it "complete." Your code believed it. The other 20 quietly filled four minutes later — with no stop-loss, in nobody's books, until you spotted it in the broker app.
You ask for 100. You get 100. You hedge 100. The numbers match across all three places they live: your code, the broker's order book, the exchange.
Your order says SELL 100 @ 158.10. At that exact moment, the bid stack might look like this:
# Buyers waiting to buy at each price: 158.10 buyer wants 50 ← takes 50 of your sell 158.05 buyer wants 30 ← takes 30 more (you accept 5 paise slip) 158.00 buyer wants 0 ← nothing here 157.95 buyer wants 200 ← you said NOT below 158.10. Stop. Filled: 80 / 100 @ avg 158.08 Unfilled: 20 — order keeps resting at 158.10, waiting for more buyers
The order didn't fail. It didn't fully succeed either. It's in a third state — partially done, partially still alive.
The strategy read status = "completed" and filled_quantity = 80. It concluded: "80 done, move on."
But the original LIMIT was still alive at Tradejini, with 20 qty waiting at 158.10 for more buyers to show up. The strategy never sent a cancel.
# After placing the order and waiting: status = broker.get_order_status(order_id) if status.status == "completed" OR status.filled_quantity > 0: # Treat as success. Place SL. Move on. return {"quantity": status.filled_quantity OR quantity} # ↓ Partial-fill handler below was effectively DEAD CODE, # because the OR above grabbed everything with even 1 share filled. elif status.filled_quantity > 0 and status.filled_quantity < quantity: cancel_remaining(order_id) # never reached alert("PARTIAL FILL: ...") # never sent
One OR. Two distinct outcomes (full fill vs partial fill) routed to the same code path. The safer branch underneath, with cancel-the-remainder and send-an-alert, never ran.
| 11:16:45 | S | 76100 PE | 100 / 100 | 114.50 | COMPLETED | PE entry — full fill |
| 11:16:48 | B | 76100 CE | 80 / 80 | 154.35 | COMPLETED | unwind of the 80 CE the strategy knew about |
| 11:16:48 | B | 76100 CE | 0 / 80 | 332.05 | CANCELLED | SL on the 80 CE — cancelled during unwind ✓ |
| 11:16:54 | B | 76100 PE | 0 / 100 | 240.45 | CANCELLED | SL on the 100 PE — cancelled during unwind ✓ |
| 11:16:54 | B | 76100 PE | 100 / 100 | 114.45 | COMPLETED | unwind of PE — strategy now thinks it is flat |
| 11:17:05 | S | 76100 CE | 100 / 100 | 158.10 | COMPLETED | the original SELL CE filled the remaining 20 — silently |
| 11:21:31 | B | 76100 CE | 20 / 20 | 182.45 | COMPLETED | operator manual squareoff — loss Rs 487 |
The strategy's log only knew about the four muted rows. The red row and the green row — the actual damage — existed only at the broker.
The 11:17:05 fill was a broker-side event on an order the strategy already moved past. No webhook, no poll, no log line.
The position-verifier only walks instruments already in active_positions. A position the strategy never recorded is invisible to the check.
BUG-175's cross-check fires on fills we are processing. It can't detect fills we don't know are happening.
The 15:14 squareoff loops over active_positions. The naked 20 CE was not in it. Squareoff would have skipped it entirely.
>= instead of OR. The consequences are everything.# BEFORE — permissive: any fill counts as success if status.status == "completed" or status.filled_quantity > 0: filled_qty = status.filled_quantity or quantity # can fabricate phantom 100 return success(filled_qty) # AFTER — strict: only a full fill is success if status.filled_quantity >= quantity: filled_qty = status.filled_quantity # real number only return success(filled_qty) elif 0 < status.filled_quantity < quantity: cancel_and_verify(order_id) # kill the remainder alert("PARTIAL FILL: ...", severity="WARN") # human visibility return partial(filled_qty) # engine unwinds both legs
Applied at three places: place_progressive_entry (RSI Sniper, IVDrift), place_repeg_entry (DD, DD-CP, First Strike), and place_rescue_entry (DD/DD-CP leg-2). Same bug, same fix, all three at once.
filled < requested,Cancel the remainder. Alert the human. Then decide what to do.
← Back to course index