Concept Lesson · Order Execution Safety

Partial fills:
when the broker
only sells you half.

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.

Real incident: 27 May 2026
Strategy: RSI Sniper
BUG-204
Manual squareoff required
The happy path

How you think a LIMIT order works

Your code
SELL 100 @ 158.10
Broker accepts
Resting in book
Exchange matches
100 buyers found
API says
"Complete: 100"
You
Place SL on 100

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.

When the market has 100 buyers at your price, everything works. The trouble starts when it doesn't.
What actually happens

LIMIT orders fill in pieces, not in one shot

Your order says SELL 100 @ 158.10. At that exact moment, the bid stack might look like this:

SENSEX 76100 CE · bid stack at 11:16:42
# 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.

A LIMIT order is a willingness, not a guarantee. The market gives what it has.
Two pictures of the same order

What the strategy saw — and what was really happening

Strategy belief
80/100
Broker reality
80+20
Same order ID. Same instant. Two different states.

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.

"Completed" at the broker doesn't mean "done forever." It can mean "done for now."
The dangerous moment

The line of code that collapsed two outcomes into one

order_manager.py · pre-fix
# 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.

A single permissive condition turned a load-bearing safety branch into dead code.
27 May 2026 · real incident

What the broker actually recorded that day

SENSEX 27 MAY 76100 · T2 entry @ 11:15 Tradejini order book
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.

Strategy belief vs broker truth

The four-minute invisible short

Strategy log says
SELL CE 100 @ 158.10 placed
Filled 80 — "completed", move on accepted as success
SL placed for 80 CE
PE filled 100 @ 114.50
Mismatch detected (CE=80, PE=100). Unwind both legs.
Bought back 80 CE @ 154.35. CE flat.
Bought back 100 PE @ 114.45. PE flat.
tranche complete. Idle until next entry.
11:30 position summary — no 76100 CE listed.
11:16:37
11:16:42
11:16:42
11:16:45
11:16:48
11:16:48
11:16:54
11:16:59
11:17:05
11:18…
11:30
11:21:31
Tradejini actually had
SELL CE 100 @ 158.10 resting
SELL CE 80/100 filled. 20 still resting. strategy never cancels
SL-BUY CE 80 placed
SELL PE 100/100 done
SL-BUY PE 100 placed
BUY CE 80 (unwind) done. SL on 80 cancelled.
SL on PE cancelled.
BUY PE 100 (unwind) done.
Remaining 20 of original SELL CE fills @ 158.10. Order = 100/100.
Naked short: 20 × 76100 CE. No SL. Not in strategy state.
Still naked. CE drifting up.
Operator buys 20 CE @ 182.45. Flat. -Rs 487 realized.
Why the safety nets missed it

The strategy could not see what it didn't already know

1

No event emitted

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.

2

Reconcile is scoped

The position-verifier only walks instruments already in active_positions. A position the strategy never recorded is invisible to the check.

3

Phantom-fill guard doesn't help

BUG-175's cross-check fires on fills we are processing. It can't detect fills we don't know are happening.

4

Squareoff walks state

The 15:14 squareoff loops over active_positions. The naked 20 CE was not in it. Squareoff would have skipped it entirely.

When state and reality diverge, only the broker order book closes the gap. The strategy can't.
The fix · BUG-204

Treat partial as partial. Cancel the rest. Tell the human.

Before — accept any non-zero fill

  1. Place SELL 100
  2. Get 80 filled, status "completed"
  3. Return "success: qty=80"
  4. Place SL for 80
  5. Remaining 20 keeps resting at broker
  6. Fills silently later. Naked short.

After — strict full-fill gate

  1. Place SELL 100
  2. Get 80 filled
  3. filled (80) < requested (100) — not done
  4. Cancel order. Verify cancel at broker.
  5. Telegram: "PARTIAL FILL: 80/100 @ 158.10"
  6. Return partial. Engine unwinds both legs.
  7. No remainder. No silent fill. No naked short.
The strict full-fill gate is one symbol — >= instead of OR. The consequences are everything.
The code

One symbol changed. Three call sites.

core/framework/order_manager.py
# 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.

The rule

If filled < requested,

you have not completed the order.

Cancel the remainder. Alert the human. Then decide what to do.

← Back to course index

Arrow keys to navigate · F for fullscreen
1 / 11