Mispricing of Equity Options With Different Time To Maturity
Log in to collectAcademic paper
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.
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.
Backtest performance
Annualised return9.4%
Beta0.56
Sortino ratio-0.174
Win rate49%
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