Skip to content

Add option chain selection helpers - #9783

Open
jhonabreul wants to merge 6 commits into
QuantConnect:masterfrom
jhonabreul:feature-option-chain-pickers
Open

Add option chain selection helpers#9783
jhonabreul wants to merge 6 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, for OptionChainFilterUniverse.

Single contract pickers and views on OptionChain, so the usual sorted-comprehension selection becomes one call and returns None instead of raising when nothing matches:

chain = self.option_chain(symbol)
put = chain.select(OptionRight.PUT, target_dte=30, moneyness=-0.15)      # closest expiry to 30 days, strike closest to 85% of spot
call = chain.select_by_delta(0.3, OptionRight.CALL, target_dte=45)      # |delta| closest to 0.3
leg = chain.select_by_strike_distance(-5, OptionRight.PUT, min_dte=20)  # strike closest to spot - 5
expiry = chain.closest_expiry(target_dte=45, min_dte=30, max_dte=60)
atm = chain.at(expiry).at_the_money(OptionRight.CALL)
above = chain.strike_prices.first_above(chain.underlying.price)
dte = chain.calls[0].days_to_expiry
  • select / pick (synonym): by right, days to expiration window and moneyness; select_by_strike_distance and select_by_delta take the strike criterion the universe strategy filters and delta targeting use.
  • closest_expiry, at(expiry), at_the_money(right): expiry pickers that compose with the Share the option universe filters with OptionChain #9779 filters, e.g. chain.at(expiry).puts_only().strikes(-2, 0).
  • calls, puts, expiries, strike_prices: cached, read-only views. StrikePrices is a StrikeList with closest_to, first_above and first_below, binary searched over the sorted strikes.
  • days_to_expiry on contracts.
Design notes
  • Unlike the universe strategy filters, which take a minimum days to expiration and pick the first expiry at or after it, select takes a target and picks the closest expiry inside an optional [min_dte, max_dte] window. Both semantics are documented side by side.
  • The views are computed once per contract count: slice chains are filled in as data arrives, so a new contract invalidates them. They come back as IReadOnlyList, so repeated reads cannot be mutated.
  • Strike targets compare ScaledStrike with the underlying price, so index options with a strike multiplier work.
  • Days to expiration are counted to the listed expiration date, like the filters in Share the option universe filters with OptionChain #9779. Counting Saturday and holiday expiries on their last trading day is a separate follow-up.
  • All helpers live in OptionChain.Selection.cs; the core class and the filters file from Share the option universe filters with OptionChain #9779 are untouched.
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.

Related Issue

N/A

Motivation and Context

Option algorithms keep re-deriving the same picks from a chain: the expiry nearest a target, the same-expiry subset, the strike nearest the spot or a delta. Hand-rolled versions crash on empty min() and pick 0-DTE contracts by accident. One null-safe call each removes that.

Requires Documentation Change

Yes: the new OptionChain members, StrikeList and days_to_expiry.

How Has This Been Tested?

  • OptionChainSelectionTests (30 test methods): each helper against a synthetic chain built with the universe serializer: expiry windows and ties, moneyness, strike distance and delta targeting, sign-insensitive delta, contracts without greeks, null-safety on empty chains and missing underlying price, view caching and invalidation, read-only strike list, and Python access through pythonnet.
  • DataDictionaryTests: the indexer setter refreshes the cached keys and values.
  • OptionChainSelectionHelpersRegressionAlgorithm (C# and Python): every helper against the hand-rolled equivalent on real GOOG data, then trades the pick from the slice chain.
  • Full unit suite at the caching commit: 38787 passed, 1 failed, a timing-based LiveTradingDataFeedTests.HandlesAllTypes case that passes alone; the later commits reran the option fixtures.

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
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.

1 participant