Unhanded Division by Zero in DefaultOptionAssignmentModel.IsDeepInTheMoney
Expected Behavior
No unhandled division by zero.
Actual Behavior
System.Exception: Attempted to divide by zero. in DefaultOptionAssignmentModel.cs:line 76 ---> QuantConnect.Exceptions.SystemExceptionInterpreter+SanitizedException: Attempted to divide by zero. at System.Decimal.DecCalc.VarDecDiv(DecCalc& d1, DecCalc& d2) at QuantConnect.Securities.Option.DefaultOptionAssignmentModel.IsDeepInTheMoney(Option option) in Common/Securities/Option/DefaultOptionAssignmentModel.cs:line 76
Potential Solution
We could check the division by zero in DefaultOptionAssignmentModel.IsDeepInTheMoney. However, the core problem is that the algorithm could place an option order while the underlying price was zero.
Reproducing the Problem
Ask @AlexCatarino
Checklist
- [x] I have completely filled out this template
- [x] I have confirmed that this issue exists on the current
masterbranch - [x] I have confirmed that this is not a duplicate issue by searching issues
- [x] I have provided detailed steps to reproduce the issue
Closing for now until we have an algorithm which can reproduce the issue
@Martin-Molinero I have an algo that can reproduce this reliably if you're interested in looking into this.
Hey @alexgbernier! Yes please, will allow us to solve this ASAP 🙏. You can share the algorithm here or with QC support, the simpler the reproducing algorithm the better 👍
Closing for now until we have a reproduceable case or deployment logs reproducing to debug further
I have emailed [email protected] with a repro algorithm on this one. Let me know if there is a better way to share directly.
MIA reproduced this bug with:
from AlgorithmImports import *
class UtilityStraddleAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2021, 1, 1)
self.set_end_date(2022, 1, 1)
self.set_cash(100000)
self._atr_7 = {}
self._atr_30 = {}
self._option_symbols = {}
# Limit Universe resolution to Daily
self.universe_settings.resolution = Resolution.DAILY
# Select top 500 by dollar volume in coarse, then filter by sector in fine
self.add_universe(self.coarse_selection_function, self.fine_selection_function)
def coarse_selection_function(self, coarse):
# Filter to those with fundamental data and sort by dollar volume
filtered = [x for x in coarse if x.has_fundamental_data]
filtered.sort(key=lambda x: x.dollar_volume, reverse=True)
return [x.symbol for x in filtered[:500]]
def fine_selection_function(self, fine):
# Select only utilities sector (MorningstarSectorCode = 55), then pick top 10
utilities = [x.symbol for x in fine if x.asset_classification.MorningstarSectorCode == MorningstarSectorCode.UTILITIES]
return utilities[:10]
def on_securities_changed(self, changes: SecurityChanges) -> None:
for security in changes.added_securities:
# Create ATR indicators if not present
if security.symbol not in self._atr_7:
self._atr_7[security.symbol] = AverageTrueRange("ATR7", 7, MovingAverageType.SIMPLE)
self._atr_30[security.symbol] = AverageTrueRange("ATR30", 30, MovingAverageType.SIMPLE)
# Warm up indicators
history = self.history([security.symbol], 30, Resolution.DAILY)
if not history.empty and security.symbol in history.index.get_level_values(0):
symbol_data = history.loc[security.symbol]
for time_index, row in symbol_data.iterrows():
bar_time = time_index
if hasattr(time_index, 'to_pydatetime'):
bar_time = time_index.to_pydatetime()
else:
bar_time = pd.to_datetime(time_index).to_pydatetime()
bar_open = float(row["open"])
bar_high = float(row["high"])
bar_low = float(row["low"])
bar_close = float(row["close"])
bar_volume = float(row["volume"])
trade_bar = TradeBar(bar_time, security.symbol, bar_open, bar_high,
bar_low, bar_close, bar_volume)
self._atr_7[security.symbol].update(trade_bar)
self._atr_30[security.symbol].update(trade_bar)
# Add option chain if not already added
if security.symbol not in self._option_symbols:
ticker = security.symbol.id.symbol
option = self.add_option(ticker, Resolution.DAILY)
option.set_filter(lambda x: x.expiration(0, 60))
self._option_symbols[security.symbol] = option.symbol
for security in changes.removed_securities:
if security.symbol in self._atr_7:
del self._atr_7[security.symbol]
if security.symbol in self._atr_30:
del self._atr_30[security.symbol]
if security.symbol in self._option_symbols:
self.liquidate(self._option_symbols[security.symbol])
del self._option_symbols[security.symbol]
def on_data(self, data: Slice) -> None:
# Update ATR indicators
for symbol, bar in data.bars.items():
if symbol in self._atr_7:
self._atr_7[symbol].update(bar)
self._atr_30[symbol].update(bar)
# Manage short straddles for each underlying
for underlying_symbol, option_symbol in self._option_symbols.items():
if option_symbol not in data.option_chains:
continue
chain = data.option_chains[option_symbol]
if not chain:
continue
if (underlying_symbol in self._atr_7 and underlying_symbol in self._atr_30
and self._atr_7[underlying_symbol].is_ready and self._atr_30[underlying_symbol].is_ready):
atr_7 = self._atr_7[underlying_symbol].current.value
atr_30 = self._atr_30[underlying_symbol].current.value
# If 7-day ATR is less than 30-day ATR, sell straddle
if atr_7 < atr_30:
contracts = sorted(chain, key=lambda c: c.expiry)
if len(contracts) == 0:
continue
# Pick nearest expiry
expiry = contracts[0].expiry
underlying_price = self.securities[underlying_symbol].price
calls = [x for x in contracts if x.expiry == expiry and x.right == OptionRight.CALL]
puts = [x for x in contracts if x.expiry == expiry and x.right == OptionRight.PUT]
if not calls or not puts:
continue
call_contract = min(calls, key=lambda c: abs(c.strike - underlying_price))
put_contract = min(puts, key=lambda c: abs(c.strike - underlying_price))
if (self.portfolio[call_contract.symbol].quantity == 0 and
self.portfolio[put_contract.symbol].quantity == 0):
self.market_order(call_contract.symbol, -1)
self.market_order(put_contract.symbol, -1)
else:
# If condition no longer holds, liquidate
self.liquidate(option_symbol)
Although we did find a bug in BKI's map files, the general issue here is using SecurityIdentifier.Symbol as the ticker to add the option (ticker = security.symbol.id.symbol and self.add_option(ticker, Resolution.DAILY)). It should either pass the underlying symbol (self.add_option(security.symbol, Resolution.DAILY)) or today's ticker (self.add_option(security.symbol.value, Resolution.DAILY)).
Also, that for where options are added need to filter securities and only select equities.