# region imports
from AlgorithmImports import *
# endregion
class ATMStrangleAlgo(QCAlgorithm):
def Initialize(self):
# Algorithm settings
self.SetStartDate(2025, 4, 15)
self.SetEndDate(2025, 6, 30)
self.SetCash(25000)
self.set_brokerage_model(BrokerageName.CHARLES_SCHWAB, AccountType.MARGIN)
self.default_order_properties.time_in_force = TimeInForce.DAY
# Underlying equity and options
self.symbol = self.AddEquity("QQQ", Resolution.Minute).Symbol
option = self.AddOption("QQQ", Resolution.Minute)
option.SetFilter(self.UniverseFunc)
self.optionSymbol = option.Symbol
# Risk parameters and thresholds
self.limit_down = -0.02 # range for closing the put
self.limit_up = 0.02 # range for closing the call
self.targetDelta = 0.3 # Target delta for equal‑delta strangle
self.stop_pct = -0.5 # stop out leg at -15%
# Tracking variables
self.currentPositions = {} # {Symbol: info_dict}
self.lastTradeDate = None
self.dayOpenPrice = None # Underlying open price for range checks
# Schedule order entry and exit
self.Schedule.On(self.DateRules.EveryDay(self.symbol),
self.TimeRules.AfterMarketOpen(self.symbol, 10),
self.OpenStrangle)
self.Schedule.On(self.DateRules.EveryDay(self.symbol),
self.TimeRules.BeforeMarketClose(self.symbol, 5),
self.CloseAllPositions)
def UniverseFunc(self, universe):
# Include weeklies expiring in 1-7 days
return universe.IncludeWeeklys().Expiration(timedelta(1), timedelta(7))
def OpenStrangle(self):
# Only trade once per day
if self.lastTradeDate == self.Time.date(): return
# Ensure no existing option positions
if any(self.Portfolio[s].Invested for s in self.currentPositions): return
# Get current option chain and underlying bar
chain = self.CurrentSlice.OptionChains.get(self.optionSymbol)
bar = self.CurrentSlice.Bars.get(self.symbol)
if chain is None or bar is None: return
underlying_price = bar.Close
self.dayOpenPrice = underlying_price
# Filter for valid contracts
contracts = [c for c in chain if c.BidPrice>0 and c.AskPrice>0 and c.Greeks and c.Greeks.Delta is not None]
if not contracts: return
# Choose nearest expiration
expiry = min({c.Expiry for c in contracts})
daily_contracts = [c for c in contracts if c.Expiry == expiry]
# Separate calls and puts
calls = [c for c in daily_contracts if c.Right == OptionRight.Call]
puts = [c for c in daily_contracts if c.Right == OptionRight.Put]
if not calls or not puts: return
# Select legs by delta
call_contract = self.SelectBestContract(calls, self.targetDelta, is_call=True)
put_contract = self.SelectBestContract(puts, self.targetDelta, is_call=False)
if not call_contract or not put_contract: return
# Place 1-contract market orders
self.MarketOrder(call_contract.Symbol, 1)
self.MarketOrder(put_contract.Symbol, 1)
# Record positions
for contract, leg_type in [(call_contract, 'call'), (put_contract, 'put')]:
mid = (contract.BidPrice + contract.AskPrice)/2
self.currentPositions[contract.Symbol] = {
'type': leg_type,
'entry_price': mid
}
self.lastTradeDate = self.time.date()
self.Debug(f"[{self.time}] Opened ATM strangle at {underlying_price:.2f}: "
f"Call={call_contract.Symbol} (Δ={call_contract.Greeks.Delta:.2f}), "
f"Put={put_contract.Symbol} (Δ={put_contract.Greeks.Delta:.2f})")
def SelectBestContract(self, contracts, target_delta, is_call):
best = None; best_diff = float('inf')
for c in contracts:
d = c.Greeks.Delta
diff = abs(d-target_delta) if is_call else abs(abs(d)-target_delta)
if diff < best_diff:
best_diff, best = diff, c
return best
def OnData(self, slice: Slice):
# Only proceed if we have open positions and open price recorded
if not self.currentPositions or self.dayOpenPrice is None:
return
# Underlying current price
bar = slice.Bars.get(self.symbol)
if bar is None:
return
current_price = bar.Close
# Check for reaching directional daily range limits
move_pct = (current_price - self.dayOpenPrice) / self.dayOpenPrice
# If price moved up at least +0.5%, close the call leg
if move_pct >= self.limit_up:
to_close = [s for s, info in self.currentPositions.items() if info['type'] == 'call']
for sym in to_close:
if self.Portfolio[sym].Invested:
self.Liquidate(sym)
self.Debug(f"Price up {move_pct:.2%}: closed call leg {sym}")
del self.currentPositions[sym]
return
# If price moved down at least -0.5%, close the put leg
if move_pct <= self.limit_down:
to_close = [s for s, info in self.currentPositions.items() if info['type'] == 'put']
for sym in to_close:
if self.Portfolio[sym].Invested:
self.Liquidate(sym)
self.Debug(f"Price down {move_pct:.2%}: closed put leg {sym}")
del self.currentPositions[sym]
return
# Check stop-loss on remaining legs
for sym, info in list(self.currentPositions.items()):
holding = self.Portfolio[sym]
if not holding.Invested:
del self.currentPositions[sym]
continue
pnl_pct = (holding.HoldingsValue - holding.HoldingsCost) / abs(holding.HoldingsCost)
if pnl_pct <= self.stop_pct:
self.Liquidate(sym)
self.Debug(f"Stopped out {info['type']} leg {sym} at {pnl_pct:.1%} loss")
del self.currentPositions[sym]
def CloseAllPositions(self):
total_pnl = 0
for sym in list(self.currentPositions):
if self.Portfolio[sym].Invested:
# Get the exit price (midpoint of bid/ask if available, otherwise use last price)
contract = self.Securities[sym]
if contract.AskPrice > 0 and contract.BidPrice > 0:
exit_price = (contract.BidPrice + contract.AskPrice) / 2
else:
exit_price = contract.Price
entry_price = self.currentPositions[sym]['entry_price']
quantity = self.Portfolio[sym].Quantity
# Option contracts typically have a multiplier of 100
leg_pnl = (exit_price - entry_price) * quantity * 100
total_pnl += leg_pnl
self.Debug(f"Closing {self.currentPositions[sym]['type']} {sym} | Entry: {entry_price:.2f}, Exit: {exit_price:.2f}, PnL: ${leg_pnl:.2f}")
self.liquidate(sym)
if total_pnl != 0:
self.Log(f"End-of-day realized PnL: ${total_pnl:.2f}")
self.currentPositions.clear()
self.Log("Closed all positions at market close")
def OnOrderEvent(self, orderEvent):
if orderEvent.Status == OrderStatus.Filled:
order = self.Transactions.GetOrderById(orderEvent.OrderId)
self.Debug(f"Order filled: {order.Symbol} {order.Quantity} @ {orderEvent.FillPrice:.2f}")