Order Flow

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"

Position snapshot verification

Close recovery and position audit paths distinguish a successful, complete broker snapshot from an unavailable or malformed response. An empty successful snapshot may prove a tracked leg is flat; a failed fetch never does. Missing Dhan data is unavailable, not an empty account. The engine requires a security ID and a finite numeric quantity on every position row. When the snapshot is unknown, reconciliation takes no mutation action and the tracked trade remains visible for operator review. The Dhan and Zerodha adapters expose this status vialast_positions_fetch_ok.