My bitemoji Zach data enthusiast. options trader.

Backtesting ATM Hold the Strike Options Put Writing Strategy

» trading

Introduction

Writing Puts on SPY is a strategy as old as time. Just look at /r/thetagang on Reddit and one will find numerous posts extolling the virtues of the strategy. One modification to this strategy, however, that has yet to be tested is the “Hold the Strike” variant of this. This tweak was suggested in this SeekingAlpha article but it appeared that the strategy had only been backtested by that author on simulated data. I managed to get my hands on some free options data from June 2019 - May 2021 and hence decided to backtest this strategy. This backtest aims to compared the results of an ATM Put Writing Strategy (with rolling down) and an ATM Put Writing Strategy that Holds the Strike (ATM HTS). The concept of “Hold-the-Strike” essentially involves rolling out to the same strike, even if the Put is ITM. This allows you to collect extrinsic premium while waiting for the market to recover.

I am aware that 2019 - 2021 was a huge bull run (except for the not-so-little dip in 2020) but the point of this was to try to see how these strategies would have performed with the 2020 dip.

Mechanics

  • 1 ATM contract, sold every 7 days (weeklies)
  • No management, all run to 1 day before expiry and then, depending on strategy:
    • Rolled to the nearest ATM strike (ATM)
    • Either rolled up to the ATM strike or held at the ITM strike (ATM HTS)

Results

  Cumulative Return (%) Sharpe Sortino Max Drawdown (%) Avg Drawdown Days Volatility (% ann.)
Benchmark (SPY) 48.48 0.77 1.05 -33.72 11 21.31
ATM Put 49.56 0.59 0.71 -66.07 56 42.85
ATM Put (Hold Strike) 56.74 0.61 1.1 -85.61 58 91.47

Discussion

From the period of 2019 - 2021, we see that ATM Put Writing Strategies beat the market in terms of cumulative returns, with the HTS mechanic appearing to best the standard ATM strategy. As the author of the SeekingAlpha article explains, this is because you don’t give up intrinsic value when you roll down. However, two significant points of note are: (1) The drawdowns are significant. The drawdown for the ATM HTS strategy was 85%. Not many people will be able to withstand such a significant drawdown and hence a hedge (such as VXX calls) might be something to compliment it and reduce drawdown. (2) The bounce from the 2020 dip was rapid. This helped the ATM HTS strategy since there was a quick recovery to above the last held ATM strike.

Code Discussion

Consolidation of SPY Data

In order to backtest this, I first needed to consolidate SPY data from the option datasets that I had obtained. Options is really, really, really huge and hence I wanted to extract just the SPY data.

import glob
import pandas as pd
import tqdm

path = R"X:\Projects\SPY Data\Data\Consol\cmd_csv"
all_files = glob.iglob(os.path.join(path, "*.csv"))
li = []
col_names = ["ID", "Ticker","Expiration","Ask","Asize","Bid","Bsize","Last","Type","Strike","Vol","OpenInterest","Underlying","DataDate"]

for f in tqdm.notebook.tqdm(all_files):
    try:
        df = pd.read_csv(f, index_col=None, skiprows=1, names=col_names, parse_dates=["Expiration", "DataDate"])
        df = df[df["Ticker"]=="SPY"]
        li.append(df)
        print("Finished " + str(f))
    except:
        print("NO SPY NO SPY" + str(f))
        
# join all the csvs together once consolidated
frame = pd.concat(li, axis=0, ignore_index = True)

This allowed me to create a list of all the SPY data frames and once this was done, I simply used pd.concat() to join it all up.

Creating backtesting functions

I then created a number of backtesting functions which I won’t go into detail here. These functions have been included as part of the OpTiks Options Backtester that I’ve been working on and you can check that out here.

Essentially, I created a class for each trade and then assigned all the necessary trade attributes to them (e.g. strike price, value, expiration date), giving each a unique ID. Within the backtester function, I then cycled through the data day-by-day, scanning for each unique options ID and updating the values as I went. Again, more on this can be found at OpTiks link above.

Benchmarking results

def benchmark_results(bal, days, benchmark="SPY"):
    """
    Function to allow one to analyse the results of a
    backtest. Requires list of balances and dates from
    backtest results. 

    Default benchmark is SPY but this can be changed. 
    """
    returns_series = pd.DataFrame({"Returns": pd.Series(bal[0:len(days)]).pct_change(), "Date":days}).set_index("Date").squeeze()
    qs.reports.html(returns_series, benchmark, output="yourfilename.html", grayscale=False, title="ATM Put Writing (hold the strike)")

Finally, I needed to benchmark the results against SPY Buy-and-Hold. I hence used QuantStats, a very useful benchmarking library, to create the necessary tearsheets. I also needed to convert the data from raw values into returns and hence used the .squeeze() method to squeeze the dataframe into a series that qs could then read. This allowed me to produce the stats and graphs that showed the results.