Options: chain snapshots & analytics

Options data & trading

Chain snapshots & analytics

One call, one bounded, point-in-time capture of the chain with market data — contracts paired with IV, open interest, greeks and prices. The bulk input for exposure profiles (GEX/DEX/VEX), IV-rank and screeners.

Snapshot vs. stream

Quote subscriptions are for a working set you keep on screen. Exposure analytics are the opposite shape: they want every relevant contract once, joined with its market data, at a single point in time. That is GetSnapshotAsync — the host fans out the venue calls, waits (bounded) for the data to land, and hands you the finished capture. No subscription management, no delta merging.

Capturing a snapshot

Snapshotusing SabrTrader.Pipeline.Options;

OptionChainSnapshot snap = await view.GetSnapshotAsync();   // null request = host defaults

Log($"{snap.UnderlyingSymbol} @ {snap.UnderlyingPrice?.ToString() ?? "?"}: " +
    $"{snap.Entries.Count} contracts across {snap.Expiries.Count} expiries");

foreach (OptionChainSnapshotEntry e in snap.Entries)
{
    OptionContract c = e.Contract;   // strike / right / expiry / multiplier
    OptionQuote    q = e.Quote;      // IV / OI / greeks / prices — nullable per field
}

Each entry pairs one OptionContract with the OptionQuote captured for it. Quote fields are nullable exactly as on the stream: a venue that doesn't serve greeks in this entitlement leaves them null. UnderlyingPrice is the spot at capture time, when the venue reported one — guard for null before centering math.

Bounding the capture

Pass an OptionChainSnapshotRequest to widen or narrow the capture. Every field is optional — null means "the host's default for the serving venue". The host treats your values as bounds it clamps to what the venue can serve economically, never as a guarantee:

Bounded requestvar snap = await view.GetSnapshotAsync(new OptionChainSnapshotRequest
{
    MaxExpiries      = 8,                          // nearest 8 expiries
    MaxContracts     = 800,                        // centered on the money, both sides
    MinGreekCoverage = 0.9,                        // wait until 90% of entries carry greeks…
    GreekWaitTimeout = TimeSpan.FromSeconds(10),   // …but never longer than this
});
Field Meaning
Expiries Exact expiries to capture. Null/empty ⇒ the venue's default expiry window, bounded by MaxExpiries.
MaxExpiries Cap on the number of expiries when Expiries is not given.
MaxContracts Cap on total contracts, centered on the underlying price (both sides of the money). Raise it for full-chain analytics like net GEX, which sums the entire chain. The host keeps a small near-the-money window per expiry (~30 contracts) regardless, so quote subscriptions stay warmable — values below that floor are clamped up.
MinGreekCoverage Fraction (0..1) of captured contracts that must carry greeks/IV before the snapshot returns — only relevant on venues that stream greeks rather than serving them in bulk.
GreekWaitTimeout Longest the capture may wait for that coverage before returning what it has.
Wider captures cost venue calls and time. Request what the computation needs, not the maximum. A near-the-money IV smile needs one expiry and a few dozen strikes; a net-GEX profile needs the full chain of the front expiries — those are different requests, not one big one.

Greek coverage on streaming venues

Some venues answer a snapshot request from a bulk endpoint — greeks arrive with the chain in one response. Others serve greeks only over their streaming feed, so the host subscribes, waits for the data to flow in, captures, and unsubscribes. MinGreekCoverage + GreekWaitTimeout bound that wait. The snapshot returns when coverage is reached or the timeout expires — check what you actually got before dividing by it:

Coverage checkint withGreeks = snap.Entries.Count(e => e.Quote.Gamma is not null);
double coverage = snap.Entries.Count == 0 ? 0 : (double)withGreeks / snap.Entries.Count;
if (coverage < 0.5)
    Log($"Thin greek coverage ({coverage:P0}) — market closed or entitlement-limited feed?");
Off-hours captures are thin by nature. Outside market hours many feeds serve stale or no greeks and little open interest. A robust analytics plugin reports coverage alongside its result instead of silently computing on partial data.

Worked example: net GEX

The classic use case — the one this surface was opened up for. Gamma exposure per contract is Gamma × OpenInterest × Multiplier × Spot² × 0.01 (dollar gamma per 1% move, one common convention), calls positive and puts negative under the usual dealer-positioning assumption. Summed per strike it gives the exposure profile; summed overall, net GEX:

NetGex.csvar snap = await view.GetSnapshotAsync(new OptionChainSnapshotRequest
{
    MaxExpiries = 4, MaxContracts = 1200,
    MinGreekCoverage = 0.8, GreekWaitTimeout = TimeSpan.FromSeconds(10),
});
if (snap.UnderlyingPrice is not decimal spot) return;        // no spot, no exposure math

var byStrike = new SortedDictionary<decimal, decimal>();
decimal netGex = 0m;

foreach (var e in snap.Entries)
{
    if (e.Quote.Gamma is not decimal gamma) continue;        // count coverage, don't invent zeros
    if (e.Quote.OpenInterest is not decimal oi || oi == 0m) continue;

    decimal exposure = gamma * oi * e.Contract.Multiplier * spot * spot * 0.01m;
    if (e.Contract.Right == OptionRight.Put) exposure = -exposure;

    byStrike[e.Contract.Strike] = byStrike.GetValueOrDefault(e.Contract.Strike) + exposure;
    netGex += exposure;
}

// byStrike now plots as the exposure profile; the zero-crossing is the flip level.
Log($"Net GEX {snap.UnderlyingSymbol}: {netGex:N0} $/1% across {byStrike.Count} strikes");
The same loop gives DEX and VEX. Swap Gamma for Delta (delta exposure — sign by right and the dealer convention you choose) or Vega (vega exposure) and the rest of the capture, coverage and per-strike plumbing is identical. Rendering the profile on a chart is a normal custom-rendered indicator.