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.

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 Return9.4%
VolatilityN/A
Beta0.56
Sharpe RatioN/A
Sortino Ratio-0.174
Maximum DrawdownN/A
Win Rate49%

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

Leave a Reply

Discover more from Quant Buffet

Subscribe now to keep reading and get access to the full archive.

Continue reading