
The strategy involves delta-hedged call options on US equities, selecting options with moneyness between 0.7 and 1.3. Positions are taken monthly, rebalanced, and held until options mature, equally weighted.
ASSET CLASS: options | REGION: United States | FREQUENCY:
Monthly | MARKET: equities | KEYWORD: Mispricing, Equity, Time, Maturity
I. STRATEGY IN A NUTSHELL
The strategy trades US equity options expiring on the third Friday of each month, excluding illiquid options. It uses a delta-hedged call approach, taking short positions in options maturing in four weeks, with moneyness between 0.7 and 1.3. The portfolio is equally weighted, rebalanced monthly, and held until maturity, maintaining delta neutrality.
II. ECONOMIC RATIONALE
Investors often overlook simple information like exact expiration dates, despite its availability. This inattention, combined with focus on current-month expirations, creates behavioral biases that the strategy exploits, producing stronger effects during the options’ expiration month.
III. SOURCE PAPER
Inattention in the Options Market [Click to Open PDF]
Eisdorfer, University of Connecticut; Sadka, Boston College; Zhdanov, Penn State University
<Abstract>
Options on US equities typically expire on the third Friday of each month, which means that either four or five weeks elapse between two consecutive expiration dates. We find that options that are held from one expiration date to the next achieve significantly lower weekly adjusted returns when there are four weeks between expiration dates. We argue that this mispricing is due to investor inattention and provide further supporting evidence based on earnings announcement dates and price patterns closer to maturity. The results remain strongly significant controlling for a large set of option and stock characteristics, and are robust to various subsamples and estimation procedures. Our findings have potentially important implications for calibrating option pricing models as well as for extracting information from option prices to forecast future variables.


IV. BACKTEST PERFORMANCE
| Annualised Return | 9.4% |
| Volatility | N/A |
| Beta | 0.56 |
| Sharpe Ratio | N/A |
| Sortino Ratio | -0.174 |
| Maximum Drawdown | N/A |
| Win Rate | 49% |
V. FULL PYTHON CODE
from AlgorithmImports import *
#endregion
class MispricingofxOptionsWithDifferentTimeToMaturity(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(1000000)
self.min_share_price:int = 5
self.min_expiry:int = 18
self.max_expiry:int = 22
self.percentage_traded:float = 1
self.selected_symbols:list[Symbol] = {}
self.subscribed_contracts:dict[Symbol, Contracts] = {}
self.weeks_counter:int = 0
self.rebalance_period:int = 3
self.weekday_num:int = 3 # represents thursday
self.market_symbol:Symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
self.recent_day:int = -1
self.recent_month:int = -1
self.fundamental_sorting_key = lambda x: x.DollarVolume
self.fundamental_count:int = 100
self.selection_flag:bool = False
self.settings.daily_precise_end_time = False
self.Settings.MinimumOrderMarginPortfolioPercentage = 0.
self.UniverseSettings.Resolution = Resolution.Minute
self.AddUniverse(self.FundamentalSelectionFunction)
self.SetSecurityInitializer(lambda x: x.SetDataNormalizationMode(DataNormalizationMode.Raw))
self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw
def FundamentalSelectionFunction(self, fundamental: List[Fundamental]) -> List[Symbol]:
# rebalance, when contracts expiried
if not self.selection_flag:
return Universe.Unchanged
selected:list = [
x for x in fundamental if x.HasFundamentalData and x.Market == 'usa' and x.Price >= self.min_share_price
]
if len(selected) > self.fundamental_count:
selected = [x for x in sorted(selected, key=self.fundamental_sorting_key, reverse=True)[:self.fundamental_count]]
self.selected_symbols = list(map(lambda stock: stock.Symbol, selected))
return self.selected_symbols
def OnData(self, data: Slice):
curr_date:datetime.date = self.Time.date()
# execute once a day
if self.recent_day != curr_date.day:
self.recent_day = curr_date.day
if self.recent_month != curr_date.month:
self.recent_month = curr_date.month
self.weeks_counter = 0
# check if any of the subscribed contracts expired
for symbol in self.selected_symbols:
if symbol in self.subscribed_contracts and self.subscribed_contracts[symbol].expiry_date <= self.Time.date():
# remove expired contracts
for contract in self.subscribed_contracts[symbol].contracts:
if self.Securities[contract].IsTradable:
# self.RemoveSecurity(contract)
self.Liquidate(contract)
del self.subscribed_contracts[symbol]
if curr_date.weekday() == self.weekday_num:
# increase week counter at the specific day of the week
self.weeks_counter += 1
# allow rebalance on the third thursday of the month,
# because stocks and contracts will be subscribed on the third friday of the month
if self.weeks_counter % self.rebalance_period == 0:
self.subscribed_contracts.clear()
self.selected_symbols.clear()
self.selection_flag = True
return
# subscribe to new contracts after selection
if len(self.subscribed_contracts) == 0 and self.selection_flag:
for symbol in self.selected_symbols:
# get all contracts for current stock symbol
contracts:list[Symbol] = self.OptionChainProvider.GetOptionContractList(symbol, self.Time)
underlying_price:float = self.Securities[symbol].Price
strikes:list = [i.ID.StrikePrice for i in contracts]
# can't filter contracts, if there isn't any strike price
if len(strikes) <= 0 or underlying_price == 0:
continue
call:Symbol|None = self.FilterContracts(strikes, contracts, underlying_price)
if call:
subscriptions:list = self.SubscriptionManager.SubscriptionDataConfigService.GetSubscriptionDataConfigs(call.Underlying)
if subscriptions:
self.AddContract(call)
expiry_date:datetime.date = call.ID.Date.date()
self.subscribed_contracts[symbol] = Contracts(expiry_date, underlying_price, [call])
# this triggers next minute after new contracts subscription
elif len(self.subscribed_contracts) != 0 and self.selection_flag:
self.selection_flag = False # this makes sure, there will be no other trades until next selection
# trade execution
self.Liquidate()
length:int = len(self.selected_symbols)
for symbol in self.selected_symbols:
if symbol in data and data[symbol]:
if symbol not in self.subscribed_contracts:
continue
call = self.subscribed_contracts[symbol].contracts[0]
underlying_price:float = self.subscribed_contracts[symbol].underlying_price
options_q:int = int(((self.Portfolio.TotalPortfolioValue * self.percentage_traded) / length) / (underlying_price * 100))
if call in data and data[call] != 0 and symbol in data and data[symbol]:
self.Sell(call, options_q)
# delta hedge
self.SetHoldings(symbol, (1 / length) * self.percentage_traded)
def FilterContracts(self, strikes:list, contracts:list, underlying_price:float):
''' filter call contracts from contracts parameter '''
''' return call contract '''
result = None
atm_strike:float = min(strikes, key=lambda x: abs(x-underlying_price))
# filtred contracts based on option rights and strikes
atm_calls:list[Symbol] = [i for i in contracts if i.ID.OptionRight == OptionRight.Call and
i.ID.StrikePrice == atm_strike and
self.min_expiry <= (i.ID.Date - self.Time).days <= self.max_expiry]
if len(atm_calls) > 0:
# sort by expiry
result = sorted(atm_calls, key = lambda item: item.ID.Date, reverse=True)[0]
return result
def AddContract(self, contract) -> None:
''' subscribe option contract, set price mondel and normalization mode '''
option = self.AddOptionContract(contract, Resolution.Minute)
option.PriceModel = OptionPriceModels.BlackScholes()
class Contracts():
def __init__(self, expiry_date, underlying_price, contracts):
self.expiry_date = expiry_date
self.underlying_price = underlying_price
self.contracts = contracts
VI. Backtest Performance