Overall Statistics
Total Orders
0
Average Win
0%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0%
Expectancy
0
Start Equity
100000
End Equity
100000
Net Profit
0%
Sharpe Ratio
0
Sortino Ratio
0
Probabilistic Sharpe Ratio
0%
Loss Rate
0%
Win Rate
0%
Profit-Loss Ratio
0
Alpha
0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
-0.786
Tracking Error
0.139
Treynor Ratio
0
Total Fees
$0.00
Estimated Strategy Capacity
$0
Lowest Capacity Asset
Portfolio Turnover
0%
Drawdown Recovery
0
# region imports
from datetime import date, timedelta

from AlgorithmImports import *

from spxw_option_series import SPXWOptionSeries
# endregion

_DATE_FORMAT = '%A, %B %d, %Y'


class SPXWOptionChainDataIssue(QCAlgorithm):
    def initialize(self) -> None:
        self.set_start_date(2012, 1, 1)

        self._spx = self.add_index(
            ticker='SPX',
            resolution=Resolution.DAILY,
            market=Market.USA,
            fill_forward=False
        )

        self._spxw_option = self.add_index_option(
            symbol=self._spx.symbol,
            target_option='SPXW',
            resolution=Resolution.DAILY,
            fill_forward=False
        )
        self._spxw_option.set_filter(
            universe_func=lambda universe: universe.include_weeklys().expiration(
                min_expiry_days=0,
                max_expiry_days=0
            )
        )

    def on_data(self, slice: Slice) -> None:
        has_spx_bar = self._spx.symbol in slice.bars
        option_chain = slice.option_chains.get(self._spxw_option.symbol)

        if not has_spx_bar and option_chain is None:
            # This slice only contains delisting events, so skip it.
            return

        current_date = self._spxw_option.local_time.date()

        spxw_option_series = SPXWOptionSeries.determine_option_series(
            date_to_evaluate=current_date,
            exchange_hours=self._spxw_option.exchange.hours
        )
        should_have_chain = spxw_option_series is not None and current_date >= spxw_option_series.first_ever_expiry_date

        if option_chain is None and should_have_chain:
            self.error(f"SPXW `{spxw_option_series}` option chain missing on `{current_date.isoformat()}` ({current_date.strftime(_DATE_FORMAT)}).")
        elif option_chain is not None and not should_have_chain:
            self.error(f"Extraneous SPXW option chain found on `{current_date.isoformat()}` ({current_date.strftime(_DATE_FORMAT)}).")

        if should_have_chain and not has_spx_bar:
            self.error(f"SPX bar missing on `{current_date.isoformat()}` ({current_date.strftime(_DATE_FORMAT)}).")
# region imports
from datetime import date, timedelta
from enum import StrEnum, auto
from typing import Optional, Self

from AlgorithmImports import *
# endregion

class SPXWOptionSeries(StrEnum):
    STANDARD_MONTHLY = auto()

    END_OF_WEEK = auto()
    END_OF_MONTH = auto()
    END_OF_QUARTER = auto()

    MONDAY_WEEKLY = auto()
    TUESDAY_WEEKLY = auto()
    WEDNESDAY_WEEKLY = auto()
    THURSDAY_WEEKLY = auto()

    @classmethod
    def determine_option_series(
        cls,
        date_to_evaluate: date,
        exchange_hours: SecurityExchangeHours
    ) -> Optional[Self]:
        if not exchange_hours.is_date_open(
            local_date_time=date_to_evaluate
        ):
            return None

        is_market_open_on_following_day = exchange_hours.is_date_open(
            local_date_time=date_to_evaluate + timedelta(days=1)
        )

        if _is_standard_monthly_options_expiry_day(
            date_to_evaluate=date_to_evaluate,
            is_market_open_on_following_day=is_market_open_on_following_day
        ):
            return cls.STANDARD_MONTHLY

        is_end_of_month = _is_end_of_month(
            date_to_evaluate=date_to_evaluate,
            exchange_hours=exchange_hours
        )
        if is_end_of_month and date_to_evaluate.month in [3, 6, 9, 12]:
            return cls.END_OF_QUARTER

        is_market_open_on_preceding_day = exchange_hours.is_date_open(
            local_date_time=date_to_evaluate - timedelta(days=1)
        )

        weekly_option_series = cls._determine_weekly_option_series(
            date_to_evaluate=date_to_evaluate,
            is_market_open_on_following_day=is_market_open_on_following_day,
            is_market_open_on_preceding_day=is_market_open_on_preceding_day
        )

        if is_end_of_month and weekly_option_series != cls.END_OF_WEEK:
            return cls.END_OF_MONTH

        return weekly_option_series

    @classmethod
    def _determine_weekly_option_series(
        cls,
        date_to_evaluate: date,
        is_market_open_on_following_day: bool,
        is_market_open_on_preceding_day: bool
    ) -> Self:
        weekday = date_to_evaluate.weekday()
        assert weekday < 5, f"The exchange should have been closed on `{date_to_evaluate.isoformat()}`, which was a weekend."

        # Note: The below checks are ordered according to the series introduction dates.
        if weekday == 2 or (weekday == 1 and not is_market_open_on_following_day):
            return cls.WEDNESDAY_WEEKLY
        elif weekday == 0 or (weekday == 1 and not is_market_open_on_preceding_day):
            return cls.MONDAY_WEEKLY
        elif weekday == 1 or (weekday == 0 and not is_market_open_on_following_day):
            return cls.TUESDAY_WEEKLY
        elif (weekday == 3 and is_market_open_on_following_day) or (weekday == 2 and not is_market_open_on_following_day):
            return cls.THURSDAY_WEEKLY
        else:
            return cls.END_OF_WEEK

    @property
    def first_ever_expiry_date(self) -> date:
        if self == self.STANDARD_MONTHLY:
            # https://cdn.cboe.com/resources/regulation/circulars/regulatory/RG17-054.pdf
            return date(2017, 5, 19)
        elif self == self.END_OF_WEEK:
            # https://cdn.cboe.com/resources/regulation/circulars/general/IC-CBOE-2005-138.pdf
            return date(2005, 11, 4)
        elif self == self.END_OF_MONTH:
            # https://cdn.cboe.com/resources/regulation/circulars/general/IC-CBOE-2014-055.pdf
            return date(2014, 8, 31)
        elif self == self.END_OF_QUARTER:
            # https://cdn.cboe.com/resources/regulation/circulars/regulatory/RG14-081.pdf
            return date(2014, 9, 30)
        elif self == self.MONDAY_WEEKLY:
            # https://ir.cboe.com/news/news-details/2016/CBOE-to-List-SPX-Monday-Expiring-Weeklys-Options-07-11-2016/default.aspx
            return date(2016, 8, 22)
        elif self == self.TUESDAY_WEEKLY:
            # https://ir.cboe.com/news/news-details/2022/Cboe-to-Add-Tuesday-and-Thursday-Expirations-for-SPX-Weeklys-Options-04-13-2022/default.aspx
            return date(2022, 4, 26)
        elif self == self.WEDNESDAY_WEEKLY:
            # https://ir.cboe.com/news/news-details/2016/CBOE-to-List-SPX-Wednesday-Expiring-Weeklys-Options-02-01-2016/default.aspx
            return date(2016, 3, 2)
        elif self == self.THURSDAY_WEEKLY:
            # https://ir.cboe.com/news/news-details/2022/Cboe-to-Add-Tuesday-and-Thursday-Expirations-for-SPX-Weeklys-Options-04-13-2022/default.aspx
            return date(2022, 5, 19)
        else:
            raise AssertionError(f"Unhandled `SPXWOptionSeries` enum case: `{self}`.")


def _is_standard_monthly_options_expiry_day(
    date_to_evaluate: date,
    is_market_open_on_following_day: bool
) -> bool:
    first_friday = _first_target_weekday_of_month(
        year=date_to_evaluate.year,
        month=date_to_evaluate.month,
        target_weekday=4  # Friday
    )
    third_friday = first_friday + timedelta(weeks=2)

    return not is_market_open_on_following_day and (
        date_to_evaluate == third_friday or date_to_evaluate + timedelta(days=1) == third_friday
    )


def _first_target_weekday_of_month(
    year: int,
    month: int,
    target_weekday: int
) -> date:
    assert 0 <= target_weekday <= 6

    first_date_of_month = date(year, month, 1)

    first_target_weekday_offset = (target_weekday - first_date_of_month.weekday() + 7) % 7
    return first_date_of_month + timedelta(days=first_target_weekday_offset)


def _is_end_of_month(
    date_to_evaluate: date,
    exchange_hours: SecurityExchangeHours
) -> bool:
    next_trading_day = exchange_hours.get_next_trading_day(
        date=date_to_evaluate
    ).date()
    
    return next_trading_day.month != date_to_evaluate.month