| 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