Options: underlyings & chains

Options data & trading

Underlyings & chains

Find option-capable underlyings, open a chain view, walk its expiries and fetch the call/put contracts you care about. Includes the OCC symbol helper for venues that use that format.

When you already know the symbol ("SPY", "AAPL"), go straight to opening a chain. For a picker UI or a scan, SearchUnderlyingsAsync queries every connected options-capable venue and merges the matches:

Searchusing SabrTrader.Pipeline.Options;

IReadOnlyList<OptionUnderlying> hits = await options.SearchUnderlyingsAsync("apple");
foreach (OptionUnderlying u in hits)
    Log($"{u.Symbol}  {u.Description}");        // e.g. "AAPL  Apple Inc."

Each hit is a small record: Symbol, Description and an UnderlyingId (the serving venue's id for it). Pass Symbol to OpenChainAsync. An empty list means nothing matched or no venue is available; the reader answers "no data" with an empty result.

Opening a chain

OpenChainAsync resolves the underlying symbol across the connected venues and returns an IOptionChainView, or null when no venue serves options for that symbol. The view is your handle for everything else in this chapter: contract listings, quote subscriptions and snapshots all go through it.

OpenIOptionChainView? view = await options.OpenChainAsync("SPY");
if (view is null) return;                        // no connected venue serves SPY options

// The listing fetched at open time:
OptionChain chain = view.Chain;
Log($"{chain.UnderlyingSymbol}: {chain.Expiries.Count} expiries, " +
    $"{chain.Contracts.Count} contracts pre-loaded");
Ids are view-scoped. The view is bound to the (hidden) venue that resolved the symbol. Every ProviderInstrumentId inside it, for contracts and for the underlying, is only meaningful within this same view. Keep ids within the view that produced them and re-resolve through OpenChainAsync in a new session.

What the listing contains

A chain can span thousands of contracts, so the listing has two speeds: Expiries is always the complete, ascending expiry list, while Contracts holds only the expiries fetched so far (at open time, the venue's default nearest expiries). You page in more per expiry as needed.

Member Meaning
UnderlyingSymbol The underlying as resolved by the serving venue.
UnderlyingProviderInstrumentId Provider id of the underlying itself, so you can subscribe its quote (for ATM centering) through the same quote surface as the contracts. Empty when the venue does not report one; center on the middle strike instead.
Expiries Every expiry the venue lists, ascending. Always complete.
Contracts The call and put contracts per strike of the expiries fetched so far.
Validate() Throws when the chain is inconsistent: an invalid contract, or a contract whose expiry is not in Expiries. Venue plugins call it on what they build.

Fetching more expiries

GetContractsAsync fetches the contracts of exactly the expiries you pass (the result again carries the full expiry list). Fetch what your computation needs rather than the whole board:

Per-expiry fetch// The first two expiries at least 30 days out:
var today = DateOnly.FromDateTime(DateTime.UtcNow);
var wanted = view.Chain.Expiries
    .Where(e => e.DayNumber - today.DayNumber >= 30)
    .Take(2)
    .ToList();

OptionChain chain = await view.GetContractsAsync(wanted);

// Call/put per strike for one expiry:
var board = chain.Contracts
    .Where(c => c.Expiry == wanted[0])
    .OrderBy(c => c.Strike)
    .ThenBy(c => c.Right);         // Call before Put per strike

Anatomy of a contract

Member Meaning
ProviderInstrumentId The id you feed back into this view for quote subscriptions, and the NativeInstrumentId of a trade leg. Opaque: pass it on, never parse it.
UnderlyingSymbol The underlying this contract belongs to.
Expiry, Strike, Right The contract identity: expiry date, strike price, and OptionRight.Call / OptionRight.Put.
ExerciseStyle American, European, or Unknown when the venue does not report it.
Multiplier Contract multiplier (default 100, the equity-option size). Carried on the contract so the chain is self-describing; exposure math needs it, see snapshots.
Currency Premium currency, when the venue reports it.
OptionClass The OCC root / exchange trading class ("SPX" vs "SPXW", "AAPL" vs the adjusted "AAPL1"), when the venue reports it. Several classes of one underlying list the same strike on the same expiry, so identity across venues is (underlying, expiry, strike, right, class). null means the venue does not distinguish classes and the standard class applies.
NativeInstrumentType The venue-native asset-type classification (for example "StockOption" or "StockIndexOption"). An order leg carries it verbatim. Empty when the source did not classify the contract; the order layer then rejects the leg.
DisplaySymbol Computed, human-readable identity like "AAPL 18DEC26 150 C": what an open order shows as its instrument string. Display only; machine addressing travels in the native-id fields.
Validate() Throws when the id or underlying is blank, or the strike or multiplier is not positive.

View lifecycle

A view holds venue-side state, so dispose it when you are done. Disposing tears down every quote subscription created through it. A view lives only as long as its venue: when the serving venue disconnects, subsequent calls on the view fail. The recovery pattern is the availability loop from the chapter overview: watch AvailabilityChanged, dispose the dead view, reopen.

Lifecycleprivate IOptionChainView? _view;

private async Task OpenAsync()
{
    _view?.Dispose();                            // release the previous view first
    _view = await _options!.OpenChainAsync("SPY");
}

public override void OnDispose()
{
    _view?.Dispose();
    _view = null;
}

The OCC symbol helper

OccOptionSymbol handles the OCC/OSI concatenated symbol form used by US retail broker APIs: {root}{yyMMdd}{C|P}{strike×1000, 8 digits}, e.g. AAPL240119C00190000 = AAPL 2024-01-19 190 Call. It formats and parses:

OccOptionSymbolusing SabrTrader.Pipeline.Options;

// Format:
var occ = new OccOptionSymbol("AAPL", new DateOnly(2026, 12, 18), IsCall: true, Strike: 150m);
string wire = occ.ToSymbol();                    // "AAPL261218C00150000"

// Strict parse: a plain equity ticker is never accepted.
if (OccOptionSymbol.TryParse(candidate, out var parsed))
    Log($"{parsed.Root} {parsed.Expiry:dd MMM yy} {parsed.Strike} {(parsed.IsCall ? "C" : "P")}");

// Just the class, from EITHER wire form: concatenated OCC or the 21-char space-padded OSI form.
string? cls = OccOptionSymbol.RootOf("AAPL  240119C00190000");   // "AAPL"; null when neither form
Safe as a discriminator. TryParse accepts only a complete well-formed symbol: a valid date, a C/P flag and exactly eight trailing strike digits. That full match lets you tell option positions from equity positions on feeds whose payloads carry no instrument-type field. Use it instead of a prefix heuristic. Parse is the throwing variant. RootOf is the looser sibling that answers only "which class is this?": it also accepts the space-padded OSI spelling, and it is the rule venues use to fill OptionContract.OptionClass from their own ids.