1. Iron Condor Mechanics: The Four-Leg Architecture
An iron condor is a defined-risk, non-directional options strategy that profits when the underlying asset trades within a bounded range until expiration. It consists of four simultaneous legs that together create a "condor-shaped" payoff profile.
The structure combines two vertical spreads: a bear call spread above the market and a bull put spread below. The net result is a position that collects premium upfront and retains it as long as price stays inside the breakeven range.
The Four Legs Defined
| Leg | Action | Strike Position | Role |
|---|---|---|---|
| Short Call | Sell | OTM above market | Defines upper profit boundary |
| Long Call | Buy | Further OTM above short call | Caps max loss on upside |
| Short Put | Sell | OTM below market | Defines lower profit boundary |
| Long Put | Buy | Further OTM below short put | Caps max loss on downside |
Payoff Zones and Break-Even Calculation
When entering the trade, a net credit is received. This credit defines the maximum profit, while the width of either vertical spread minus the credit defines maximum loss.
Probability of Profit
Iron condors typically carry a 60-75% probability of profit at standard strike selections (approximately the 16-delta short strikes, placing each breakeven roughly one standard deviation from current price). However, winning trades are small relative to occasional losing trades — risk management is paramount.
2. Delta Management and Directional Risk
While iron condors are designed to be delta-neutral at entry, price movement immediately creates directional exposure. An agent must monitor net delta continuously and make adjustments before a losing leg threatens the max loss threshold.
Understanding Net Position Delta
The four legs each carry their own delta. At equilibrium, the short put's positive delta and the short call's negative delta cancel. As price moves toward a short strike, that leg's delta grows, skewing the position.
# Delta monitoring for a live iron condor from dataclasses import dataclass from typing import Dict import numpy as np @dataclass class OptionLeg: strike: float leg_type: str # 'call' or 'put' position: int # +1 long, -1 short quantity: int delta: float class IronCondorMonitor: def __init__(self): self.legs: Dict[str, OptionLeg] = {} self.delta_threshold = 0.12 # trigger adjustment self.max_loss_pct = 0.50 # close at 50% max loss def net_delta(self) -> float: total = 0.0 for leg in self.legs.values(): total += leg.delta * leg.position * leg.quantity * 100 return total def needs_adjustment(self) -> bool: return abs(self.net_delta()) > self.delta_threshold def adjustment_direction(self) -> str: nd = self.net_delta() if nd > self.delta_threshold: return "short_call_breached_roll_up" elif nd < -self.delta_threshold: return "short_put_breached_roll_down" return "neutral"
Delta-Neutral Adjustment Strategies
When net delta exceeds the threshold, an agent has three primary adjustment tools:
- Roll the tested side: Buy back the breached short strike and sell a new one further OTM. Collects additional premium if IV has risen.
- Add a hedge: Buy a single long option or small vertical in the direction of the breach to offset delta quickly.
- Convert to broken-wing butterfly: Widen the wing on the untested side, reducing buying power while shifting the profit zone toward the breakeven.
Do not roll a short strike that is already in-the-money. Closing the position outright at a defined loss is almost always preferable to chasing a debit roll on an ITM leg.
3. Theta Decay Profile and Time-Based Management
Theta (time decay) is the iron condor's primary profit engine. Options lose value every day as expiration approaches — iron condors sell this decay. Understanding theta's non-linear behavior is critical for agent-level automation.
The Theta Acceleration Curve
Theta decay is not linear. It accelerates sharply in the final 30 days before expiration, with the most rapid decay occurring in the last two weeks. An iron condor entered at 45 DTE will see roughly 40% of its total theta in the last 15 days — but gamma risk also spikes in this window.
import numpy as np import scipy.stats as stats def black_scholes_theta(S, K, T, r, sigma, option_type='call'): """Theta in dollars per day for one contract (100 shares).""" if T <= 0: return 0 d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T)) d2 = d1 - sigma*np.sqrt(T) nd1 = stats.norm.pdf(d1) if option_type == 'call': theta = (-(S*nd1*sigma)/(2*np.sqrt(T)) - r*K*np.exp(-r*T)*stats.norm.cdf(d2)) / 365 else: theta = (-(S*nd1*sigma)/(2*np.sqrt(T)) + r*K*np.exp(-r*T)*stats.norm.cdf(-d2)) / 365 return theta * 100 # per contract def condor_daily_theta(S, short_call, long_call, short_put, long_put, T, r, sigma): """Net theta of full iron condor position.""" sc_theta = black_scholes_theta(S, short_call, T, r, sigma, 'call') lc_theta = black_scholes_theta(S, long_call, T, r, sigma, 'call') sp_theta = black_scholes_theta(S, short_put, T, r, sigma, 'put') lp_theta = black_scholes_theta(S, long_put, T, r, sigma, 'put') # Short legs positive theta (we collect), long legs negative return (-sc_theta) + lc_theta + (-sp_theta) + lp_theta
Time-Based Exit Rules
Experienced traders exit iron condors well before expiration to avoid gamma risk. A well-tuned agent should implement these DTE-based exits:
| DTE Remaining | Action | Rationale |
|---|---|---|
| 21 DTE | Close if 50% profit achieved | Capture majority of theta; avoid gamma spike |
| 15 DTE | Close at 25% profit if still open | Gamma risk outweighs remaining theta |
| 7 DTE | Close immediately regardless of P&L | Pin risk and gap risk become unmanageable |
| Any DTE | Close at 200% of credit received (loss) | Hard max-loss rule to prevent runaway loss |
4. IV Crush Exploitation: Entry Signal Engineering
Implied volatility rank (IVR) is the most important entry signal for iron condors. When IV is elevated — meaning options are expensive — more premium can be collected for the same strike distance. After the catalyst (earnings, economic event) passes, IV collapses, dramatically reducing the option values even if price barely moved.
IV Rank Calculation
IVR compares current IV to its 52-week high and low:
def iv_rank(current_iv: float, iv_52w_high: float, iv_52w_low: float) -> float: """Returns 0-100. Above 50 = elevated IV, favorable for selling.""" if iv_52w_high == iv_52w_low: return 50.0 return (current_iv - iv_52w_low) / (iv_52w_high - iv_52w_low) * 100 def iv_percentile(current_iv: float, historical_ivs: list) -> float: """Percentage of past readings below current IV (more robust than IVR).""" return sum(1 for iv in historical_ivs if iv < current_iv) / len(historical_ivs) * 100 # Entry filter: only sell iron condors when IV is elevated def should_enter_condor(ivr: float, dte: int, vix: float) -> bool: return ( ivr >= 50 # IV elevated relative to recent history and 30 <= dte <= 50 # optimal theta-to-gamma window and vix < 35 # avoid entering during extreme panic )
IV Crush Trade Examples
The classic IV crush play is placing an iron condor before earnings, then closing shortly after the announcement regardless of price action. Even if the stock moves significantly, the collapse in IV (often 40-60%) can make the position profitable.
Pre-earnings: IV = 85%. Entry credit = $3.20 on a $10-wide condor. Post-earnings: IV = 45%, stock moved 3%. Position value drops to $1.80. Close for $1.40 profit (44% of max) in under 24 hours.
5. Rolling and Adjusting Positions
Rolling means closing an existing leg (or full spread) and re-opening it at a different strike or expiration. For iron condors, rolling is the primary defensive mechanism when price threatens a short strike.
Roll Types and When to Use Each
- Roll out in time: Close current expiry, re-open same strikes in next monthly expiry. Collects additional credit; use when position is tested early (30+ DTE remaining) and IV is still elevated.
- Roll the tested side: Buy back the threatened short strike, sell a new one further OTM. Converts a losing spread into a higher-credit spread. Requires IV to be elevated on the new strike.
- Roll the untested side in: Move the winning spread closer to the money to collect additional premium. Increases risk but improves the credit-to-max-loss ratio.
- Convert to time spread: If IV has collapsed dramatically, convert to a calendar or diagonal to maintain positive vega exposure.
class CondorAdjuster: def __init__(self, broker_client, position): self.broker = broker_client self.position = position async def roll_tested_side(self, side: str, new_short_strike: float, new_long_strike: float, expiry: str): """Close breached spread, open new spread further OTM.""" if side == 'call': # Buy back short call, sell long call (close bear call spread) await self.broker.close_spread( self.position.short_call, self.position.long_call, 'call' ) # Open new bear call spread further OTM credit = await self.broker.open_vertical( short_strike=new_short_strike, long_strike=new_long_strike, option_type='call', expiry=expiry, action='sell' ) elif side == 'put': await self.broker.close_spread( self.position.short_put, self.position.long_put, 'put' ) credit = await self.broker.open_vertical( short_strike=new_short_strike, long_strike=new_long_strike, option_type='put', expiry=expiry, action='sell' ) return {'additional_credit': credit, 'new_position': 'updated'} def should_roll(self, underlying_price: float, current_iv: float, dte: int) -> dict: """Determine if and how to roll the position.""" sc_dist = self.position.short_call - underlying_price sp_dist = underlying_price - self.position.short_put if sc_dist < 0: # price above short call return {'action': 'roll', 'side': 'call', 'urgency': 'high'} elif sp_dist < 0: # price below short put return {'action': 'roll', 'side': 'put', 'urgency': 'high'} elif sc_dist / underlying_price < 0.02: # within 2% of short call return {'action': 'monitor', 'side': 'call', 'urgency': 'medium'} elif sp_dist / underlying_price < 0.02: return {'action': 'monitor', 'side': 'put', 'urgency': 'medium'} return {'action': 'hold', 'urgency': 'none'}
6. Full IronCondorAgent Implementation
The following is a complete autonomous agent that scans for IV rank signals, selects strikes, manages the lifecycle of iron condor positions, and executes rolls when necessary. It integrates with Purple Flea's trading infrastructure at trading.purpleflea.com.
import asyncio import logging from datetime import datetime, timedelta from typing import Optional, List import httpx # Purple Flea Trading API configuration PF_TRADING_URL = "https://trading.purpleflea.com" PF_WALLET_URL = "https://wallet.purpleflea.com" class IronCondorAgent: """ Autonomous agent that runs a theta-harvesting iron condor strategy. Entry: IVR >= 50, DTE 30-45 Management: Delta threshold, 50% profit target, 21-DTE exit Exit: 50% profit, max loss at 200% credit, or 21 DTE """ def __init__(self, api_key: str, wallet_address: str, max_position_size: float = 5000.0): self.api_key = api_key self.wallet = wallet_address self.max_size = max_position_size self.positions = {} self.logger = logging.getLogger('IronCondorAgent') self.client = httpx.AsyncClient( headers={'Authorization': f'Bearer {api_key}'}, timeout=30 ) async def scan_for_entries(self, symbols: List[str]) -> list: """Find symbols with elevated IV suitable for condor entry.""" candidates = [] for symbol in symbols: resp = await self.client.get(f'{PF_TRADING_URL}/iv-data/{symbol}') data = resp.json() ivr = iv_rank(data['current_iv'], data['iv_52w_high'], data['iv_52w_low']) dte = data['nearest_monthly_dte'] vix = data['vix'] if should_enter_condor(ivr, dte, vix): candidates.append({ 'symbol': symbol, 'ivr': ivr, 'dte': dte, 'price': data['underlying_price'], 'iv': data['current_iv'] }) return sorted(candidates, key=lambda x: x['ivr'], reverse=True) async def select_strikes(self, symbol: str, price: float, iv: float, dte: int) -> dict: """Select strikes targeting ~0.16 delta on short legs, $5-wide wings.""" T = dte / 365 sigma = iv one_std_move = price * sigma * (T ** 0.5) # Short strikes at approximately 1 std dev OTM short_call = round(price + one_std_move, 0) short_put = round(price - one_std_move, 0) # Long strikes $5 further OTM (wing width) long_call = short_call + 5 long_put = short_put - 5 return { 'short_call': short_call, 'long_call': long_call, 'short_put': short_put, 'long_put': long_put, 'wing_width': 5, 'one_std_move': one_std_move } async def open_condor(self, symbol: str, strikes: dict, expiry: str) -> dict: """Submit all four legs as a combo order.""" order = { 'symbol': symbol, 'strategy': 'iron_condor', 'expiry': expiry, 'legs': [ {'strike': strikes['short_call'], 'type': 'call', 'action': 'sell', 'qty': 1}, {'strike': strikes['long_call'], 'type': 'call', 'action': 'buy', 'qty': 1}, {'strike': strikes['short_put'], 'type': 'put', 'action': 'sell', 'qty': 1}, {'strike': strikes['long_put'], 'type': 'put', 'action': 'buy', 'qty': 1}, ], 'order_type': 'limit', 'limit_price': 'mid' # always try to get mid-market fill } resp = await self.client.post(f'{PF_TRADING_URL}/orders/combo', json=order) result = resp.json() self.positions[result['order_id']] = {**strikes, **result, 'symbol': symbol} return result async def manage_positions(self): """Main management loop — run every 5 minutes during market hours.""" for pos_id, pos in list(self.positions.items()): data = await self.client.get(f'{PF_TRADING_URL}/positions/{pos_id}') p = data.json() pnl_pct = p['unrealized_pnl'] / p['max_profit'] dte = p['dte'] # Exit rules if pnl_pct >= 0.50 and dte <= 21: await self.close_position(pos_id, reason="50pct profit target") elif p['unrealized_pnl'] <= -p['max_profit'] * 2: await self.close_position(pos_id, reason="max loss 200pct") elif dte <= 7: await self.close_position(pos_id, reason="7 DTE hard exit") else: # Delta check monitor = IronCondorMonitor() if monitor.needs_adjustment(): direction = monitor.adjustment_direction() self.logger.info(ff'Adjustment needed: {direction}') async def close_position(self, pos_id: str, reason: str = ""): self.logger.info(ff'Closing {pos_id}: {reason}') await self.client.post(ff'{PF_TRADING_URL}/positions/{pos_id}/close') del self.positions[pos_id] async def run(self, symbols: List[str], poll_seconds: int = 300): self.logger.info("IronCondorAgent starting...") while True: try: # Find new entries if under position limit if len(self.positions) < 5: candidates = await self.scan_for_entries(symbols) if candidates: best = candidates[0] strikes = await self.select_strikes( best['symbol'], best['price'], best['iv'], best['dte'] ) expiry = (datetime.now() + timedelta(days=best['dte'])).strftime('%Y-%m-%d') await self.open_condor(best['symbol'], strikes, expiry) # Manage existing positions await self.manage_positions() except Exception as e: self.logger.error(ff'Error in main loop: {e}') await asyncio.sleep(poll_seconds)
7. Risk Management and Position Sizing
Iron condors carry defined risk but that risk must still be carefully allocated. A single bad position can wipe out months of theta gains if sizing is not disciplined.
Kelly Criterion for Iron Condors
Adapting the Kelly formula for defined-risk credit strategies:
def kelly_fraction_condor( prob_profit: float, # e.g. 0.68 credit_received: float, # e.g. $2.50 max_loss: float, # e.g. $7.50 (wing width - credit) kelly_fraction: float = 0.25 # fractional Kelly for safety ) -> float: """Returns fraction of account to risk per condor.""" prob_loss = 1 - prob_profit # Kelly: f = (p * b - q) / b where b = win/loss ratio b = credit_received / max_loss kelly = (prob_profit * b - prob_loss) / b return max(0, kelly * kelly_fraction) # Example: 68% PoP, collect $2.50 on $7.50 max loss f = kelly_fraction_condor(0.68, 2.50, 7.50) # f ≈ 0.057 → risk ~5.7% of account per condor
Correlation Risk Across Multiple Condors
Running condors on correlated underlying assets compounds tail risk. If the S&P drops 5% in one day, all equity condors breach simultaneously. An agent must track sector concentration and cap correlated exposure.
Never run more than 2-3 condors on assets from the same sector simultaneously. VIX spikes and macro events create correlated breaches that no individual risk rule can protect against.
8. Performance Benchmarking and Monitoring
An agent must maintain a live performance dashboard to validate that the strategy remains profitable over rolling windows, not just individual trades.
class CondorPerformanceTracker: def __init__(self): self.trades = [] def record_trade(self, credit: float, pnl: float, max_profit: float, max_loss: float, dte_at_entry: int): self.trades.append({ 'credit': credit, 'pnl': pnl, 'max_profit': max_profit, 'max_loss': max_loss, 'dte': dte_at_entry, 'won': pnl > 0 }) def summary(self) -> dict: if not self.trades: return {} wins = [t for t in self.trades if t['won']] losses = [t for t in self.trades if not t['won']] total_pnl = sum(t['pnl'] for t in self.trades) return { 'total_trades': len(self.trades), 'win_rate': len(wins) / len(self.trades), 'avg_win': sum(t['pnl'] for t in wins) / (len(wins) or 1), 'avg_loss': sum(t['pnl'] for t in losses) / (len(losses) or 1), 'total_pnl': total_pnl, 'expectancy': total_pnl / len(self.trades) }
9. Backtesting Framework for Iron Condor Agents
Before deploying live capital, every iron condor agent should be validated against historical data. A proper backtest simulates realistic entry conditions, bid-ask spreads, and management actions rather than just checking theoretical payoffs.
Realistic Backtest Assumptions
- Use mid-price with 10-20% slippage: Never assume perfect fills at mid. Real executions often land 10-15% worse than mid on multi-leg options orders.
- Model commissions: Even $0.65/contract adds up across four legs opened and four legs closed = $5.20 round-trip minimum.
- Include assignment risk: Short options that go deeply ITM near expiration may be assigned early. Model this in your last-5-DTE window.
- Use IV surface, not single IV: Calls and puts of the same DTE but different strikes trade at different IVs (volatility skew). Use actual surface data, not ATM IV for all legs.
class IronCondorBacktester: def __init__(self, price_history: pd.DataFrame, iv_history: pd.DataFrame): self.prices = price_history self.ivs = iv_history self.results = [] self.slippage = 0.15 # 15% worse than mid self.commission_per_leg = 0.65 def run(self, ivr_threshold: float = 50, target_dte: int = 40, profit_target_pct: float = 0.50, stop_pct: float = 2.0): """Iterate over historical data, simulate condor entries and exits.""" for i in range(60, len(self.prices)): row = self.prices.iloc[i] iv_row = self.ivs.iloc[i] ivr = iv_rank(iv_row['atm_iv'], iv_row['iv_52w_high'], iv_row['iv_52w_low']) if ivr < ivr_threshold: continue # no entry signal S = row['close'] sigma = iv_row['atm_iv'] T = target_dte / 365 one_std = S * sigma * (T ** 0.5) sc = round(S + one_std) lc = sc + 5 sp = round(S - one_std) lp = sp - 5 # Estimate entry credit (simplified BS) sc_prem = max(0.01, S * sigma * (T**0.5) * 0.10) sp_prem = max(0.01, S * sigma * (T**0.5) * 0.10) credit = (sc_prem + sp_prem) * (1 - self.slippage) commission = self.commission_per_leg * 4 net_credit = credit - commission / 100 max_loss = 5 - net_credit # Simulate forward price path over DTE days end_idx = min(i + target_dte, len(self.prices) - 1) outcome_price = self.prices.iloc[end_idx]['close'] if sp <= outcome_price <= sc: pnl = net_credit # full profit result = 'max_profit' elif outcome_price > sc + net_credit: pnl = -max_loss result = 'max_loss_call' elif outcome_price < sp - net_credit: pnl = -max_loss result = 'max_loss_put' else: # Partial profit/loss in breakeven zone pnl = net_credit - max(0, abs(outcome_price - sc) - net_credit, abs(outcome_price - sp) - net_credit) result = 'partial' self.results.append({ 'entry_date': row.get('date', i), 'entry_price': S, 'ivr': ivr, 'net_credit': net_credit, 'pnl': pnl, 'result': result }) def summary(self) -> dict: if not self.results: return {} total = len(self.results) wins = [r for r in self.results if r['pnl'] > 0] total_pnl = sum(r['pnl'] for r in self.results) return { 'total_trades': total, 'win_rate': len(wins) / total, 'expectancy_per_trade': total_pnl / total, 'total_pnl': total_pnl, 'avg_win': sum(r['pnl'] for r in wins) / (len(wins) or 1) }
10. Multi-Asset Portfolio of Iron Condors
Running a single iron condor at a time leaves capital idle between cycles. A portfolio approach runs multiple condors simultaneously across uncorrelated assets, smoothing returns and improving capital utilization.
Portfolio Construction Rules
- Maximum 3 condors per correlated sector: Energy, tech, financials should each have at most 3 simultaneous positions. A sector shock creates correlated losses.
- Stagger expirations: Do not let multiple condors expire on the same date. Spread expirations across weekly and monthly cycles to smooth cash flow.
- Ladder entry across IV environments: If IV is elevated across the board, scale in over multiple sessions rather than deploying all capital at one IV level.
- Monitor portfolio-level delta: Sum the net delta across all positions. If the portfolio is net long or short delta by more than 0.5% of notional, add a hedge.
- Maintain cash reserve of 20%: Keep 20% of total capital undeployed for adjustment capital. Rolling legs requires buying power.
A diversified portfolio of 5 iron condors across uncorrelated assets, with an average 65% win rate and 2:1 loss-to-win ratio, produces a positive expectancy of approximately $0.18 per dollar of max risk deployed per cycle — before compounding. Over 12-15 cycles per year per symbol, the math favors systematic deployment.
Deploy Your Iron Condor Agent
Access 275+ perpetual markets for options-like volatility plays. Fund your agent wallet, register, and start collecting theta decay systematically.