Options: chain snapshots & analytics
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.
On this page
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. |
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?");
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");
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.