Overview

This article explores how short iron condor strategies can be backtested. The important part of this article is the reconstruction of expired options, which used to be a challenge reported many times in the Developer community Q&A forum. The challenge is that one cannot directly access expired options through a single API call. To get historical data on options, one will need to reconstruct options Refinitiv Identification Codes (RIC) following the logic of RIC construction rules and the rules specified by the exchange where the option is traded. Further, in this article, RIC reconstruction and validation functions are presented; they can be used for options on OPRA exchange-traded indices and stocks. Functions reconstruct and validate AM settlement options expiring on the 3rd Friday of each month.

In this article, we test and visualize the profit/loss of the short Iron Condor strategy with 10% and 20% Out of Money (OTM) sizes for NDX; however one can use the functions and codes of this article to backtest other OPRA exchange-traded indices (such as SPX) and equities (such as IBM, TWTR) at the same time specifying OTM sizes other than 10/20%. It should be noted that options with different underlying assets have different liquidity, and one may not get data for specific OTM ranges for some underlying assets. The current backtesting model is not at the production level and is not tested on a wide range of underlying assets. Except for NDX, we tested the functions on SPX, IBM, TWTR and AAPL, which produced the required results to be used for backtesting of options on these assets. Additionally, we considered also the impact of stock split event on the underlying price and strike price, and adjusted the prices accordingly. The impact of other corporate events, such as stock dividends, right offering, M&A is not considered and tested in the scope of this article.

The article follows the following structure. In Section 1, we build and present a function for expired option RIC reconstruction and validation. Section 2 introduces functions to conduct option transactions to build iron condor legs and report the strategy outcome. Also, we use a function to offset the transactions before expiration based on VIX movements. Finally, Section 3 implements a short iron condor strategy on NDX and visualizes the backtesting results.

Install and import packages

To start, we first install and import the necessary packages. We use the Refinitiv Data platform API to retrieve the data. To do that, we need to authorize ourselves with an app key. The code are built using Python 3.9. Other prerequisite packages are installed below:

Section 1: Reconstruct expired option RICs

Below, we illustrate the Refinitiv Identification Code (RIC) structure of an expired option on the NASDAQ 100 index (NDX) as an underlying. Options on other indices and equities follow the same structure. The first three letters indicate the name of the asset, which can be a maximum of 5 for other assets according to ‘RULES7’. The following letter represents the Expiration month code, which is different for put and call options and can be found in ‘RULES2’. Additionally, the letter indicating expiration month is lower case for any strikes > 999.999. The following two digits represent the Expiration day, which is the 3rd Friday of every month for AM settled monthly options. This is followed by another two digits indicating the Expiration year. Subsequently, we have five characters for the Strike price with different integral and decimal part combinations depending on the value of the strike price (more can be found in ‘RULES7’). “.” followed by up to 2 characters is the Exchange code, OPRA in the examined case. “^” symbol indicates that the option is expired, which is followed by the Expiration month code and the Expiration year. Based on the following considerations, the first RIC from the below image is the RIC for NDX call option with a strike price of 12100, which was traded at OPRA Exchange and expired on November 20, 2020. The other RIC is a put option with a strike price of 10425.

Below can be found the comprehensive list of Rules used to reconstruct expired option RICs on indices and equities.

RULES7 | RIC rules to construct options on equities and indices (search in Workspace).

RULES2 | RIC Rules for Delivery Month Codes for Futures & Options (search in Workspace).

RULES3 and RULES4 | RIC Rules for Exchange Identifiers (search in Workspace).

Guideline to pull expired options in Refinitiv Eikon | can be accessed at My Refinitiv.

Guideline on RIC structure Enhancement for Strike Price 10000 and above | can be accessed at My Refinitiv.

Equity Options Product Specifications | CBOE exchange rules to be accessed here.

SPX Options Product Specification | CBOE exchange rules to be accessed here.

NDX options Product Specification | NASDAQ Factsheet to be accessed here.

In order to reconstruct RICs, we need to get all the required components of the RIC structure presented above. Most of the components are inputted directly to the function get_potential_rics introduced further in this article, whereas we have constructed separate functions to get Expiration days and Transaction days (we need this for the strike price component) as we will need to call those functions several times throughout the process.

Additionally, we check for stock split corporate events for equities. Those are the one of the most common corporate event types, and will greatly impact on the underlying price and therefore on the strike price component of the RIC. In case there is a stock split event impacting on the prices we adjust the historical prices by an adjustment factor to make sure we arrive at a valid strike and reconstruct a valid RIC.

1.1 Function for Expiration days

First, we define a function to get expiration dates for each month. Since AM settled monthly options expire on the 3rd Friday of each month, we define a function that gets that after having a year as an input. It should be noted that the function also considers exchange holidays, and if a day is a holiday, the previous day is considered as suggested by the exchange rules.

For the current and for all the following functions, code explanations are made in code cell by using code comments.

Below we see results for 2020:

{2020: [17, 21, 20, 17, 15, 19, 17, 21, 18, 16, 20, 18]}

1.2 Function for Transaction days

Next, we define a function returning a transaction day for each of 12 months which is used when a. Requesting underlying asset prices, based on which we calculate strike prices while also considering the specified Out of Money (OTM) size, and b. Conducting monthly option transaction.

We provide two possible days for conducting option transactions which are controlled by the function parameter: 1. First business day of each month, 2. Third Friday (which matches with general expiration cycles) of each month. We always open options contract positions to be expiring on the following month of the purchase. For example, for January 2021, we open an option contract expiring on February 2021 either on the first business day, which is January 4, or on the third Friday of the month, which is January 15.

Below we run the function to showcase the output for 2020 with a parameter value of ‘first’.

Below we see results for 2020:
[2, 3, 2, 1, 1, 1, 1, 3, 1, 1, 2, 1]

1.3 Function to get adjustment factor of stock split

Here, we check for stock split corporate events for equities and retrieve the adjustment factor. Further, the adjustment factor is used to adjust underlying prices and strike price of option contracts. The function takes asset RIC, either year or an exact date of the request as input and returns the adjustment factor(s). If no stock split event happened after the expiration date of an option, the function returns adjustment factor(s) of 1, which, in fact, doesn’t change the price values. It should be noted that if more than one stock split happened after the requested date, we need to take into consideration the all adjustments combined.

In case of year is used as an input, trans_day argument needs to be passed as well. Based on those arguments the function will first calculate request dates for each month and then return the list of adjustment factors for each month. In contrast, if an exact date is inputted, there is no need for trans_day argument and the function will return the adjustment factor for that date only. This is done to ensure the flexibility of the usage of the function.

1.4 Function to get potential RICs

As mentioned earlier, most of the RIC components are constructed via a separate function introduced below. This function calls the functions mentioned above and uses the input to construct other components of RIC. The function takes the year, transaction day, asset name, OTM size, tolerated difference in OTM size, and option type as an input and returns a dictionary of potential RICs per month. For each month, it produces several RICs, which depend on the parameter of tolerated difference in OTM size. We use tolerated difference in OTM because it is mostly impossible to arrive at a valid strike price with any specified OTM.

The general workflow on how the function works is illustrated in the image below:

The function works for both indices and equities. The exact RIC codes (e.g., “.NDX” or ‘MSFT.O’) for each asset needs to be given to the function. Further, the function trims the necessary part of the asset RIC for option RIC construction. The rest of the details on constructing each component of an option RIC is explained inside the code as comments.

After the code is fully executed, we report the logs in a text file.

Below we run the function to showcase the output for call options for 2020 with 10% OTM and 0.5% tolerance.

{1: ['NDXa172090900.U^A20',...,'NDXa172091900.U^A20'], ..., 12: ['NDXl1820A2130.U^L20', ..., 'NDXl1820A2250.U^L20']}

The function get_potential_rics lists the RICs for each month with strike prices ranging from lower to upper strike bounds. In order to make sure we are trading the option with a strike price closest to the specified OTM price, we need to sort the RICs of each month in order of closeness of options contract strike price with the OTM price. The function below takes potential RICs and strike prices (calculated based on OTM) as an input and returns the sorted dictionary of RICs.

Below we run the function to showcase the sorted output.

{1: ['NDXa172091400.U^A20', ... , 'NDXa172091900.U^A20'], ..., 12: ['NDXl1820A2195.U^L20', ...,  'NDXl1820A2130.U^L20']}

1.5 Function to validate potential RICs

As the name of the get_potential_rics function indicates, it produces a list of potential RICs, part of which (sometimes all, if you request for higher OTMs for illiquid option contract) is not an actual RIC. In order to validate those, we need to make API calls. Here we use the get_historical_price_summaries function from the RDP API, which results in “None” if the RIC is invalid; otherwise, it returns the values from the specified fields. One may use get_date function from Eikon API; however, it returns an error if the RIC is invalid, and one may need to write try/except loop and hide the errors/warnings.

It should be noted that the function mentioned below should be used only if one wants to validate all RICs from the list of potential RICs. As a rule, in options strategy backtesting, when one finds a valid RIC for a month, one does not have to test others from that month as the option contract with the closest OTM size is already found for trading. In our example, to test the short iron condor strategy, we do not call this function; instead, we check the validity of a RIC right inside the transaction function (see function trans_option) to avoid extensive API requests and hitting the daily limit of 10000 requests.

Below we run the function to showcase the valid RICs.

{1: ['NDXa172091400.U^A20',..., 'NDXa172091900.U^A20'], ..., 12: ['NDXl1820A2190.U^L20', ..., 'NDXl1820A2130.U^L20']}

Section 2: Option transactions and strategy outcome

After we have the list of potential RICs for each month, we trade them by taking long or short positions for a contract every month of a given year. The trans_option function takes the year, transaction day (“first” or “third”), asset name, potential sorted RICs, option type and position (“short” or “long”) and returns a dataframe consisting of transaction details.

Here should be additionally stated that the function works by the potential RICs, and one does not have to run the function get_valid_rics. This function takes the first RIC from the month (already sorted having the ones we want at the beginning) and checks the validity by requesting Trade, BID, and ASK prices through RDP API function get_historical_price_summaries. If valid, the prices are considered for the trade; if not, the next RIC is tried. This continues until a valid RIC with a valid option contract price is found for a month. If no valid RIC is found for a month, no option contract is traded for that month, which is reported in the Log file accordingly. As it comes to the valid option contract price, it is considered to be one of the following (in order as provided below):

a. Trade price

b. BID/ASK mid price

c. ASK price minus average bid/ask spread for the period

d. BID price plus average bid/ask spread for the period

e. ASK price

f. BID price

In order to get more values for a more robust estimate of the option contract price, we request price data from 2 months before the transaction date. The transaction date is either the first business day or the 3rd Friday of every month, which needs to be specified as a function parameter. Options expire in the expiration cycle of the following month of the purchase.

The rest of the details on building a dataframe of transaction details is explained inside the code as comments.

After the code is fully executed, we report the logs in a text file.

2.1 Function to create option positions

Below we run the function to showcase the resulting dataframe of the transaction output. Explanations follow the dataframe table.

png

As we can see above, the resulting dataframe consists of the following transaction details:

  • RIC — Option RIC with a strike price closest to the strike calculated based on OTM size (e.g. for call option: strike = price + price * OTM / 100)
  • Trans_Date — Transaction date, which is either the first business day or the expiration day of a month based on a parameter trans_day
  • Exp_Date — Expiration day of an option contract, which is the 3rd Friday of each month for OPRA exchange-traded monthly options with AM settlement
  • Option Type — Call or Put as specified in the respective parameter of the function
  • Position — Long or Short as specified in the respective parameter of the function
  • Strike — Strike price of the traded option contract. This is retrieved based on the underlying asset price value and following RULES7
  • Price_trans_Date — Price of the underlying asset at the transaction date
  • Option Price — Price of the option contract retrieved/calculated based on the considerations from the cell above
  • OTM size — The actual size of OTM, calculated based on the underlying asset price and the Strike of the traded option contract

As option strategies are mostly done by pairs we created a separate pair_trans function which conducts paired transactions by calling get_potential_rics, sort_option, and trans_option functions for call and put options with the specified parameter values. This function takes the year, trans_day, asset name, OTM sizes, and positions for call and put options and returns transaction details for both call and put option trades.

Below we run the function to showcase the resulting dataframe of the transaction outputs.

png

2.2 Function to offset positions

One will not open positions in option trading strategies and wait until expiration to calculate the strategy outcome. Most of the time, traders use offset models, which create triggers for closing the open positions and calculate the outcome as of the position close day. Traders use sophisticated models for determining offset triggers; however, this article aims not to suggest the best model for that but rather showcase how Refinitiv APIs can be used for option strategy backtesting, which would support both opening and closing of the positions. Thus, in this article, we base our offset strategy on CBOE Volatility Index (VIX).

Particularly, we first create a threshold equal to 2 standard deviations of the previous 30 days VIX change. Then we calculate the 3-day moving average (MA) of VIX and track MA change after the transaction date. If the MA exceeds the threshold at any point after the position is open, we create an offset transaction to short RICs with long positions and long ones with short positions. Usage of MA, instead of actual VIX change, makes price movements smoother, allowing to avoid False Alarms, and threshold based on previous period’s standard deviations allows to adapt to the changing market conditions.

The exit_trans function takes the call and put option transactions as an input, and returns a dataframe containing the offset transactions. To conduct offset transactions, the function first requests VIX data from RDP API function get_historical_price_summaries for each transaction date of inputted option transactions. The request period is from 30 days before the transaction to the expiration date of the respective option contract. Then based on the VIX trade price, the function calculates VIX change and the MA (including the change). Finally, the threshold for that month is calculated. Then, the function loops over all days following the position’s open date. If the MA change for a day exceeds the threshold, offset transaction date is fixed, and historical price summaries are requested for respective option contracts, both put and call. The considerations described for option contract price retrieval/calculations are applied for the offset transactions as well. After all transaction details are known, those are appended to the transaction dictionary. The rest of the details on building a dataframe of transaction details is explained inside the code as comments.

After the code is fully executed, we report the logs in a text file.

Below we define a function to calculate simple moving average, which is used inside the exit_trans function.

png

2.3 Function to calculate and report the outcome of the strategy

After we have all transactions, we calculate the transaction’s outcome by calculating and summing up profit/loss over all traded options contracts. For that, we define function trans_outcome, which takes option transactions, asset name, and VIX consideration as an input and returns the same option transactions dataframe by adding the outcome details, including contract price, exercise outcome, and total profit/loss per options transaction. We do not need to run exit_trans before running this function; instead, we decide through the parameter cons_vix consider either offsetting or not.

Before running the calculation part, the function first checks if the respective pair exists in the transaction list. If it doesn’t, we remove the unpaired option transaction and report it in the log file. The rest of the details on calculating the transaction outcome are explained inside the code as comments and in the cells following the running of the code.

After the code is fully executed, we report the logs in a text file.

Below we run the function to showcase the resulting dataframe of transaction outcomes. Explanation follows the dataframe table.

png

In addition to the columns from the original transaction dataframe the following outcome details are added:

  • Close_date — transaction close date, which is expiration day for transactions that are not offsite, and offset day for transactions that are closed by offset transactions
  • Close_date_prices — underlying asset prices as of the close day. We use this to calculate exercise outcome
  • Contract Price — Contract price, which equals to option price multiplied by 100, which is the option contract multiplier
  • Exercise outcome — shows the profit/loss from exercised options and equals to 0 if the option contract is expired worthless
  • Total Profit/Loss — shows aggregated profit/loss from an option transaction and equals to sum of the option contract and exercise outcome
  • Outcome — textual representation of whether the option is exercised or expired worthless

Below we show the aggregated profit during each month of 2020 grouped by the expiration date.

png

Here, it should be noted that this is the outcome for a paired transaction only and not for an option strategy. In the next section, we implement a short iron condor strategy and visualize the backtesting outcome during 2015–2021.

Section 3: Implement and visualize short iron condor strategy outcome

This section implements the short iron condor strategy with 10%/20% legs for options on the NDX index. The short iron condor strategy aims to profit from low volatility. The maximum profit is equal to the net credit received from option contract prices, and this profit is realized if the stock price is equal to or between the strike prices of the short options at expiration. In this outcome, all options expire worthless, and the net credit is kept as income. The maximum loss is equal to the difference between the strike prices of the long put spread (or short call spread) less the net credit received. More about the short iron condor strategy can be found here. It should be mentioned that in the scope of the current article, transactions fees are not taken into consideration and are equal to zero. The actual profit and loss are calculated based on contract price differences and exercise outcomes.

3.1 Implement short iron condor strategy

Here we visualize results from the iron condor strategy with 10/20% OTM size for NDX. There are other datasets in the GitHub folder which are retrieved by the codes above. The below-mentioned codes could be easily adjusted to visualize that as well.

Before moving to the visualizations, first, we define and call a function that takes option transactions, checks if four legs of the short iron condor strategy exist, removes unpaired transactions, and returns complete pairs of transactions.

After the code is fully executed, we report the logs in a text file.

We run the function to showcase the resulting dataframe of complete transactions.

png

3.2 Visualize the outcome of the strategy

After we have the complete short iron condor transaction legs, we first look at the number of open and offsite positions in each year of the observation period.

png

As can be observed from the graph above, there have been missing transactions for several months throughout the years. This is because of the lack of liquidity in higher OTMs. For example, if we look at the short iron condor example with 5%/10% legs, we will observe many more complete transactions (you can try this by reading the excel file “short_IC5_10ndx1521.xlsx”. Additionally, we can notice from the graph that three transactions are offsite during the observation period on average.

After we have the complete short iron condor transactions, we visualize the outcome through multiple graphs. First, we look at the total and cumulative sum of profits during the observation period.

png

The graph above illustrates that the short iron condor strategy on NDX with 10%/20% OTM legs was primarily positive in terms of the cumulative sum of profits. There were three big plunges in the cumulative sum of the profits. First, was caused by the stock market tank in December 2018 caused by the interest rate hikes by Federal Reserve System, which resulted in increased volatility. Volatility spikes caused the following two profit drops in May 2020 and December 2020 because of the impact of COVID on stock markets. After the two latter crashes, the cumulative sum of profits became negative, which regained during the following periods when markets became more stable.

Further, we look at the total profits and the ones attributed to Contract price and Exercise outcome components annually.

png

Here we experience a similar picture to the one from the graph above regarding the total profits. Particularly, we observe negative total profits for 2018 and 2020, and the profit is reaching its maximum in 2021. As expected, we can see that the profits are generated from contract price differences, whereas Exercised options resulted in losses. This is because one benefits from the short iron condor strategy when the price is between strike prices of short positions and options become worthless at expiration.

Next, we also look at the number of exercised and expired worthless contracts along with total profits.

png

We can see from the graph above that most of the options expired worthless. Only 1–3 options contracts were exercised, resulting in losses presented in the previous graph. Particularly, three exercised options from 2020 resulted in losses of $128,818.

Further, we deep dive and look at the individual components of iron condor strategy, such as profits from call options versus put options, short positions versus long positions, and profits from 4 different legs of the strategy. First, we create the dataframes as shown in the cell below and then plot the results.

png

Looking at the graphs one by one, we can observe that, as expected, expired options resulted in most of the profits and exercised one’s losses. Moving to the profits from call versus put options, we can claim that put options mainly were profitable, and call options resulted in losses with an exception from 2018. Additionally, short transactions when we received option premium resulted in profits and long positions to lose, which is expected since most contracts expired worthless. Finally, short put options contributed most to the strategy’s returns, whereas long put and short call legs mostly resulted in losses. This is because the market mainly moved upwards during the observation period.

Finally, we plot the returns from the strategy per month to see whether there is a tendency for the short iron condor strategy to be profitable or losable during certain months regularly.

png

Summary

This article walked through the backtesting of a short iron condor strategy with 10%/20% legs for option contracts written on NDX as an underlying. The estimates are done from 2015 to 2021 October included. The most important and, at the same time, challenging part of the backtesting was the reconstruction of expired option RICs as those are not easily accessible through a single API call. We illustrated the construction process and built functions to reconstruct and validate RICs for OPRA exchange-traded indices and equities monthly options. It is worth mentioning that the reconstruction part can be used as a standalone product for other use cases different than strategy backtesting.

Further, moving to the actual backtesting, we built functions to create transactions for short iron condor strategy legs and offset those with opposite transactions in case of a market trigger. As a market trigger, we used the three-day MA change. Finally, we calculated the strategy outcome and visualized the results with different graphs. Although all the processes above are showcased for NDX, we run the backtesting for SPX, IBM, TWTR and AAPL as well. The datasets of options transactions during 2015–2021 for those assets are included in the GitHub folder.

It is also worth mentioning the limitations of the current backtesting tool. First of all, the expired option RIC reconstruction is not easily scalable. One may not learn and incorporate exchange and Refinitiv rules for other types of assets, such as FX, and other expiration periods, such as weekly. Next, the solution involves many API requests and depending on the option and number of years to request, one may hit the daily limit. Thus, it is advised to request data for relatively short periods and export the retrieved output to excel. Then the outputs can be merged, and visualizations run for the entire period of observation. Finally, at larger OTMs and for certain assets, the market may not be very liquid, and one may end up having no options contracts to trade for some months.

--

--