Add strike, expiration and moneyness filters to option chains and universes - #9783
Merged
jhonabreul merged 14 commits intoSep 11, 2026
Merged
Conversation
jhonabreul
force-pushed
the
feature-option-chain-pickers
branch
3 times, most recently
from
September 9, 2026 20:24
0abb215 to
79bb448
Compare
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
force-pushed
the
feature-option-chain-pickers
branch
from
September 9, 2026 21:25
79bb448 to
3007d54
Compare
…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.
Martin-Molinero
approved these changes
Sep 11, 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.
Martin-Molinero
approved these changes
Sep 11, 2026
…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.
Martin-Molinero
approved these changes
Sep 11, 2026
…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.
Martin-Molinero
approved these changes
Sep 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Builds on the chain filters from #9779, now merged. More filters, shared by the option universe selection (
set_filter), theIOptionContractFiltersinterface andOptionChain, so the same words work in both places:strikes(strikes)andexpiration(dates)select exact values, any number of them. Time of day is ignored.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.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 withinOptionFilterUniverse.DefaultAtTheMoneyStrikeDistanceof 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. Unlikestrikes(0, 0), which takes the first strike at or above the price.zero_dte(), the contracts expiring today, andfarthest_expiration(), its opposite, built likefront_month().days_to_expiryon contracts, andOptionPayoff.IsInTheMoney,IsAtTheMoneyandIsOutOfTheMoneynext to the existing payoff helpers.Design notes
strikes(100, 105)would bind to the relativestrikes(min, max)in both languages,strikes([100, 105])cannot. Python lists of numbers and dates convert.strikes(min, max)does; an explicit ATM distance is scaled the same way.DefaultAtTheMoneyStrikeDistancelives on the non-genericOptionFilterUniverseso the universe and the chain share one value; a static on the generic base would be one per closed type.OptionFilterUniverseandOptionChain.FarthestExpirationwalks the contracts once, keeping the latest expiration seen and the contracts at it.FarthestExpirationlive in the contract base, so futures universes get them too.OptionChain.Filters.cs; each one forwards to the universe filter throughOptionChainFilterUniverse, 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 cachedKeysandValueslists, so afterchain.Contracts[symbol] = contracttheValuescollection was stale.Add()cleared all three. The setter now does the same;DataDictionaryTestscovers it.Notes for review
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
OptionChainand the option universe,days_to_expiryand theOptionPayoffpredicates.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_expiryagainst 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 andFarthestExpirationon 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()andfarthest_expiration()against hand-rolled expectations onoption_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 withcalls_only().expiring_after(time).front_month().FutureUniverseFiltersRegressionAlgorithm(C# and Python): ES and GC universes selected withexpiring_afterandexpiring_before, checked onfutures_chain()and the slice chains.FutureOptionChainFiltersRegressionAlgorithm(C# and Python): the March 2020 ES future selected byexpiration([date]), its options bystrikes(-3, 3).out_of_the_money(), the exact contracts each filter returns onoption_chain(), and the slice chains against the universe filter and the live future price.Types of changes
Checklist:
bug-<issue#>-<description>orfeature-<issue#>-<description>