Lean icon indicating copy to clipboard operation
Lean copied to clipboard

Unhanded Division by Zero in DefaultOptionAssignmentModel.IsDeepInTheMoney

Open AlexCatarino opened this issue 2 years ago • 7 comments

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 master branch
  • [x] I have confirmed that this is not a duplicate issue by searching issues
  • [x] I have provided detailed steps to reproduce the issue

AlexCatarino avatar Jan 16 '24 21:01 AlexCatarino

Closing for now until we have an algorithm which can reproduce the issue

Martin-Molinero avatar Feb 05 '24 13:02 Martin-Molinero

@Martin-Molinero I have an algo that can reproduce this reliably if you're interested in looking into this.

alexgbernier avatar Jan 07 '25 05:01 alexgbernier

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 👍

Martin-Molinero avatar Jan 07 '25 12:01 Martin-Molinero

Closing for now until we have a reproduceable case or deployment logs reproducing to debug further

Martin-Molinero avatar Jan 14 '25 00:01 Martin-Molinero

I have emailed [email protected] with a repro algorithm on this one. Let me know if there is a better way to share directly.

jaybennett89 avatar Mar 15 '25 21:03 jaybennett89

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)

Martin-Molinero avatar Apr 30 '25 13:04 Martin-Molinero

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.

jhonabreul avatar May 19 '25 14:05 jhonabreul