Lean icon indicating copy to clipboard operation
Lean copied to clipboard

OptionFilterUniverse Expiration is confusing and inefficient

Open pandiani42 opened this issue 5 months ago • 1 comments

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

  • [x] I have completely filled out this template
  • [x] I have confirmed that this issue exists on the current master branch
  • [x] I have confirmed that this is not a duplicate issue by searching issues
  • [x] 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.

pandiani42 avatar Aug 18 '25 12:08 pandiani42

Hey @pandiani42! Thank you for the report, you are correct. We currently resolve all option symbols for the request and later perform the history for all of them for the whole range. It's clear how this approach wont scale nicely for large option chains, like you mentions with SPX. Ideally the filter should be respected for the history request too 👍

Martin-Molinero avatar Aug 18 '25 13:08 Martin-Molinero