Skip to content

OptionFilterUniverse Expiration is confusing and inefficient #8932

@pandiani42

Description

@pandiani42

Expected Behavior

When filtering options with OptionFilterUniverse.Expiration() the documentation says "minExpiry: The minimum time until expiry to include, for example, TimeSpan.FromDays(10) would exclude contracts expiring in less than 10 days".

Actual Behavior

Looking at the example below, when using OptionHistory, the expiration filter shows every option with an expiry between start + minExpiry and end + maxExpiry. The "days until expiration" in the usual meaning for options are not restricted at all. Even if this behaviour is intended, it is confusing. Also, the filter is not efficient given the huge amount of data for option chains with daily expiries, e.g. SPXW. With the current behaviour, the number of expiries for SPXW over a time period of n trading days is quadratic in n.

Potential Solution

The behaviour of the OptionFilterUniverse expiration filter could be changed or extended with a parameter such that the 'dte' column in the example code is between minExpiry and maxExpiry. At the very least, the documentation should be clarified.

Reproducing the Problem

research.ipynb with 'Foundation-Py-Default' Kernel in masterv17255.

import datetime as dt
qb = QuantBook()
spx_symbol = qb.add_index('SPX', Resolution.DAILY).symbol
option = qb.add_index_option(spx_symbol, 'SPXW', Resolution.DAILY)

first_date = dt.date(2025, 8, 4) # monday
qb.set_start_date(first_date + dt.timedelta(days=10)) # somewhere after the last expected expiry
option.set_filter(lambda u: u.weeklys_only().calls_only().expiration(1, 2).strikes(-1, 1))
df = qb.option_history(option.symbol, first_date, first_date + dt.timedelta(days=3), Resolution.DAILY).data_frame.reset_index()

df['expiry'] = pd.to_datetime(df['expiry'])
df['date'] = df['time'].dt.normalize()
df['dte'] = df['expiry'] - df['date']
df.groupby(['date', 'expiry', 'dte'])['close'].count().rename('strike_count').reset_index()

Result: DTE is not between 1 and 2, but between 0 and 4.

 date     expiry        dte      strike_count
2025-08-04 2025-08-05 1 days             2
2025-08-04 2025-08-06 2 days             4
2025-08-04 2025-08-07 3 days             4
2025-08-04 2025-08-08 4 days             4
2025-08-05 2025-08-05 0 days             2
2025-08-05 2025-08-06 1 days             4
2025-08-05 2025-08-07 2 days             4
2025-08-05 2025-08-08 3 days             4
2025-08-06 2025-08-06 0 days             4
2025-08-06 2025-08-07 1 days             4
2025-08-06 2025-08-08 2 days             4

I will not write a second ticket and possibly the behaviour is intended, but i like to mention that the strikes filter also behaves weirdly in this example.

df = df.sort_values(['date', 'expiry', 'dte', 'strike'])
mask = df['dte'] == dt.timedelta(days=4)
df[mask][['date', 'expiry', 'dte', 'strike', 'type', 'symbol']]
--->
    date     expiry    dte  strike  type                    symbol
2025-08-04 2025-08-08 4 days  6295.0  Call  SPXW YUSFHNNH0WA6|SPX 31
2025-08-04 2025-08-08 4 days  6300.0  Call  SPXW YUSFGV0TFAUM|SPX 31
2025-08-04 2025-08-08 4 days  6345.0  Call  SPXW YUSFHNVQPMR2|SPX 31
2025-08-04 2025-08-08 4 days  6350.0  Call  SPXW YUSFGXNEWLXQ|SPX 31

Checklist

  • I have completely filled out this template
  • I have confirmed that this issue exists on the current master branch
  • I have confirmed that this is not a duplicate issue by searching issues
  • I have provided detailed steps to reproduce the issue

Probably, the issue has been discussed before, but i could not find anything and the example code is quite useful for further discussion in my opinion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions