Options: underlyings & chains
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.
On this page
Searching underlyings
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");
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")}");
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.