Concept Lesson · Order Execution Safety

Double-Leg Atomicity:
both legs fill
or neither does.

A straddle is a bet on volatility, not direction. If only one leg fills, you've accidentally taken a directional bet with real money.

Strategies: DecayDoctor, Option Cracker
Framework: core/framework/engine.py
Design spec: 2026-04-26
Background

What is a straddle?

Leg 1: SELL CE

Sell a Call option at the ATM strike. You profit if the market stays flat or drops.

Leg 2: SELL PE

Sell a Put option at the same strike. You profit if the market stays flat or rises.

Together, the two legs form a short straddle. Losses from one leg are offset by gains from the other — as long as both are in place.

A straddle with only one leg is just a naked option sell. That's an entirely different risk profile.
The problem

Placing two orders is not atomic

Step 1
SELL CE
Result
Filled
...
Step 2
SELL PE
Result
??? timeout

The exchange doesn't have a "place two orders atomically" API. You send leg 1, wait for fill, send leg 2, wait for fill. Between them, anything can happen: network errors, illiquid strikes, circuit breakers, API rate limits.

If leg 1 fills but leg 2 doesn't, you're holding a naked short — unlimited directional risk with no hedge.

The gap between leg 1 filling and leg 2 filling is where all the danger lives.
Design

Three invariants that must hold

2/2
Both legs must have non-zero filled quantities
=
Filled quantities must be exactly equal (CE qty == PE qty)
SL
Both legs must have valid SL orders placed on the exchange

If any one of these fails, the entire entry is unwound: SL orders cancelled, positions exited, tranche marked failed.

All three, or nothing. No partial straddles in production.
Mechanism

The 15-second imbalance budget

T + 0s
Leg 1 fills — clock starts. You now hold a naked short.
T + 0s to T + 15s
Leg 2 must fill within this window. System uses repeg or rescue mode pricing.
T + 15s
Budget expires. If leg 2 hasn't filled, the engine unwinds leg 1 immediately.

Why 15 seconds? Long enough for a LIMIT order to fill in normal liquidity. Short enough that a naked short doesn't accumulate meaningful losses on a trending day.

The budget is configured via DOUBLE_LEG_TIMEOUT_SECONDS. Set it to None to disable (Option Cracker's legacy mode).

Every second you hold only one leg, you're exposed to directional risk. The budget caps that exposure.
Scenarios

What can happen during a double-leg entry?

1. Both fill, equal qty, both SLs place

The happy path. Straddle is live.

Tranche activated.

2. Leg 1 never fills

No exposure created. Leg 2 is never attempted.

Entry skipped entirely.

3. Leg 1 fills, Leg 2 times out

Budget expires. Naked short exists.

Unwind leg 1.

4. Both fill, quantities don't match

CE = 325 units, PE = 200 units. Not a straddle.

Unwind ALL filled legs.
Four more scenarios on the next slides — all the ways SL placement can fail.
Scenario 2

Leg 1 never fills

Leg 1
SELL CE
Result
No fill
Leg 2
Never sent

This is the safest failure. Leg 2 is never even attempted because there's nothing to balance against.

engine.py — logic
# Leg 1 didn't fill → first_fill_time stays None
# The imbalance clock never starts
# Leg 2 loop is skipped entirely

filled_legs = []   # empty
# → "both failed" branch
# → emit entry_group_failed
# → no unwind needed (nothing to unwind)
No fill, no risk, no cleanup. Clean exit.
Scenario 3

Leg 1 fills, Leg 2 times out

T + 0s
CE fills at 200. Clock starts. You're naked short a call.
T + 5s
PE order placed at LIMIT. No fill yet — strike is illiquid.
T + 10s
Rescue mode kicks in — tries 5%, then 8% below LTP. Still no fill.
T + 15s
Budget expired. PE marked as failed. Engine enters unwind.
T + 16s
Cancel CE's SL order → place emergency BUY to exit the CE position.

The unwind BUY may cost more than the original SELL (market moved against you during those 15 seconds). That's the cost of atomicity — a small known loss instead of an unbounded naked position.

15 seconds of nakedness, then cleanup. The alternative was unlimited exposure.
Scenario 4

Both fill, but quantities don't match

Leg 1 (CE)

325 units

Full lot. Filled as expected.

Leg 2 (PE)

200 units

Partial fill. 125 units left unfilled when budget expired.

325 CE short + 200 PE short = not a straddle. You're net-short 125 units of CE with no PE offset. That's a directional bet on the market going down.

The engine requires exact equality. Different quantities → unwind both legs. Cancel both SLs, exit both positions.

Close, but not equal, is not good enough. A lopsided straddle is a directional bet in disguise.
Scenario 5

SL placement fails on Leg 1

Step 1
CE fills at 200. Position exists on the exchange.
Step 2
SL BUY order placement fails — trigger price out of range, API error, or validation rejection.
Step 3
Engine attempts emergency exit: BUY back the CE to close the position.
Step 4a
Emergency exit succeeds → CE position closed. Leg marked filled=False.
Step 5
Atomicity check sees 0 filled legs → entry fails cleanly. Leg 2 is never attempted (or unwound if already filled).

mark_tranche_entered is never called → no combined-premium SL, no WebSocket subscription, no active monitoring. The tranche simply doesn't exist.

SL failure = position without protection = unacceptable. Emergency exit is the only safe response.
Scenario 5b · worst case

SL fails AND emergency exit fails

CE
Filled
SL
Failed
Emergency exit
Also failed
Result
CRITICAL alert

The absolute worst case. You have a filled position with no SL and no exit. The system:

1

Track it

Position added to active_positions with sl_order_id=None and needs_emergency_exit=True

2

Alert

CRITICAL Telegram alert fires immediately. Operator must intervene manually.

3

Backstop

SL health monitor and end-of-day squareoff will find it on the next pass and attempt closure.

This is unrecoverable by automation alone. The CRITICAL alert means: human, look at this now.
Scenario 6

Both legs fill, SL fails on Leg 2

Step 1
CE fills. SL placed successfully for CE.
Step 2
PE fills within budget.
Step 3
SL placement for PE fails. PE marked filled=False due to SL failure.
Step 4
Atomicity check: filled_legs has only 1 of 2 legs (CE).
Step 5
Unwind both legs. Cancel CE's SL, exit CE position. Exit PE position.

Even though both legs technically filled, the PE is unprotected. The engine treats had_sl_failure=True with only 1 valid leg as a partial entry — and unwinds everything.

A straddle with one protected leg and one naked leg is worse than no straddle at all.
Mechanism

Rescue mode: aggressive pricing for Leg 2

When leg 1 fills, you're on the clock. Normal LIMIT pricing might not fill fast enough. Rescue mode uses progressively aggressive prices:

Attempt 1
LTP − 5%
Attempt 2
LTP − 8%
Attempt 3
LTP − 10%

Each attempt fetches a fresh LTP, computes the limit price, places the order, and waits for fill or timeout. If the budget expires mid-attempt, it stops immediately.

Why sell below market?

You're selling options. Selling below LTP gives away edge (worse price for you) but dramatically increases fill probability. You're trading price quality for speed.

Target quantity

Leg 2 targets leg 1's actual filled quantity, not the original request. If leg 1 partial-filled to 200 of 325, leg 2 tries 200 to maximize matching odds.

Pay a little premium to fill fast, or pay a lot of directional risk to fill never.
Complete logic

The atomicity decision tree

filled_legs == 2 AND quantities equal AND both SLs valid?
YES → Activate tranche. Emit entry_group_activated.
↓ NO
filled_legs == 2 AND quantities differ?
UNWIND BOTH → Cancel SLs, exit both positions. Reason: "quantity_mismatch"
↓ NO
filled_legs == 1? (timeout, SL failure, or leg 2 failed)
UNWIND the filled leg → Cancel its SL, exit its position. Reason: "timeout" / "sl_failed" / "leg_failed"
↓ NO
filled_legs == 0?
No action needed → Emit entry_group_failed. Clean exit.
Every path leads to a balanced state: either a full straddle, or no straddle at all.
Important interaction

Combined-Premium SL only activates on success

Some strategies use a combined-premium (CP) SL that monitors the total premium of both legs via WebSocket. But it only sets up after a successful entry:

engine.py — success path
# Only reached if ALL THREE invariants hold:
# (1) both filled, (2) equal qty, (3) both SLs placed

self.controller.mark_tranche_entered(tranche_idx, summary)
# ↑ This call sets up CP SL, subscribes WebSocket, starts monitoring

# If any invariant failed, we're in the unwind branch above.
# mark_tranche_entered is never called.
# → No CP SL, no WebSocket, no monitoring.
# → Because there's no straddle to monitor.

The per-leg SLs (200% exchange-level) are the crash-safety net. The CP SL (30% combined-premium) is the active protection. If the WebSocket dies, the per-leg SLs catch it, and a CRITICAL alert fires.

Atomicity check gates everything downstream. No straddle = no combined monitoring.
Audit trail

Every scenario emits events

Success trail

  • entry_group_started
  •   entry_order_placed [CE]
  •   entry_fill_confirmed [CE]
  •   sl_order_placed [CE]
  •   entry_order_placed [PE]
  •   entry_fill_confirmed [PE]
  •   sl_order_placed [PE]
  • entry_group_activated

Failure trails

  • entry_group_started
  •   entry_order_placed [CE]
  •   entry_fill_confirmed [CE]
  •   recovery_adjustment (if SL failed)
  • entry_group_unwound
  •   reason: timeout
  •   reason: quantity_mismatch
  •   reason: sl_failed
  •   reason: leg_failed
Every decision is recorded. You can replay any entry and see exactly what happened and why.
Trade-off

Atomicity has a cost

What you lose

  • Unwind costs money — the exit price is worse than the entry
  • Rescue mode gives away edge (selling 5-10% below LTP)
  • Partial fills that almost matched get fully unwound
  • Some entries that would have worked get killed by the timer

What you avoid

  • Naked short positions with unlimited directional risk
  • Lopsided straddles that look hedged but aren't
  • Unmonitored positions with no SL protection
  • Silent failures that compound into large losses

The unwind might lose you Rs 500-2000. A naked short on a trending day can lose Rs 50,000+.

Small known losses beat large unknown losses. Every time.
New vocabulary

Words you'll use from now on

Imbalance
budget

The countdown

Max seconds you tolerate holding only one leg. Default 15s. Configured per strategy.

Rescue
mode

Aggressive fill

Progressively worse LIMIT prices (5%, 8%, 10% below LTP) to maximize fill probability when the clock is ticking.

Unwind

Undo a partial entry

Cancel SL orders, exit filled positions. Return to zero exposure. The system's "undo" button for broken straddles.

Naked
short

Unhedged sell

A sold option with no offsetting position. Unlimited loss potential in one direction. The thing atomicity prevents.

The rule

Both legs fill with equal quantities.

Both SLs place successfully.

Or everything is unwound.

No partial straddles. No unprotected legs. No silent failures.

← Back to course index

Arrow keys to navigate · F for fullscreen
1 / 19