Order Execution Flow
Go-Live Gate
execute_* strategy method
│
├── GoLiveManager.get_state(strategy) DRY_RUN / SHADOW / LIVE from SQLite records
├── DRY_RUN → scanner prices become simulated fills; no broker orders
├── SHADOW → can_trade() + available_balance ≥ max_margin × 1.10 → real orders + SHADOW_DR_* metrics snapshot
└── LIVE → can_trade() + available_balance ≥ max_margin × 1.10 → real orders only
Box Spread
execute_box_spread(opp)
│
├── can_trade() killed? low_balance? daily_limit? max_positions?
│ └── abort if any check fails
│
└── _place_legs_ioc(leg_specs, lot, tag)
│
├── get_ltp(NSE_FNO, [id1, id2, id3, id4]) ← FRESH prices, replaces stale scan prices
│
├── For each leg:
│ 1. anchor = scanner bid (SELL) / ask (BUY); if LTP moved past it, use LTP as anchor
│ slipped = anchor × (1 + 0.2%) BUY / anchor × (1 − 0.2%) SELL [ORDER_SLIPPAGE_PCT]
│ 2. limit_px = tick_round(slipped) ← nearest valid ₹0.05
│ 3. place_order(..., validity="IOC")
│ 4. daily_order_count += 1
│ 5. _poll_fill(oid, timeout=15 s)
│ ├── TRADED → ✓ continue
│ ├── PART_TRADED_AND_CANCELLED → ✓ treat as filled
│ ├── CANCELLED / REJECTED → retry with 0.8% slip
│ └── timeout (15 s) → retry with 0.8% slip
│ 6. Retry: re-fetch LTP for this leg · place IOC LIMIT at 0.8%
│ → filled ✓ continue → still failed → _auto_reverse
│ 7. _auto_reverse(filled_legs_so_far):
│ ├── Pass 1: IOC LIMIT at LTP ± 0.1% per filled leg
│ └── Pass 2: MARKET fallback for legs that didn't fill on LIMIT
│
└── All 4 filled → open_trades.append(trade) · _save_trades() · Telegram alert
MDC Controlled Scale-In Guards
MasterDCScanner.scan(open_trades)
│
├── no open MDC anchor → normal first-entry classification
├── newest remaining MDC anchor → require valid open_time, spot_at_entry, and near_straddle_entry
├── cooldown → require ≥ 90 minutes
├── movement → require |spot − anchor spot| ≥ max(200 pts, 50% of anchor near straddle)
├── anchor health → require state OPEN, live P&L available, and P&L > −10% of risk basis
├── classify current regime normally → DC / DCS / DCS_SKEW / DDC / IC
└── ExecutionEngine exact-leg guard → reject incomplete, malformed, or identical complete option security-ID sets before slot checks, simulation, or orders
Double Calendar Straddle MARKET ORDERS
Runtime mode comes from SQLite records. In DRY_RUN, no orders are placed — scanner LTPs are used as simulated fill prices. SHADOW and LIVE use MARKET orders with no LTP pre-fetch; shadow also writes a comparison record to metrics.
execute_double_calendar(signal)
│
├── state == DRY_RUN? → use scanner LTPs as fills, compute entry_cost via TransactionCosts.dc_entry_cost(), no orders
│
├── [LIVE] can_trade() kill switch + balance + limits
│
├── Place 4 MARKET legs in order (far first reduces net gamma risk during entry):
│ 1. BUY far_CE (MARKET, price=0)
│ 2. BUY far_PE (MARKET, price=0)
│ 3. SELL near_CE (MARKET, price=0)
│ 4. SELL near_PE (MARKET, price=0)
│ each leg: daily_order_count += 1 → _poll_fill(oid)
│ ├── filled → continue to next leg
│ └── not filled → _auto_reverse(filled_legs_so_far) → abort
│
└── All 4 filled / dry-run →
├── execution.double_calendar.build_dc_trade() strategy="DC", entry_cost=₹X, net_debit=₹Y, near_straddle_entry=Z
├── open_trades.append(trade) · TradeStore.save()
├── metrics.record_execution(..., outcome="simulated"/"executed")
├── if SHADOW: metrics.record_close(tag="SHADOW_DR_DC_*", close_reason="shadow_entry_snapshot", shadow=True)
└── Telegram alert with near/far premiums, net debit, 3 stop conditions
Stretched DC MARKET ORDERS
Controlled at runtime by SQLite records: DCS for symmetric DCS and SDCS for DCS_SKEW. Live path uses MARKET orders for all 8 legs — same pattern as DC. Tag prefix is DR_ in dry-run and LIVE_ in shadow/live.
execute_stretched_double_calendar(signal)
│
├── state == DRY_RUN? → use scanner LTPs as fills, compute entry_cost via TransactionCosts.dcs_entry_cost() (8 legs)
│
├── [LIVE] can_trade() kill switch + balance + limits
│
├── [LIVE] Place 8 MARKET legs (far first):
│ 1. BUY far_CE (ATM, MARKET)
│ 2. BUY far_PE (ATM, MARKET)
│ 3. BUY far_OTM_CE (ATM + wing, MARKET)
│ 4. BUY far_OTM_PE (ATM − wing, MARKET)
│ 5. SELL near_CE (ATM, MARKET)
│ 6. SELL near_PE (ATM, MARKET)
│ 7. SELL near_OTM_CE (ATM + wing, MARKET)
│ 8. SELL near_OTM_PE (ATM − wing, MARKET)
│ each leg: daily_order_count += 1 → _poll_fill(oid)
│ ├── filled → continue to next leg
│ └── not filled → _auto_reverse(filled_legs_so_far) → abort
│
└── All 8 filled / dry-run → execution.double_calendar.build_dcs_trade() → strategy="DCS", wing_offset=N pts stored in trade record
Stretched DC — Close Flow
_close_stretched_double_calendar(trade)
│
├── get_ltp(NSE_FNO, [8 ids: near_ce, near_pe, far_ce, far_pe, near_otm_ce, near_otm_pe, far_otm_ce, far_otm_pe])
│
├── is_dry? → log all 8 LTPs, return True immediately
│
└── execution.double_calendar.dcs_close_legs() → place 8 close legs (IOC LIMIT at LTP ± 0.2% slip; MARKET if LTP=0):
1. BUY near_CE (ATM) ← buy back short near leg
2. BUY near_PE (ATM)
3. BUY near_OTM_CE (ATM + wing)
4. BUY near_OTM_PE (ATM − wing)
5. SELL far_CE (ATM) ← sell long far leg
6. SELL far_PE (ATM)
7. SELL far_OTM_CE (ATM + wing)
8. SELL far_OTM_PE (ATM − wing)
any not filled → Telegram ⚠️ DCS Close PARTIAL FAILURE — check Dhan manually
AI Market DRY-RUN ONLY
Phase 1 AI_MARKET execution is intentionally dry-run only. The OpenAI response is persisted first; if the parsed verdict is BULLISH or BEARISH, the engine resolves a NIFTY weekly option near the recommended strike and records a simulated buy-side trade. No broker order is sent even if AI_MARKET_DRY_RUN is changed accidentally.
AutonomousMarketAnalyst.run_once_for_today()
│
├── skip if disabled, no OPENAI_API_KEY, not an NSE trading day, or today's analysis exists
├── AIRunner.run() → strict JSON verdict, raw response, token usage, estimated cost
├── SQLiteStore.save_ai_market_analysis()
├── NO_TRADE → store analysis only; Telegram summary
└── BULLISH / BEARISH → execute_ai_market(parsed, analysis_id)
execute_ai_market(parsed, analysis_id)
│
├── duplicate guard: no second AI_MARKET trade for the same analysis
├── resolve option type: BULLISH → CE, BEARISH → PE
├── resolve strike from parsed recommendation or nearest ATM fallback
├── fetch near-week option LTP from option chain
├── persist open trade with target, stop, lots, expiry, security_id, and raw analysis id
└── mirror row in ai_market_trades for dashboard review
Directional Diagonal Calendar DRY-RUN
DDC is a 2-leg directional diagonal. Runtime state now controls whether it simulates or places real orders; shadow/live use the same far-buy then near-sell pattern as the dry-run trade record.
DirectionalDiagonalScanner.scan(open_trades, ai_direction_hint=None)
│
├── skip if another strategy="DDC" trade is open
├── capture day open on first scan at/after 09:15 IST
├── compute (spot - day_open) / day_open
│ ├── below DDC_TREND_THRESHOLD_PCT → no signal
│ ├── weak trend plus same-day AI hint → optional BULLISH/BEARISH direction
│ ├── positive trend → BULLISH CE diagonal
│ └── negative trend → BEARISH PE diagonal
├── fetch near-week + monthly chains; require near DTE 5-9 and ATM IV 20%-25%
├── far leg = monthly ATM CE/PE
└── near leg = near-week OTM CE/PE at ATM ± DDC_OTM_DISTANCE_PTS
DDC Entry + Close Flow
execute_diagonal_calendar(signal)
│
├── state == DRY_RUN → no live orders
├── persist simulated fills:
│ ├── BUY far-month ATM option at far_ltp
│ └── SELL near-week OTM option at near_ltp
├── entry debit = far_ltp - near_ltp
└── open_trades.append() · TradeStore.save() · Telegram dry-run alert
monitor_open_positions()
│
├── current P&L = (far_current_ltp - near_current_ltp - entry_debit) × lot
├── close if P&L ≥ 30% of entry debit
├── close if P&L ≤ -20% of entry debit
├── close if spot breaches the short near strike
└── close if near-week expiry day is at/after 15:00 IST, or near expiry already passed
Iron Condor Fallback DRY-RUN
IC is a 4-leg fallback strategy. It runs only when no primary options trade is open, including MDC_* routed DC-family positions. Runtime go-live state controls whether it simulates or places real orders.
run_scan_cycle()
│
├── monitor existing positions, then run MDC as the primary options router
├── if any primary options trade is open → skip IC fallback
├── realized_range = dc_scanner.realized_range_today()
└── IronCondorScanner.scan(open_trades, realized_range)
├── require near-week DTE 3-7 and ATM IV 18%-26%
├── require realized range ≤ 60% of near ATM straddle
├── build strikes: short CE/PE ATM±300, long CE/PE ATM±500
├── net credit = (short_ce + short_pe - long_ce - long_pe) × lot
└── require net credit ≥ 20% of wing width × lot
IC Entry + Close Flow
execute_iron_condor(signal)
│
├── state == DRY_RUN → no live orders
├── persist simulated fills:
│ ├── SELL short CE and short PE at scanner LTP
│ └── BUY long CE and long PE at scanner LTP
├── entry_credit = (short premiums - long premiums) × lot
├── max_loss = wing_width - entry_credit
└── open_trades.append() · TradeStore.save() · Telegram dry-run alert
monitor_open_positions()
│
├── current_credit_value = (short_ce + short_pe - long_ce - long_pe) × lot
├── current P&L = entry_credit - current_credit_value
├── close if credit captured ≥ 50% of entry credit
├── close if current loss ≥ 1.5× entry credit
└── close if near-week expiry day is at/after 15:00 IST, or near expiry already passed
Double Calendar — Close Flow
_close_double_calendar(trade)
│
├── is_dry? → log simulated LTPs, compute close_cost, record net P&L, return True immediately
│
├── get_ltp(NSE_FNO, [near_ce, near_pe, far_ce, far_pe])
│
└── execution.double_calendar.dc_close_legs() → place 4 close legs (IOC LIMIT, 0.2% slip; MARKET if LTP=0):
1. BUY near_CE ← buy back short near leg
2. BUY near_PE
3. SELL far_CE ← sell long far leg
4. SELL far_PE
any not filled → Telegram ⚠️ DC Close PARTIAL FAILURE — check Dhan manually
Calendar Spread DISABLED
Execution is suppressed in run_scan_cycle() — the flow below is for reference only.
Uses MARKET orders with fill verification — each leg is polled
for TRADED status before the next is placed.
execute_calendar_spread(signal)
│
├── can_trade()
├── net_profit < CAL_MIN_NET_PROFIT_PER_LOT (₹50) → skip, log WARNING
│
├── place_order(far_id, MARKET) daily_order_count += 1
│ └── _poll_fill(oid1)
│ └── not filled → ABORT (no leg2 placed)
│
├── place_order(near_id, MARKET) daily_order_count += 1
│ └── _poll_fill(oid2)
│ └── not filled → _auto_reverse(leg1) · ABORT
│
└── both confirmed → open_trades.append() · _save_trades() · Telegram "OPENED"