Skip to content

Share the option universe filters with OptionChain - #9779

Open
jhonabreul wants to merge 12 commits into
QuantConnect:masterfrom
jhonabreul:feature-option-filter-shared-engine
Open

Share the option universe filters with OptionChain#9779
jhonabreul wants to merge 12 commits into
QuantConnect:masterfrom
jhonabreul:feature-option-filter-shared-engine

Conversation

@jhonabreul

@jhonabreul jhonabreul commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Description

The option universe filters (option.set_filter(u => u.strikes(-5, 5).expiration(0, 30).calls_only())) now also run on an OptionChain, with the same names and semantics. Each call returns a new chain:

chain = slice.option_chains.get(symbol)
puts = chain.puts_only().expiration(20, 40).strikes(-3, 0)
legs = chain.iron_condor(30, 5, 10)                      # 4 contracts, or empty
oi = chain.where(lambda c: c.open_interest > 100)        # Python only, C# keeps Linq
  • Every filter of OptionFilterUniverse is available: strikes, expiration, calls/puts, standards/weeklys, front/back month, greeks, IV and OI ranges (with the d/g/t/v/r/iv/oi aliases) and the 18 strategy pickers (naked_callput_ladder).
  • OptionFilterUniverse keeps its public API; the filter bodies did not move.
Class model

One filter engine, two front-ends:

  • ContractSecurityFilterUniverse (existing) now constrains its data to ISymbolProvider instead of IChainUniverseData, which extends IBaseData and so excluded chain contracts, and reads Symbol.ID. ApplyTypesFilter skips its pass when every contract type is accepted.
  • BaseOptionFilterUniverse<TUniverse, TData>: the former OptionFilterUniverse, renamed in place. The Option security field became two abstract members, ExchangeHours and SecurityType, and the greeks, IV and OI filters read their values through three abstract accessors, GetGreeks, GetImpliedVolatility, GetOpenInterest.
  • OptionFilterUniverse: thin subclass over OptionUniverse, supplies all of the above from the security and the universe data. Same constructors, CreateDataInstance and implicit list conversion as before.
  • OptionChainFilterUniverse (new, internal): thin subclass over OptionContract, exchange hours from the market hours database, strike multiplier from the contract's symbol properties.
  • IOptionContractFilters<TSelf> (new): declares every shared filter. Implemented by the base as IOptionContractFilters<TUniverse> and by OptionChain as IOptionContractFilters<OptionChain>, so a filter added to one side must be added to the other; a reflection test checks the interface covers every universe filter.
  • OptionChain: now partial, filters in OptionChain.Filters.cs. Each filter builds an OptionChainFilterUniverse over the contracts, applies the base filter and returns a subset chain sharing the source's symbol, time, underlying and auxiliary data.
  • BaseContract, OptionContract, OptionUniverse and IChainUniverseData are not modified.
classDiagram
    direction TB

    namespace Legend {
        class New["New or renamed in place"]
        class FrontEnd["Front-end users call"]
        class Touched["Existing, touched where marked [new] or [changed]"]
        class Untouched["Existing, untouched"]
    }

    class ISymbolProvider {
        <<interface, untouched>>
        +Symbol Symbol
    }
    class IOptionContractFilters~TSelf~ {
        <<interface, new>>
        +Strikes(min, max) TSelf
        +Expiration(min, max) TSelf
        +CallsOnly() PutsOnly() TSelf
        +StandardsOnly() WeeklysOnly() TSelf
        +FrontMonth() BackMonth() BackMonths() TSelf
        +Delta() Gamma() Theta() Vega() Rho() IV() OI() TSelf
        +NakedCall() ... PutLadder() TSelf
    }
    class OptionUniverse {
        <<untouched>>
        universe file row
    }
    class OptionContract {
        <<untouched>>
        chain entry
    }
    class ContractSecurityFilterUniverse {
        <<abstract, touched>>
        TData is ISymbolProvider [changed, was IChainUniverseData]
        reads Symbol.ID instead of ID [changed]
        +Expiration() FrontMonth() StandardsOnly() T
        #ApplyTypesFilter() skips the pass for the default types [changed]
    }
    class BaseOptionFilterUniverse {
        <<abstract, renamed in place>>
        was OptionFilterUniverse
        +Strikes() CallsOnly() greeks ranges TUniverse
        +NakedCall() ... PutLadder() TUniverse
        #ExchangeHours abstract [new]
        #SecurityType abstract [new]
        #GetGreeks() GetImpliedVolatility() GetOpenInterest() abstract [new]
    }
    class OptionFilterUniverse {
        <<front-end>>
        TData = OptionUniverse
        +OptionFilterUniverse(Option security) [unchanged API]
        accessors read the universe data [new]
    }
    class OptionChainFilterUniverse {
        <<internal, new>>
        TData = OptionContract
        +OptionChainFilterUniverse(OptionChain chain)
        accessors read the contract
    }
    class FutureFilterUniverse {
        <<untouched>>
    }
    class OptionChain {
        <<front-end>>
        BaseChain of OptionContract
        +Strikes() CallsOnly() ... OptionChain [new]
        -Filter(f) new chain of the survivors [new]
        underlying price fix in the universe data constructor [changed]
    }

    ISymbolProvider <|.. OptionUniverse
    ISymbolProvider <|.. OptionContract

    ContractSecurityFilterUniverse <|-- BaseOptionFilterUniverse
    ContractSecurityFilterUniverse <|-- FutureFilterUniverse
    BaseOptionFilterUniverse <|-- OptionFilterUniverse
    BaseOptionFilterUniverse <|-- OptionChainFilterUniverse
    IOptionContractFilters <|.. BaseOptionFilterUniverse : TSelf = TUniverse
    IOptionContractFilters <|.. OptionChain : TSelf = OptionChain

    OptionFilterUniverse ..> OptionUniverse : filters
    OptionChainFilterUniverse ..> OptionContract : filters
    OptionChain ..> OptionChainFilterUniverse : creates per call

    style IOptionContractFilters fill:#F6E4D8,stroke:#B4551F,color:#1B2622
    style BaseOptionFilterUniverse fill:#F6E4D8,stroke:#B4551F,color:#1B2622
    style OptionChainFilterUniverse fill:#F6E4D8,stroke:#B4551F,color:#1B2622
    style OptionFilterUniverse fill:#DCEFEA,stroke:#0F7A68,color:#1B2622
    style OptionChain fill:#DCEFEA,stroke:#0F7A68,color:#1B2622
    style ContractSecurityFilterUniverse fill:#FFF3C4,stroke:#B08900,color:#1B2622
    style ISymbolProvider fill:#EDEDED,stroke:#8A8A8A,color:#1B2622
    style OptionUniverse fill:#EDEDED,stroke:#8A8A8A,color:#1B2622
    style OptionContract fill:#EDEDED,stroke:#8A8A8A,color:#1B2622
    style FutureFilterUniverse fill:#EDEDED,stroke:#8A8A8A,color:#1B2622
    style New fill:#F6E4D8,stroke:#B4551F,color:#1B2622
    style FrontEnd fill:#DCEFEA,stroke:#0F7A68,color:#1B2622
    style Touched fill:#FFF3C4,stroke:#B08900,color:#1B2622
    style Untouched fill:#EDEDED,stroke:#8A8A8A,color:#1B2622
Loading
Bugs found and fixed along the way
  • Chains built from universe data (algorithm.option_chain(symbol)) reported a zero underlying price: BaseChain pre-sets an empty QuoteBar, so the Underlying ??= in the OptionChain constructor never assigned. Fixed by assigning the first contract's underlying.
  • The strategy filters dereferenced the underlying price without checking it: they now select nothing when it is unknown, and ProtectiveCollar returns empty instead of throwing when a leg is missing.
Notes for review
  • OptionFilterUniverse.cs looks large in the diff but is mostly return types (OptionFilterUniverseTUniverse); the class was renamed in place instead of moved.
  • OptionChain.Where only takes a Python predicate. A C# Func overload changed the type of chain.Where(...) in existing algorithms, so C# keeps Linq.
  • Expirations are compared on the listed date, as before. Counting Saturday and holiday expiries on their last trading day is a separate follow-up.
  • Filters count days from BaseChain.ExchangeTime (new, defaults to Time). The slice factory sets it from the contract's exchange time zone, since slice chains are stamped with the algorithm time and the two dates differ for algorithms outside the exchange's time zone.
  • The chain's StandardsOnly/WeeklysOnly apply to the contracts already selected, so unlike the universe they compose with the expiry filters in any order.

Related Issue

N/A

Motivation and Context

Option algorithms keep re-deriving the same contract selections from a chain, in a vocabulary different from the one they already used in set_filter. Sharing the filters gives one grammar for both, with the same semantics, and removes the hand-rolled expiry and strike scans that crash on empty sequences.

Requires Documentation Change

Yes: the OptionChain filters (same names as the option universe filters) and IOptionContractFilters.

How Has This Been Tested?

  • OptionChainTests: every chain filter, alone and chained, returns the same contracts as the universe filter over identical data (68 cases); the exchange-time reference date; type filters in any order; filters on an empty chain; the underlying price of universe-built chains; Python access to the filters and where. Test data is written with OptionUniverse.ToCsv and read back with OptionUniverse.Reader, asserting the round trip.
  • OptionChainFiltersRegressionAlgorithm and OptionChainStrategyFiltersRegressionAlgorithm (C# and Python): filters on option_chain() and slice chains against hand-rolled expectations, trading the selection.
  • TimeSliceTests.ChainExchangeTimeIsInTheExchangeTimeZone: a Tokyo-zone slice stamps option and futures chains with New York exchange time.
  • OptionFilterUniverseTests, OptionFilterTests, OptionStrategyFilterTests, PythonOptionTests, FutureFilterUniverseTests: pass unchanged, one test added for the universe's type filter ordering rule.
  • Option and future regression algorithms (Name~Option|Name~Future): 404 passed, no statistics moved.
  • Full unit suite: 38714 passed, 2 failed, both timing-based LiveTradingDataFeedTests.HandlesAllTypes cases that pass when run alone.

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>

Turn OptionFilterUniverse into the generic BaseOptionFilterUniverse, renamed
in place, so the same filters run over universe rows and chain contracts.
OptionChain gets the universe filter vocabulary (strikes, expiration,
calls_only, standards/weeklys, front/back month, greeks, IV, OI and where)
through an internal OptionChainFilterUniverse, each call returning a new chain.

- IChainContractData and IOptionContractData let OptionContract be filtered
  without being BaseData; IOptionContractFilters declares the shared surface
  and OptionChainTests asserts the chain mirrors every universe filter
- Expirations count on the contract's last trading date, so Saturday and
  holiday expiries match expiration() and the strategy pickers on their
  last trading day
- Chains built from universe data now carry the underlying price
- OptionChainFiltersRegressionAlgorithm exercises the filters on option_chain()
  and slice chains in C# and Python
Write the test universe file with OptionUniverse.ToCsv and CsvHeader and read it
back with OptionUniverse.Reader, asserting the round trip, so the tests follow the
file format instead of hard coding it. Drop the reflection parity test, the shared
IOptionContractFilters interface keeps the chain and the universe in sync.
The universe strategy pickers, naked_call through put_ladder, select their legs
straight from an option chain with the same arguments and validation, returning
an empty chain when nothing matches or the underlying price is unknown.
IOptionContractFilters declares them so the chain and the universe stay in sync.
OptionChain is partial now: the class core keeps the constructors and Clone,
the universe filters and strategy pickers live in their own file.
Slice chains are stamped with the algorithm time, so their filters could count
days from the wrong date when the algorithm and the exchange time zones differ.
BaseChain.ExchangeTime, set by the slice factory from the contract subscription
time zone, is now the filters' reference date, as in the universe selection.
The strategy filters now return an empty selection when there is no
underlying price, in the base shared by the universe and the chain, so
the chain wrapper no longer needs a per-filter flag and argument
validation runs on every chain. ProtectiveCollar returns empty instead
of throwing when a leg is missing.

Reuse the Contracts selector for the greeks, IV, OI and strategy filters
and route the Linq extensions through it, replacing three copies of the
same filter primitive. ApplyTypesFilter skips the pass when every
contract type is accepted, and the chain filter universe shares the
chain's cached contract list instead of copying it.

Filtered chains share the auxiliary data with their source, the chain
type filters document that they apply in any order, and a reflection
test checks every universe filter is declared on the chain interface.
The subset constructor delegates to the copy constructor, which the revert had
undone, so filtered chains share the source's auxiliary data again.
The expiration filters and the strategy pickers compare the contract's listed
date again, as before the shared engine. Counting Saturday and holiday expiries
on their last trading day moves to its own change.
The contract filter base only needs ISymbolProvider, and the option base asks
its subclasses for greeks, implied volatility and open interest through three
abstract accessors, so the universe data and the chain contracts implement
nothing new. ApplyTypesFilter skips its pass when every contract type is
accepted.
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