OptionFilterUniverse Expiration is confusing and inefficient
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
masterbranch - [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.
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 👍