Generate code in 3 minutes
Enter your idea and AI generates runnable code.
Here is a complete, robust implementation of an Iron Condor strategy using the QuantConnect (Lean) Python API.
An Iron Condor is a neutral strategy defined by four legs with the same expiration date:
This algorithm enters a position when the portfolio is not invested, targeting options expiring in roughly 30-45 days. It selects strikes based on a fixed distance from the underlying price to ensure the "body" of the condor captures the current price action.
# region imports
from AlgorithmImports import *
# endregion
class IronCondorStrategy(QCAlgorithm):
def initialize(self):
# 1. Set Backtest Params
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 12, 1)
self.set_cash(100000)
# 2. Add Underlying Equity (SPY)
self.ticker = "SPY"
equity = self.add_equity(self.ticker, Resolution.MINUTE)
equity.set_data_normalization_mode(DataNormalizationMode.RAW)
self.underlying_symbol = equity.symbol
# 3. Add Option and Set Filter
# We need a universe of options to select from.
option = self.add_option(self.ticker, Resolution.MINUTE)
self.option_symbol = option.symbol
# Filter:
# - Strikes: -20 to +20 relative to ATM
# - Expiry: 30 to 45 days out
option.set_filter(lambda universe: universe.include_weeklys()
.strikes(-20, 20)
.expiration(timedelta(30), timedelta(45)))
def on_data(self, slice: Slice):
# Avoid trading if we already have positions
if self.portfolio.invested:
return
# Try to get the option chain for our symbol
chain = slice.option_chains.get(self.option_symbol)
if not chain:
return
# 4. Select Expiration Date
# Sort contracts by expiry and select the furthest available date within our filter
expiry = sorted(chain, key=lambda x: x.expiry, reverse=True)[0].expiry
# Filter contracts for this specific expiry
contracts = [x for x in chain if x.expiry == expiry]
if not contracts:
return
# 5. Select Strikes
# Get the underlying price to determine OTM levels
underlying_price = chain.underlying.price
# Separate Puts and Calls
puts = sorted([x for x in contracts if x.right == OptionRight.PUT], key=lambda x: x.strike)
calls = sorted([x for x in contracts if x.right == OptionRight.CALL], key=lambda x: x.strike)
if not puts or not calls:
return
# Logic:
# Short Strikes: ~5 points OTM
# Long Strikes (Protection): ~10 points OTM (5 points width wings)
# Find Short Put (Strike < Price)
short_put = min(puts, key=lambda x: abs(x.strike - (underlying_price - 5)))
# Find Long Put (Strike < Short Put)
long_put = min(puts, key=lambda x: abs(x.strike - (underlying_price - 10)))
# Find Short Call (Strike > Price)
short_call = min(calls, key=lambda x: abs(x.strike - (underlying_price + 5)))
# Find Long Call (Strike > Short Call)
long_call = min(calls, key=lambda x: abs(x.strike - (underlying_price + 10)))
# Validation: Ensure strikes are distinct and ordered correctly for an Iron Condor
# Order: Long Put < Short Put < Price < Short Call < Long Call
if not (long_put.strike < short_put.strike < underlying_price < short_call.strike < long_call.strike):
self.debug(f"Strikes not aligned for Iron Condor at {self.time}. Skipping.")
return
# 6. Execute Strategy
# QuantConnect provides a helper 'OptionStrategies.IronCondor' to bundle the legs
iron_condor = OptionStrategies.iron_condor(
self.option_symbol,
long_put.strike,
short_put.strike,
short_call.strike,
long_call.strike,
expiry
)
self.buy(iron_condor, 1)
self.debug(f"Opened Iron Condor at {self.time}: "
f"Long Put {long_put.strike} | Short Put {short_put.strike} | "
f"Short Call {short_call.strike} | Long Call {long_call.strike}")
def on_order_event(self, order_event):
# Log fills for debugging
if order_event.status == OrderStatus.FILLED:
self.log(f"Order Filled: {order_event}")
Universe Filtering (set_filter):
Strike Selection:
OptionStrategies.iron_condor:
MarketOrder or LimitOrder commands, we use the QuantConnect helper.Exit Logic:
if self.portfolio.invested: return line prevents opening new positions while one is active. Once the options expire and disappear from the portfolio, the algorithm will look for a new setup.Q: How do I change the width of the wings?
A: Modify the subtraction/addition logic in the strike selection section. For example, change underlying_price - 10 to underlying_price - 15 to make the wings wider (increasing risk but also increasing probability of profit).
Q: How can I add a profit taker (e.g., close at 50% profit)?
A: You would need to track the Ticket returned by self.Buy. In OnData, check the current unrealized profit of the strategy. If it exceeds 50% of the credit received, use self.Liquidate() to close the position.
Q: Why use DataNormalizationMode.RAW?
A: When trading options, it is critical to use Raw data for the underlying equity. Adjusted data (which accounts for splits and dividends retroactively) will cause the strike prices of historical options to misalign with the underlying price data.
Q: Can I use Delta for selection instead of price?
A: Yes. You would need to ensure the option chain contains Greeks (which is standard in QC backtests). You would replace the min(key=lambda x: abs(x.strike...)) logic with min(key=lambda x: abs(x.greeks.delta - 0.20)) to find the 20 Delta contract.