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. Includes the OCC symbol helper for venues that use that format.
On this page
Searching underlyings
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");
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
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.