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 — plus the OCC symbol helper for the venues that speak it.

When you already know the symbol ("SPY", "AAPL") you can skip 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. An empty list means nothing matched or no venue is available; the reader never throws for "no data".

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 — contracts and underlying — is only meaningful within this same view. Don't persist these ids across sessions and don't mix ids from two views; re-resolve through OpenChainAsync instead.

What the listing contains

A chain can span thousands of contracts, so the listing is deliberately two-speed: Expiries is always the complete, ascending expiry list, while Contracts holds only the expiries actually fetched — 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 doesn't report one — center on the middle strike instead.
Expiries Every expiry the venue lists, ascending. Always complete.
Contracts The call+put contracts per strike of the expiries fetched so far. Partial by design.

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, not the whole board:

Per-expiry fetch// The two nearest monthlies after 30 days, say:
var wanted = view.Chain.Expiries
    .Where(e => e.DayNumber - DateOnly.FromDateTime(DateTime.UtcNow).DayNumber >= 30)
    .Take(2)
    .ToList();

OptionChain chain = await view.GetContractsAsync(wanted);

// Call/put per strike for one expiry:
var dec = 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 — quote subscriptions, and the NativeInstrumentId of a trade leg.
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 doesn't say.
Multiplier Contract multiplier (equity options: 100). Carried on the contract so the chain is self-describing — exposure math needs it, see snapshots.
Currency Quote currency, when the venue reports it.
NativeInstrumentType The venue-native asset-type classification (for example "StockOption" / "StockIndexOption") — an order leg must carry it verbatim. Empty when the source didn't classify; the order layer then rejects rather than guess.
DisplaySymbol Human-readable identity like "AAPL 18DEC26 150 C" — what an open order shows as its instrument string. Display only: machine addressing always travels in the native-id fields.

View lifecycle

A view holds venue-side state — dispose it when you're done. Disposing tears down every quote subscription created through it. And a view does not outlive its venue: if 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();                            // never leak the previous view
    _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 round-trips both ways:

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 can never false-positive:
if (OccOptionSymbol.TryParse(candidate, out var parsed))
    Log($"{parsed.Root} {parsed.Expiry:dd MMM yy} {parsed.Strike} {(parsed.IsCall ? "C" : "P")}");
Safe as a discriminator. TryParse only accepts a complete well-formed symbol — a valid date, C/P flag and exactly eight trailing strike digits. That full-match discipline means you can use it to tell option positions from equity positions on feeds whose payloads carry no instrument-type field. Never substitute a prefix heuristic.