Exit Rules

Position Monitoring & Exit Rules

monitor_open_positions() is called every scan cycle (every 30 s). DC/DCS threshold ordering lives in execution.position_monitor; the engine still owns LTP fetching, order placement, and final close bookkeeping.

Position State Machine

StateWhen SetMeaning
OPENAt entryNormal monitoring.
PROFIT_LOCKPeak observed P&L reaches TRAILING_PROFIT_LOCK_PCT of expected profitTrailing floor is active for DC-family option spreads.
EXPIRY_APPROACHNear expiry DTE ≤ 2Position is near expiry and should be watched under tighter operator scrutiny.
CLOSINGExit condition has fired and close routing is about to runClose is in progress or awaiting confirmation; repeated monitor actions are skipped.

Box Spread — 3 exit conditions (checked in priority order)

#ConditionActionWhy
1 days_left < 0 Close immediately Bot was down over expiry weekend
2 days_left ≤ 2 Close early Avoid pin risk near expiry
3 net_profit_after_close ≥ 0 AND gross_profit ≥ 70% of max_gross Close early Capture profit early, free up margin — but only if closing costs don't wipe the gain
# Profit target calculation (live LTPs fetched each cycle)
current_box_value    = C1_ltp − C2_ltp + P2_ltp − P1_ltp
current_profit_gross = (current_box_value − entry_net_debit) × lot
max_profit_gross     = (K2 − K1 − entry_net_debit) × lot
est_close_cost       = trade["total_cost"]  # use opening cost as proxy for closing cost
net_profit_after_close = current_profit_gross − est_close_cost

if net_profit_after_close > 0 and current_profit_gross >= max_profit_gross * 0.70:
    close_position()                    # 70% gross captured AND net is positive → exit
elif net_profit_after_close <= 0 and current_profit_gross > 0:
    hold_to_expiry()                    # close cost would wipe the profit — hold

Calendar Spread — 1 exit condition

ConditionAction
|z_score| ≤ CAL_EXIT_SIGMA (0.5) Close — spread reverted to mean

Double Calendar — 4 exit conditions (checked in priority order)

#ConditionActionWhy
1 dte_near < 0, or dte_near = 0 at/after NEAR_EXPIRY_TIME_STOP_HOUR:NEAR_EXPIRY_TIME_STOP_MIN IST Time stop — close all 4 legs Holds through expiry day morning, then exits before the close window.
2 Net P&L ≤ −DC_STOP_PNL_PCT (20%) × |net_debit|
(after entry + estimated close costs)
P&L stop — Trigger 1 Max loss guardrail regardless of IV or spot. Fires first — fastest to compute.
3 Entry IV − current IV ≥ DC_STOP_IV_DROP (10 pts)
Current IV estimated from live straddle LTPs
IV crush stop — Trigger 2 DC is Vega-long — sustained IV drop kills far legs faster than near theta helps. 10pt drop is the configured regime-change stop.
4 |spot − atm_strike| > DC_STOP_TENT_WIDTH (1.30) × near_straddle_entry
Only active when DTE ≥ DC_STOP_TENT_DTE (3)
Tent break — Trigger 3 Spot outside the profit tent means delta is no longer neutral. Tight tent at DTE ≥ 3 only — inside 3 DTE the near gamma is too high to trust the tent shape.
Net P&L ≥ DC_PROFIT_TARGET_PCT (32%) × |net_debit|
(net of entry cost + estimated close cost)
Profit target — close all 4 legs 32% of net debit captured — theta is front-loaded so the bulk arrives in the first 2-3 days.
# DC P&L calculation (live LTPs fetched each cycle)
near_pnl    = (near_ce_entry − nc_ltp) + (near_pe_entry − np_ltp)  # profit from decaying short
far_pnl     = (fc_ltp − far_ce_entry) + (fp_ltp − far_pe_entry)    # profit from long legs
gross_pnl   = (near_pnl + far_pnl) × lot_size

entry_cost  = trade["entry_cost"]                                    # stored at execution time
close_cost  = TransactionCosts.dc_close_cost(lot, nc, np, fc, fp)   # estimated live
net_pnl     = gross_pnl − entry_cost − close_cost

# Trigger 1: P&L stop
pnl_stop = −abs(trade["net_debit"]) × DC_STOP_PNL_PCT               # −20% of net debit
if net_pnl <= pnl_stop: close_all_4_legs()

# Trigger 2: IV drop (estimated from straddle LTPs, no extra API call)
cur_iv = (nc_ltp + np_ltp) / (0.8 × spot × sqrt(dte_near / 365)) × 100
if entry_iv − cur_iv >= DC_STOP_IV_DROP: close_all_4_legs()

# Trigger 3: tent break (only when DTE ≥ 3)
tent_half = trade["near_straddle_entry"] × DC_STOP_TENT_WIDTH
if dte_near >= DC_STOP_TENT_DTE and abs(spot − atm_strike) > tent_half: close_all_4_legs()

# Profit target: 32% of net debit, net of costs
target = abs(trade["net_debit"]) × DC_PROFIT_TARGET_PCT
if net_pnl >= target: close_all_4_legs()

Stretched DC (DCS) — same 4 triggers, higher profit target

DCS uses the same P&L, IV, and time-stop triggers as DC, but its tent break is wider: DCS_STOP_TENT_WIDTH = 1.40. Its profit target is 42% of net debit (vs 32%) because the OTM wings take 1-2 extra days to contribute meaningful theta differential, so the position is held slightly longer.

AI Market — 3 exit conditions

#ConditionActionBookkeeping
1 Live option LTP reaches the parsed target price Profit target — close dry-run leg Remove from open_trades, close matching ai_market_trades row.
2 Live option LTP reaches the parsed stop price Stop loss — close dry-run leg Records realised simulated P&L against the analysis id.
3 End-of-day monitor window Time close Prevents the AI single-leg trade from carrying overnight.

AI_MARKET is phase-1 dry-run only. The close path fetches live LTP for valuation but never sends a broker close order.

Master DC (MDC_*) — same base exits as the routed strategy

MDC trades are persisted with an MDC_* strategy key, but exit routing strips the prefix and reuses the same base close logic as the selected strategy. For example: MDC_DC follows the DC exit rules, MDC_DCS and MDC_DCS_SKEW follow the DCS exit rules, MDC_DDC follows DDC rules, and MDC_IC follows IC rules.

Iron Condor (IC) — 3 exit conditions

#ConditionActionCalculation
1 Credit captured ≥ IC_PROFIT_TARGET_PCT (50%) × entry credit Profit target — close all 4 legs current_pnl = entry_credit - current_credit_value
2 Current loss ≥ IC_STOP_LOSS_CREDIT_X (1.5×) × entry credit Stop loss — close all 4 legs current_loss = current_credit_value - entry_credit
3 dte_near < 0, or dte_near = 0 at/after configured time-stop IST Time stop — close all 4 legs Uses live LTPs if available; falls back to entry credit on expiry-edge LTP failure.

Backtest reuse of live exit helpers

The backtesting engine never re-implements exit logic. arb_bot/backtest/simulation/exit_evaluator.py calls the exact same evaluate_dc_exit(), evaluate_dcs_exit(), evaluate_ddc_exit(), and evaluate_ic_exit() functions (with DCS_SKEW routed through evaluate_dcs_exit()), preceded by update_calendar_position_state() on every cycle — identical to live monitoring. The human-readable reason string each helper returns is normalized to a stable enum-like code for reporting: PROFIT_TARGET, PNL_STOP, TENT_BREAK, TIME_STOP, IV_STOP, SHORT_STRIKE_BREACH, TRAILING_STOP, plus the engine-only END_OF_RANGE. Because the helpers read Config directly and the engine temporarily patches those class attributes per cycle, parameter sweeps change real exit decisions without any code duplication.