book
Checkout our new book! Hands on AI Trading with Python, QuantConnect, and AWS Learn More arrow

Asset Classes

Index Options

Introduction

This page explains how to get historical data for Index Options. Some of the data you can get include prices, open interest, Greeks, and universe selection data.

Trades

To get historical trade data, call the history method with the TradeBar type and a security's Symbol. This method returns a DataFrame with columns for the open, high, low, close, and volume.

Select Language:
class IndexOptionTradeBarHistoryAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 12, 19)
        # Get the Symbol of a security.
        index = self.add_index('SPX')
        symbol = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1].symbol
        # Get the 5 trailing daily TradeBar objects of the security in DataFrame format. 
        history = self.history(TradeBar, symbol, 5, Resolution.DAILY)
closehighlowopenvolume
expirystriketypesymboltime
2024-12-204000.01SPX 32NKZCP89T8EM|SPX 312024-12-12 15:15:000.050.070.050.075773.0
2024-12-13 15:15:000.050.100.050.053052.0
2024-12-16 15:15:000.030.100.030.106423.0
2024-12-17 15:15:000.030.050.030.056085.0
2024-12-18 15:15:000.851.000.030.0310551.0
# Calculate the daily returns.
daily_returns = history.close.pct_change().iloc[1:]
expiry      strike  type  symbol                    time               
2024-12-20  4000.0  1     SPX 32NKZCP89T8EM|SPX 31  2024-12-13 15:15:00     0.000000
                                                    2024-12-16 15:15:00    -0.400000
                                                    2024-12-17 15:15:00     0.000000
                                                    2024-12-18 15:15:00    27.333333

If you intend to use the data in the DataFrame to create TradeBar objects, request that the history request returns the data type you need. Otherwise, LEAN consumes unnecessary computational resources populating the DataFrame. To get a list of TradeBar objects instead of a DataFrame, call the history[TradeBar] method.

# Get the 5 trailing daily TradeBar objects of the security in TradeBar format. 
history = self.history[TradeBar](symbol, 5, Resolution.DAILY)
# Iterate through the TradeBar objects and access their volumes.
for trade_bar in history:
    t = trade_bar.end_time
    volume = trade_bar.volume

Request minute, hour, or daily resolution data. Otherwise, the history request won't return any data.

Quotes

To get historical quote data, call the history method with the QuoteBar type and a security's Symbol. This method returns a DataFrame with columns for the open, high, low, close, and size of the bid and ask quotes. The columns that don't start with "bid" or "ask" are the mean of the quote prices on both sides of the market.

Select Language:
class IndexOptionQuoteBarHistoryAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 12, 19)
        # Get the Symbol of a security.
        index = self.add_index('SPX')
        symbol = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1].symbol
        # Get the 5 trailing minute QuoteBar objects of the security in DataFrame format. 
        history = self.history(QuoteBar, symbol, 5, Resolution.MINUTE)
askcloseaskhighasklowaskopenasksizebidclosebidhighbidlowbidopenbidsizeclosehighlowopen
expirystriketypesymboltime
2024-12-204000.01SPX 32NKZCP89T8EM|SPX 312024-12-18 15:11:001.459.81.151.1511.00.900.900.30.9087.01.1755.3500.7251.025
2024-12-18 15:12:001.409.61.401.4510.00.850.950.70.9063.01.1255.2751.0501.175
2024-12-18 15:13:001.404.91.401.40122.00.800.900.30.85104.01.1002.9000.8501.125
2024-12-18 15:14:001.254.91.251.4059.00.600.800.30.8095.00.9252.8500.7751.100
2024-12-18 15:15:001.054.91.001.2548.00.600.850.30.6010.00.8252.8750.6500.925
# Calculate the spread at each minute.
spread = history.askclose - history.bidclose
expiry      strike  type  symbol                    time               
2024-12-20  4000.0  1     SPX 32NKZCP89T8EM|SPX 31  2024-12-18 15:11:00    0.55
                                                    2024-12-18 15:12:00    0.55
                                                    2024-12-18 15:13:00    0.60
                                                    2024-12-18 15:14:00    0.65
                                                    2024-12-18 15:15:00    0.45
dtype: float64

If you intend to use the data in the DataFrame to create QuoteBar objects, request that the history request returns the data type you need. Otherwise, LEAN consumes unnecessary computational resources populating the DataFrame. To get a list of QuoteBar objects instead of a DataFrame, call the history[QuoteBar] method.

# Get the 5 trailing minute QuoteBar objects of the security in QuoteBar format. 
history = self.history[QuoteBar](symbol, 5, Resolution.MINUTE)
# Iterate through each QuoteBar and calculate the dollar volume on the bid.
for quote_bar in history:
    t = quote_bar.end_time
    bid_dollar_volume = quote_bar.last_bid_size * quote_bar.bid.close

Request minute, hour, or daily resolution data. Otherwise, the history request won't return any data.

Slices

To get historical Slice data, call the history method without passing any Symbol objects. This method returns Slice objects, which contain data points from all the datasets in your algorithm. If you omit the resolution argument, it uses the resolution that you set for each security and dataset when you created the subscriptions.

Select Language:
class SliceHistoryAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 12, 1)
        # Add some securities and datasets.
        index = self.add_index('SPX')
        contract = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1]
        self.add_option_contract(contract.symbol)
        # Get the historical Slice objects over the last 5 days for all the subcriptions in your algorithm.
        history = self.history(5, Resolution.DAILY)
        # Iterate through each historical Slice.
        for slice_ in history:
            # Iterate through each TradeBar in this Slice.
            for symbol, trade_bar in slice_.bars.items():
                close = trade_bar.close

Open Interest

To get historical open interest data, call the history method with the OpenInterest type and a security's Symbol. This method returns a DataFrame with a single column.

Select Language:
class IndexOptionOpenInterestHistoryAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 12, 19)
        # Get the Symbol of a security.
        index = self.add_index('SPX')
        symbol = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1].symbol
        # Get the 5 trailing daily OpenInterest objects of the security in DataFrame format. 
        history = self.history(OpenInterest, symbol, 5, Resolution.DAILY)
openinterest
expirystriketypesymboltime
2024-12-204000.01SPX 32NKZCP89T8EM|SPX 312024-12-12 23:00:00306249.0
2024-12-15 23:00:00305821.0
2024-12-16 23:00:00301048.0
2024-12-17 23:00:00299501.0
2024-12-18 23:00:00294504.0
# Calculate the daily change in open interest.
oi_delta = history.openinterest.diff().iloc[1:]
expiry      strike  type  symbol                    time               
2024-12-20  4000.0  1     SPX 32NKZCP89T8EM|SPX 31  2024-12-15 23:00:00    -428.0
                                                    2024-12-16 23:00:00   -4773.0
                                                    2024-12-17 23:00:00   -1547.0
                                                    2024-12-18 23:00:00   -4997.0
Name: openinterest, dtype: float64

If you intend to use the data in the DataFrame to create OpenInterest objects, request that the history request returns the data type you need. Otherwise, LEAN consumes unnecessary computational resources populating the DataFrame. To get a list of OpenInterest objects instead of a DataFrame, call the history[OpenInterest] method.

# Get the 5 trailing daily OpenInterest objects of the security in OpenInterest format. 
history = self.history[OpenInterest](symbol, 5, Resolution.DAILY)
# Iterate through the TradeBar objects and access their volumes.
for oi in history:
    t = oi.end_time
    open_interest = oi.value

Daily Option Chains

To get historical daily Option chain data, call the history method with the Option Symbol object. The data this method returns contains information on all the currently tradable contracts, not just the contracts that pass your filter. If you pass flatten=True, this method returns a DataFrame with columns for the data point attributes.

Select Language:
class IndexOptionDailyOptionChainHistoryAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 12, 23)
        # Add an Index Option universe.
        option = self.add_index_option('SPX')
        # Get the trailing 5 daily Option chains in DataFrame format.
        history = self.history(option.symbol, 5, flatten=True)
closedeltagammahighimpliedvolatilitylowopenopeninterestrhothetaunderlyingvaluevegavolume
timesymbol
2024-12-12SPX YOGZ798QKLRI|SPX 315879.601.0000000.0000005897.450.0000005865.005866.455280.00.051183-0.030094SPX: ¤6,084.205879.600.0000009.0
SPX YP8JPVHGPQ9A|SPX 315878.951.0000000.0000005898.450.0000005864.305866.5568.00.203532-0.029968SPX: ¤6,084.205878.950.0000000.0
SPX YQ70D5ADE4VI|SPX 315876.701.0000000.0000005892.700.0000005862.205863.7042.00.392162-0.029810SPX: ¤6,084.205876.700.0000000.0
SPX YQYKVRJ3J9DA|SPX 315873.701.0000000.0000005889.750.0000005856.855860.5035.00.541633-0.029685SPX: ¤6,084.205873.700.0000000.0
SPX YRP5YAENLMPA|SPX 315873.150.9999700.0000005888.451.6518615857.205859.351.00.680416-0.032476SPX: ¤6,084.205873.150.0045160.0
................................................
2024-12-18SPX 32RLQ2G18QHCE|SPX 314740.75-0.9994350.0000024751.200.2687454727.554738.450.0-44.1007681.613144SPX: ¤6,050.314740.750.0771200.0
SPX 32XJE2QF1AJ7Y|SPX 315445.85-0.6754180.0000795466.350.7533695424.205445.0528.0-101.357524-0.721391SPX: ¤6,050.315445.8521.8345200.0
SPX 337HSSRKH55N2|SPX 315001.30-0.9976080.0000065012.400.1391144990.204999.303.0-214.9148711.611553SPX: ¤6,050.315001.300.6383360.0
SPX 33HG7ISPWZS26|SPX 314583.80-0.9922530.0000174595.600.1189434574.204582.0010.0-303.8603651.514502SPX: ¤6,050.314583.802.2327160.0
SPX 33REM8TVCUEHA|SPX 314193.20-0.9755110.0000434205.900.1117974185.604191.908.0-379.4868921.404206SPX: ¤6,050.314193.206.9476940.0
# Select the 2 contracts with the greatest volume each day.
most_traded = history.groupby('time').apply(lambda x: x.nlargest(2, 'volume')).reset_index(level=1, drop=True).volume
time        symbol                  
2024-12-12  SPX YOGZ79IHUCOE|SPX 31     20200.0
            SPX 32NKZCPHP4626|SPX 31    18417.0
2024-12-13  SPX YOGZ79IHUCOE|SPX 31      7109.0
            SPX 32OCJVBQ3CMGE|SPX 31     5930.0
2024-12-14  SPX YOGZ79IHUCOE|SPX 31     17443.0
            SPX 32NKZCRZNW3RI|SPX 31    16458.0
2024-12-17  SPX YOGZ7C245MVI|SPX 31     32796.0
            SPX 32NKZCS1BFG9A|SPX 31    30285.0
2024-12-18  SPX YOGZ79IHUCOE|SPX 31     14183.0
            SPX YOGZ7C245MVI|SPX 31     13335.0
Name: volume, dtype: float64

To get the data in the format of OptionUniverse objects instead of a DataFrame, call the history[OptionUniverse] method.

# Get the historical OptionUniverse data over the last 30 days.
history = self.history[OptionUniverse](option.symbol, timedelta(30))
# Iterate through each daily Option chain.
for option_universe in history:
    t = option_universe.end_time
    # Select the contract with the most volume.
    most_traded = sorted(option_universe, key=lambda contract: contract.volume)[-1]

Indicators

To get historical indicator values, call the indicator_history method with an indicator and the security's Symbol.

Select Language:
class IndexOptionIndicatorHistoryAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 12, 19)
        # Get the Symbol of the Option contract.
        underlying = self.add_index('SPX').symbol
        symbol = sorted(self.option_chain(underlying), key=lambda c: c.open_interest)[-1].symbol
        # Get the 21-day SMA values of the contract for the last 5 trading days. 
        history = self.indicator_history(SimpleMovingAverage(21), symbol, 5, Resolution.DAILY)

To organize the data into a DataFrame, use the data_frame property of the result.

# Organize the historical indicator data into a DataFrame to enable pandas wrangling.
history_df = history.data_frame
currentrollingsum
2024-12-12 15:15:000.61785712.975
2024-12-13 15:15:000.58095212.200
2024-12-16 15:15:000.53571411.250
2024-12-17 15:15:000.47738110.025
2024-12-18 15:15:000.4726199.925
# Get the maximum of the SMA values.
sma_max = history_df.current.max()

The indicator_history method resets your indicator, makes a history request, and updates the indicator with the historical data. Just like with regular history requests, the indicator_history method supports time periods based on a trailing number of bars, a trailing period of time, or a defined period of time. If you don't provide a resolution argument, it defaults to match the resolution of the security subscription.

To make the indicator_history method update the indicator with an alternative price field instead of the close (or mid-price) of each bar, pass a selector argument.

Select Language:
# Get the historical values of an indicator over the last 30 days, applying the indicator to the security's volume.
history = self.indicator_history(indicator, symbol, timedelta(30), selector=Field.VOLUME)

Some indicators require the prices of multiple securities to compute their value (for example, the indicators for the Greeks and implied volatility). In this case, pass a list of the Symbol objects to the method.

Select Language:
class IndexOptionMultiAssetIndicatorHistoryAlgorithm(QCAlgorithm):

    def initialize(self) -> None:
        self.set_start_date(2024, 12, 19)
        # Get the Symbol of the underlying asset.
        underlying = self.add_index('SPX').symbol
        # Get the Option contract Symbol.
        option = sorted(self.option_chain(underlying), key=lambda c: c.open_interest)[-1].symbol
        # Get the Symbol of the mirror contract.
        mirror = Symbol.create_option(
            underlying, option.id.market, option.id.option_style, 
            OptionRight.Call if option.id.option_right == OptionRight.PUT else OptionRight.PUT,
            option.id.strike_price, option.id.date
        )
        # Create the indicator.
        indicator = ImpliedVolatility(
            option, self.risk_free_interest_rate_model, DividendYieldProvider(underlying),
            mirror, OptionPricingModelType.FORWARD_TREE
        )
        # Get the historical values of the indicator over the last 60 trading minutes.
        history = self.indicator_history(indicator, [underlying, option, mirror], 60, Resolution.MINUTE)
        # Get the average IV value.
        iv_avg = history.data_frame.current.mean()

Examples

The following examples demonstrate some common practices for trading Index Options with historical data.

Example 1: Standard-Weekly Contracts Cointegration

The following example analyzes the cointegration relationship between the front-month ATM SPX and SPXW calls. By measuring their spread divergence, we trade mean reversal on their spread convergence.

Select Language:
from sklearn.linear_model import LinearRegression

class IndexOptionHistoricalDataAlgorithm(QCAlgorithm):
    _threshold = 2
    _coef = 0
    _intercept = 0
    _mean_spread = 0
    _sd_spread = 1
    _spx_contract = None
    _spxw_contract = None

    def initialize(self) -> None:
        self.set_start_date(2025, 1, 1)
        self.set_end_date(2025, 2, 1)
        self.set_cash(10000000)

        # Select the Index Options to analyze and trade by week.
        self.schedule.on(self.date_rules.week_start(), self.time_rules.at(9, 15), self.select_contracts)

    def select_contracts(self) -> None:
        index = Symbol.create("SPX", SecurityType.INDEX, Market.USA)
        # Obtain the SPX ATM call contract since it is the most liquid to trade with.
        spx = Symbol.create_canonical_option(index)
        spx_contracts = [x for x in self.option_chain(spx) if x.expiry < self.time + timedelta(30)]
        if not spx_contracts:
            self._spx_contract = None
            self._spxw_contract = None
            return
        expiry = max(x.expiry for x in spx_contracts)
        self._spx_contract = sorted([x for x in spx_contracts if x.expiry == expiry and x.right == OptionRight.CALL], key=lambda x: abs(x.strike - x.underlying_last_price))[0]
        # Obtain the SPXW contract with the same strike, right, and expiry.
        spxw = Symbol.create_canonical_option(index, "SPXW", Market.USA, "?SPXW")
        spxw_contracts = self.option_chain(spxw)
        strike = self._spx_contract.strike
        self._spxw_contract = next(filter(lambda x: x.expiry == expiry and x.right == OptionRight.CALL and x.strike == strike, spxw_contracts), None)

        if self._spxw_contract:
            # Subscribe to the contracts we will trade 
            self.add_index_option_contract(self._spx_contract.symbol)
            self.add_index_option_contract(self._spxw_contract.symbol)
            # Obtain the historical data and find their cointegration relationship.
            history = self.history([self._spx_contract.symbol, self._spxw_contract.symbol], 1000, Resolution.MINUTE).droplevel([0, 1, 2]).unstack(0).close
            lr = LinearRegression().fit(np.log(history.iloc[:, [0]]), np.log(history.iloc[:, 1]))
            self._coef, self._intercept = lr.coef_, lr.intercept_
            # Obtain the mean and SD of the spread between the options.
            residual = history.apply(lambda x: self.get_spread(x[self._spx_contract.symbol], x[self._spxw_contract.symbol]), axis=1)
            self._mean_spread, self._sd_spread = np.mean(residual.values), np.std(residual.values, ddof=1)

    def on_data(self, slice: Slice) -> None:
        if self._spxw_contract:
            spx = slice.quote_bars.get(self._spx_contract.symbol)
            spxw = slice.quote_bars.get(self._spxw_contract.symbol)
            if spx and spxw:
                # Obtain the current spread to see if there is any price divergence to trade.
                spread = self.get_spread(spx.close, spxw.close)
                z = (spread - self._mean_spread) / self._sd_spread

                # If the spread diverges above or below the threshold, trade to bet on mean reversal.
                if z >= self._threshold and not self.portfolio[spx.symbol].is_long:
                    self.market_order(spx.symbol, int(10 * self._coef))
                    self.market_order(spxw.symbol, -10)
                elif z <= self._threshold and not self.portfolio[spxw.symbol].is_long:
                    self.market_order(spx.symbol, int(-10 * self._coef))
                    self.market_order(spxw.symbol, 10)

                # If prices converge, exit positions.
                if (z <= 0 and self.portfolio[spx.symbol].is_long) or (z >= 0 and self.portfolio[spxw.symbol].is_long):
                    self.liquidate()

    def get_spread(self, x: float, y: float) -> float:
        return np.log(y) - self._intercept - self._coef[0] * np.log(x)

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: