Quant BuffetRelax, Not Over Thinking

Mispricing of Equity Options With Different Time To Maturity

Log in to collect

Academic 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