QuantConnect / QuantConnect/Lean
OptionFilterUniverse Expiration is confusing and inefficient
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 21.7k
- Forks
- 5.3k
- Avg merge
- 2d 22h
- Merged PRs (30d)
- 34
Description
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
masterbranch - 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.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by running research.ipynb with the Foundation-Py-Default kernel and reviewing OptionFilterUniverse.Expiration through the linked ContractSecurityFilterUniverse documentation. Compare the observed DTE and strike results with the intended OptionHistory behavior. Done means the expiration semantics are agreed and either corrected or clearly documented, with the reproduction reflecting the expected bounds.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, jupyter-notebook, python
- Domain
- backend, data, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100