Skip to content

Add strike, expiration and moneyness filters to option chains and universes - #9783

Merged
jhonabreul merged 14 commits into
QuantConnect:masterfrom
jhonabreul:feature-option-chain-pickers
Sep 11, 2026
Merged

Add strike, expiration and moneyness filters to option chains and universes#9783
jhonabreul merged 14 commits into
QuantConnect:masterfrom
jhonabreul:feature-option-chain-pickers

Conversation

@jhonabreul

@jhonabreul jhonabreul commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Builds on the chain filters from #9779, now merged. More filters, shared by the option universe selection (set_filter), the IOptionContractFilters interface and OptionChain, so the same words work in both places:

chain = self.option_chain(symbol)
puts = chain.puts_only().otm().expiring_after(self.time + timedelta(days=30))   # out of the money puts, 30+ days out
legs = chain.expiration([expiry]).strikes([745, 750])                           # exact expiration and strikes
wings = chain.zero_dte().strikes_above(spot + 5).strikes_below(spot + 20)       # today's expiry, a strike band
straddle = chain.farthest_expiration().atm()                                    # the strikes on either side of spot, atm(2.5) for a band in points
days = [contract.days_to_expiry for contract in chain.front_month()]
  • Sets: strikes(strikes) and expiration(dates) select exact values, any number of them. Time of day is ignored.
  • Bounds: strikes_above(price), strikes_below(price), expiring_after(date), expiring_before(date). The bound itself is excluded, so above plus below plus the exact set is the whole chain.
  • Moneyness: otm() / out_of_the_money(), itm() / in_the_money(), atm(max_strike_distance=None) / at_the_money(max_strike_distance=None). By default ATM is the strike on either side of the underlying price, the highest at or below it and the lowest at or above it, each only when within OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance of the price, a settable percentage defaulting to 2%: one or two strikes on every ladder, from 5-point SPXW steps to $0.50 on F, and none on a chain filtered away from the money. With a distance, every strike within it, in units of the underlying price; zero is the exact strike. Unlike strikes(0, 0), which takes the first strike at or above the price.
  • zero_dte(), the contracts expiring today, and farthest_expiration(), its opposite, built like front_month().
  • days_to_expiry on contracts, and OptionPayoff.IsInTheMoney, IsAtTheMoney and IsOutOfTheMoney next to the existing payoff helpers.
Design notes
  • The set overloads take a collection on purpose: strikes(100, 105) would bind to the relative strikes(min, max) in both languages, strikes([100, 105]) cannot. Python lists of numbers and dates convert.
  • The moneyness filters select nothing when the underlying price is unknown, like the strategy filters, and scale the price by the strike multiplier like strikes(min, max) does; an explicit ATM distance is scaled the same way.
  • A percentage range as the ATM default was rejected: 2% of SPX spans about 60 strikes on the 5-point SPXW ladder. The bracketing strikes adapt to any ladder, and the percentage only guards against a far strike counting when the price sits outside the strikes selected. DefaultAtTheMoneyStrikeDistance lives on the non-generic OptionFilterUniverse so the universe and the chain share one value; a static on the generic base would be one per closed type.
  • The expiration set and bound filters are live for futures universes too, since they sit in the contract base; the future option universes and chains get every option filter through OptionFilterUniverse and OptionChain.
  • The strategy filters' closest strike helper now breaks ties towards the lower strike explicitly; before, ties followed the input order.
  • FarthestExpiration walks the contracts once, keeping the latest expiration seen and the contracts at it.
  • The expiration bounds and FarthestExpiration live in the contract base, so futures universes get them too.
  • The chain wrappers stay in OptionChain.Filters.cs; each one forwards to the universe filter through OptionChainFilterUniverse, as in Share the option universe filters with OptionChain #9779.
Bug found and fixed along the way
  • DataDictionary's indexer setter cleared its cached sorted items but not its cached Keys and Values lists, so after chain.Contracts[symbol] = contract the Values collection was stale. Add() cleared all three. The setter now does the same; DataDictionaryTests covers it.
Notes for review
  • The history first adds single contract pickers (select, closest_expiry, strike_prices) and then removes them: the filters cover the same selections and return chains, so one grammar remains. The final diff has none of them.

Related Issue

N/A

Motivation and Context

Option algorithms keep re-deriving the same selections from a chain: the contracts at an exact strike or expiry, the ones above or below a price, the out of the money side, the nearest strike. Hand-rolled versions crash on empty sequences, miss the time of day on expiries and pick 0-DTE contracts by accident. One filter each, with the same name on the universe and the chain, removes that.

Requires Documentation Change

Yes: the new filters on OptionChain and the option universe, days_to_expiry and the OptionPayoff predicates.

How Has This Been Tested?

  • OptionChainTests: every new filter returns the same contracts as the universe filter over identical data, alone and chained, including empty results and the time of day on dates; the moneyness split over several underlying prices and ATM distances, including the default bracketing strikes with the price on a strike, between strikes, too far from one or both sides, and outside the ladder, a distance just short of the nearest strikes and one reaching several, and the configurable default percentage; nothing selected without an underlying price, on the chain and the universe; Python lists of strikes and dates through pythonnet; days_to_expiry against the contract dates; a future option chain built from universe rows, its hours, contract types, moneyness against the future price and parity with a future option universe.
  • FutureFilterTests: the expiration sets, bounds and FarthestExpiration on a futures universe.
  • OptionPayoffTests: the three predicates over calls and puts below, at and above the price.
  • DataDictionaryTests: the indexer setter refreshes the cached keys and values.
  • OptionChainFiltersRegressionAlgorithm (C# and Python): an out of the money GOOG universe, and sets, bounds, moneyness, zero_dte() and farthest_expiration() against hand-rolled expectations on option_chain() and on the slice chains.
  • IndexOptionChainFiltersRegressionAlgorithm (C# and Python): SPX and SPXW universes selected with the new filters, the exact contracts each filter returns on the SPX ladder from the universe data, the slice chains against their universe filters and the live index price, zero_dte() on the weekly expirations, and a trade picked with calls_only().expiring_after(time).front_month().
  • FutureUniverseFiltersRegressionAlgorithm (C# and Python): ES and GC universes selected with expiring_after and expiring_before, checked on futures_chain() and the slice chains.
  • FutureOptionChainFiltersRegressionAlgorithm (C# and Python): the March 2020 ES future selected by expiration([date]), its options by strikes(-3, 3).out_of_the_money(), the exact contracts each filter returns on option_chain(), and the slice chains against the universe filter and the live future price.
  • Every test and regression algorithm with option or future in its name at the moneyness commit, 3559 passed, including every option strategy regression algorithm for the tie-break change; the option filter fixtures and both chain regression algorithms at the last commit.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • Refactor (non-breaking change which improves implementation)
  • Performance (non-breaking change which improves performance. Please add associated performance test and results)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Non-functional change (xml comments/documentation/etc)

Checklist:

  • My code follows the code style of this project.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.
  • My branch follows the naming convention bug-<issue#>-<description> or feature-<issue#>-<description>

@jhonabreul
jhonabreul force-pushed the feature-option-chain-pickers branch 3 times, most recently from 0abb215 to 79bb448 Compare September 9, 2026 20:24
Single contract pickers and views on OptionChain, null-safe instead of raising:

- select() and its synonym pick(): best match by right, target/min/max days to
  expiration and one of moneyness, strike_from_atm or target_delta
- closest_expiry(), at(expiry), at_the_money(), calls, puts, expiries and
  strike_prices (StrikeList with closest_to, first_above, first_below)
- days_to_expiry on contracts, counted to the last trading date for options

Expirations and days to expiration follow the last trading date, so Saturday
expiring equity options before February 2015 match their Friday.
…exer set

Calls, Puts, StrikePrices and Expiries are computed once per contract count and
returned as read-only views, since slice chains are filled in as data arrives.
DataDictionary's indexer setter now clears its cached keys and values like Add()
does, otherwise Values kept returning the list from before the set.
StrikeTarget carries the one strike criterion of OptionChain.Select and Pick,
at the money, moneyness, distance from ATM or delta, so the criteria can no longer
conflict and the selection math is testable on its own. Shorter doc comments on
the selection helpers, StrikeList and days to expiry.
One method per strike criterion instead of a target type: Select and Pick take
the moneyness, SelectByStrikeDistance the distance from the underlying price
and SelectByDelta the target delta, all sharing the right and expiration
narrowing.
StrikeList is a read only collection whose closest, first above and first below
lookups binary search the sorted strikes. ClosestExpiry reads the cached expiry
view and Select the distinct expiries of its candidates, both searched by days
to expiration for the window bounds and the target.
The chain pickers, at() and days_to_expiry use the contract's listed date,
matching the shared filters. Counting Saturday and holiday expiries on their
last trading day moves to its own change.
@jhonabreul
jhonabreul force-pushed the feature-option-chain-pickers branch from 79bb448 to 3007d54 Compare September 9, 2026 21:25
…d Puts views

New shared filters on the option universe, the IOptionContractFilters interface and the chain: Strikes(strike) and Expiration(date) select exact values, ZeroDte the contracts expiring today, OutOfTheMoney/OTM, InTheMoney/ITM and AtTheMoney/ATM split the contracts around the underlying price, ATM being the closest strike with the lower one on ties. OptionPayoff gains IsInTheMoney, IsAtTheMoney and IsOutOfTheMoney, and the strategy filters' closest strike helper is shared with an explicit tie-break.

The Calls and Puts views and the AtTheMoney(right) picker are removed: CallsOnly(), PutsOnly() and Select(right) already cover them. At(expiry) delegates to Expiration(expiry).
…arthest expiration filters

The single contract pickers, the strike and expiry views and StrikeList are removed: the filters cover the same selections and return chains. Strikes and Expiration take sets of values, StrikesAbove, StrikesBelow, ExpiringAfter and ExpiringBefore select strict bounds, and FarthestExpiration is the opposite of ZeroDte, built like FrontMonth. All are shared by the option universe, the IOptionContractFilters interface and the chain.
@jhonabreul jhonabreul changed the title Add option chain selection helpers Add strike, expiration and moneyness filters to option chains and universes Sep 10, 2026
…t expiration in one pass

IndexOptionChainFiltersRegressionAlgorithm uses the strike, expiration and moneyness filters on SPX and SPXW contracts in the universe selection, on the slice chains and on OptionChain(). The GOOG universe filter keeps only the out of the money calls and its slice chain assertions cover the new filters. FarthestExpiration walks the contracts once instead of sorting them.
The closest strike is at the money only when its distance to the underlying price is within the tolerance, in units of the underlying price. The default of zero requires a strike equal to the price, so a filtered chain whose nearest strike is far from the money no longer reports it as at the money.
The expiration set and bound filters are tested on a futures universe and used in the futures universe selection of a regression algorithm. The option chain filters are tested on a future option chain built from universe rows, against the future option universe, and used in a regression algorithm on the universe selection of the future and its options, on the slice chains and on OptionChain().
A null tolerance, the default, accepts the closest strike within DefaultAtTheMoneyTolerance of the underlying price, so ordinary ladders report their nearest strike as at the money while a strike far from the money, as on a chain filtered away from it, is still rejected. An explicit tolerance stays an absolute distance in underlying price units, scaled by the strike multiplier like the price, and zero still requires an exact strike.
…eMoney

The argument, renamed maxStrikeDistance, bounds how far a strike can be from the underlying price, in units of it, for its contracts to be at the money, and every strike within it is selected. Its default, DefaultAtTheMoneyStrikeDistance, is two percent of the price: the nearest strikes on the usual ladders, from one dollar steps on SPY to five on IBM or half a dollar on F. A zero distance is the exact strike, through Strikes.
…ey by default

Without a distance, AtTheMoney selects the highest strike at or below the underlying price and the lowest at or above it, each only when it is within OptionFilterUniverse.DefaultAtTheMoneyStrikeDistance of the price, a settable percentage that defaults to 2%. A percentage range was too wide on fine ladders like SPXW, where 2% spans dozens of strikes, while the bracketing strikes give the one or two contracts traders call at the money on every ladder. An explicit distance still selects every strike within it.
@jhonabreul
jhonabreul merged commit 44803cc into QuantConnect:master Sep 11, 2026
6 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants