
The strategy sells liquid S&P 500 put options on expiration Fridays, excluding those with low volume, holds equally-weighted positions over the weekend, and closes them the following Monday.
ASSET CLASS: options | REGION: United States | FREQUENCY:
Daily | MARKET: equities | KEYWORD: Trading, Options, Expiration Weekends
I. STRATEGY IN A NUTSHELL
The strategy involves selling liquid S&P 500 Index put options with at least 100 contracts traded over the past five days. Positions are opened every expiration Friday (third Friday of the month) and closed after the weekend (Monday or next trading day).
II. ECONOMIC RATIONALE
The negative weekend returns of options may arise from higher nontrading risk, heightened aversion to unlimited downside, and persistent market mispricing. Traders often overlook nontrading periods, causing inconsistent time decay and exploitable inefficiencies.
III. SOURCE PAPER
Option Mispricing Around Nontrading Periods [Click to Open PDF]
Jones, Shemesh
<Abstract>
We find that option returns are significantly lower over nontrading periods, the vast majority of which are weekends. Our evidence suggests that nontrading returns cannot be explained by risk, but are rather the result of widespread and highly persistent option mispricing driven by the incorrect treatment of non-smoothness in stock return variance. The size of the effect implies that the broad spectrum of finance research involving option prices should account for nontrading effects and non-smoothness in variance more generally. Our study further suggests how alternative industry practices could improve the effciency of option markets in a meaningful way.

IV. BACKTEST PERFORMANCE
| Annualised Return | 21.26% |
| Volatility | N/A |
| Beta | 0.008 |
| Sharpe Ratio | N/A |
| Sortino Ratio | -0.505 |
| Maximum Drawdown | N/A |
| Win Rate | 55% |
V. FULL PYTHON CODE
from AlgorithmImports import *
class TradingOptionsDuringExpirationWeekends(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2011, 1, 1)
self.SetCash(100000)
self.symbol = self.AddEquity("SPY", Resolution.Minute).Symbol
# Next expiry date.
self.expiry_date = None
option = self.AddOption("SPY", Resolution.Minute)
option.SetFilter(-20, 20, 25, 35)
self.Schedule.On(self.DateRules.EveryDay(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol, 1), self.Close)
def OnData(self, slice):
# Open new trades only on market close.
if not (self.Time.hour == 15 and self.Time.minute == 59):
return
if self.expiry_date:
if self.Time.date() < self.expiry_date.date():
return
for i in slice.OptionChains:
chains = i.Value
if not self.Portfolio.Invested:
puts = list(filter(lambda x: x.Right == OptionRight.Put, chains))
if not puts: return
underlying_price = self.Securities[self.symbol].Price
expiries = [i.Expiry for i in puts]
# Determine expiration date nearly one month.
expiry = min(expiries, key=lambda x: abs((x.date()-self.Time.date()).days-30))
strikes = [i.Strike for i in puts]
# determine at-the-money strike
strike = min(strikes, key=lambda x: abs(x-underlying_price))
atm_put = [i for i in puts if i.Expiry == expiry and i.Strike == strike]
if atm_put:
if not self.expiry_date:
self.expiry_date = atm_put[0].Expiry
return
options_q = int(self.Portfolio.MarginRemaining / (underlying_price * 100))
if not (self.Time.month == 8 and self.Time.year == 2015):
self.Sell(atm_put[0].Symbol, options_q)
self.expiry_date = atm_put[0].Expiry
if self.Portfolio.Invested:
self.Liquidate(self.symbol)
def Close(self):
if self.Portfolio.Invested:
self.Liquidate()
VI. Backtest Performance