Options levels (reference)
Reference
Options levels
The public surface of SabrTrader.Pipeline.Levels — gamma / options-levels
feeds, snapshots, strike profiles, settings schema and the ambient registry. Ships in
SabrTrader.Pipeline.Contracts (bundled by SabrTrader.Sdk).
Values & enums
public readonly record struct GammaLevel(
GammaLevelType Type, double Price, int Rank, string? Label, string? VendorTag);
public sealed class GammaLevelSnapshot
{
public required string Symbol { get; init; }
public required string ProviderKey { get; init; }
public required DateOnly SessionDate { get; init; }
public required DateTimeOffset AsOfUtc { get; init; }
public required IReadOnlyList<GammaLevel> Levels { get; init; }
public GammaScalars? Scalars { get; init; }
public double? UnderlyingPrice { get; init; }
}
public enum GammaLevelType
{
CallResistance = 0, PutSupport = 1, HVL = 2,
ZeroDteCallResistance = 3, ZeroDtePutSupport = 4, GexLevel = 5,
ExpectedMoveHigh = 6, ExpectedMoveLow = 7, BlindSpot = 8,
SwingLevel = 9, GammaScalpLevel = 10, VendorSpecific = 11,
}
public enum GammaRegime { Unknown = 0, Positive = 1, Negative = 2 }
public sealed class GammaScalars
{
public double? Iv { get; init; }
public double? IvRank { get; init; }
public double? NetDex { get; init; }
public double? NetGex { get; init; }
public double? PutCallRatioOi { get; init; }
public GammaRegime Regime { get; init; }
}
Feed & registry
public interface ILevelsFeed
{
string ProviderKey { get; }
string DisplayName { get; }
LevelsCapabilities Capabilities { get; }
int HistoryDays { get; }
event Action<string>? SnapshotChanged;
Task ConnectAsync(CancellationToken cancellationToken);
Task DisconnectAsync();
void Track(string symbol);
GammaLevelSnapshot? GetSnapshot(string symbol);
GammaLevelSnapshot? GetSnapshot(string symbol, DateOnly sessionDate);
string? MapInstrument(string instrumentId);
}
public interface ILevelsRegistry
{
IReadOnlyList<ILevelsFeed> Feeds { get; }
event Action? FeedsChanged;
ILevelsFeed? ResolveFeed(string instrumentId);
}
public static class LevelsAmbient
{
public static ILevelsRegistry Registry { get; }
public static void SetRegistry(ILevelsRegistry? registry);
public static IDisposable Use(ILevelsRegistry registry);
}
[Flags]
public enum LevelsCapabilities
{
None = 0, NamedLevels = 1, FuturesNative = 2, Intraday = 4, History = 8, StrikeProfile = 16,
ExpirySelection = 32,
}
public sealed class NullLevelsRegistry : ILevelsRegistry
{
public static NullLevelsRegistry Instance { get; }
public IReadOnlyList<ILevelsFeed> Feeds { get; }
public event Action? FeedsChanged;
public ILevelsFeed? ResolveFeed(string instrumentId);
}
public static class LevelsSymbolMapping
{
public static bool IsFuturesContract(string instrumentId);
public static string? NormalizeToRoot(string instrumentId);
}
Building a levels venue
A levels vendor is a venue plugin like any other:
VenueCategory.Levels on its manifest, VenueCapabilities.Levels declared, and
one ILevelsFeed published as a session port. Because that shape is identical for every
vendor, the SDK ships the session — LevelsVenueSession — so a levels plugin is a
descriptor plus a feed and nothing else.
public sealed class LevelsVenueSession : IVenueSession
{
public LevelsVenueSession(
string displayName,
Func<ILevelsFeed> feedFactory, // called once per connect attempt
IVenueNoticeSink? notices = null);
public IDataProvider? Data => null; // a levels venue serves no charts
public ITradingProvider? Trading => null; // …and routes no orders
public T? GetPort<T>() where T : class; // resolves ILevelsFeed while Connected
}
public IVenueSession CreateSession(VenueSessionContext context)
=> new LevelsVenueSession(
displayName: "My Levels",
feedFactory: () => new MyLevelsFeed(context.Settings["ApiKey"], context.Notices.Log),
notices: context.Notices);
Two behaviours worth knowing. The feed is built INSIDE
ConnectAsync, never at
session creation — vendor feed constructors validate credentials and throw on a blank key, and the
host must be able to create-and-abandon a session from any profile without a throw. And connect
failures are reported NON-permanent: a levels vendor cannot distinguish a rejected key from a
service hiccup, so the host's auto-retry keeps trying.Strike profiles
public interface ILevelsStrikeProfile
{
StrikeProfileSnapshot? GetStrikeProfile(string symbol);
}
public sealed class StrikeProfileSnapshot
{
public required string Symbol { get; init; }
public required string ProviderKey { get; init; }
public required DateTimeOffset AsOfUtc { get; init; }
public required IReadOnlyList<StrikeExposure> Strikes { get; init; }
public double? UnderlyingPrice { get; init; }
}
public readonly record struct StrikeExposure(double Strike, double NetGex, double CallGex, double PutGex);
Expiration selection
Implement this when your vendor can compute its levels from a chosen subset of
option expirations. A vendor that publishes pre-computed levels leaves it alone and changes nothing.
public interface ILevelsExpirySelection
{
IReadOnlyList<DateOnly> GetAvailableExpiries(string symbol);
IDisposable Track(string symbol, ExpirySelection selection);
GammaLevelSnapshot? GetSnapshot(string symbol, ExpirySelection selection);
StrikeProfileSnapshot? GetStrikeProfile(string symbol, ExpirySelection selection);
}
public enum ExpirySelectionKind
{
VendorDefault = 0, AllExpiries, Nearest, WithinDays, MonthlyOpex, OnDate,
}
public readonly record struct ExpirySelection
{
public ExpirySelectionKind Kind { get; }
public int Days { get; } // WithinDays
public DateOnly Date { get; } // OnDate
public static ExpirySelection VendorDefault { get; }
public static ExpirySelection AllExpiries { get; }
public static ExpirySelection Nearest { get; }
public static ExpirySelection ZeroDte { get; } // WithinDays(0)
public static ExpirySelection MonthlyOpex { get; }
public static ExpirySelection WithinDays(int days);
public static ExpirySelection On(DateOnly date);
public IReadOnlyList<DateOnly> Resolve(IReadOnlyCollection<DateOnly> available, DateOnly session);
public string ToToken();
public static bool TryParse(string? token, out ExpirySelection selection);
}
Intent, not mechanism. A selection says WHICH boards the caller wants, never
how you get them. A vendor whose endpoint already returns every expiration satisfies it by filtering
what it holds; a vendor with a per-expiration endpoint satisfies it by fetching that slice. Do not
assume fetching the whole chain is cheap for everyone.
Never invent your own rule.
Resolve is the single implementation
of what "0 DTE" or "monthly OPEX" means, and every feed calls it. Pass the session date in rather than
reading a clock, so the answer stays deterministic. Expirations before the session never contribute.
Say what you actually produced. Stamp
GammaLevelSnapshot.Expiry and StrikeProfileSnapshot.Expiry with the selection
the numbers REALLY represent, not the one you were asked for. A feed that cannot slice reports
VendorDefault and consumers then label the line plainly. Drawing a full-chain wall under a
"0 DTE" label is a trading error, so the snapshot is the authority, not the caller. A selection you
cannot satisfy publishes an EMPTY level set that still states itself — never substitute a neighbouring
board. If you advertise StrikeProfile as well, the profile must honour the same selection,
or a histogram will describe different boards than the lines drawn over it.
Track is the lifetime. Do the work a selection costs inside
Track, on your own thread, and keep it warm until the handle is disposed. Both
GetSnapshot overloads must stay lock-free and allocation-free enough to run on the chart
render thread, so serve a precomputed projection rather than computing on the caller. Drop a projection
once no handle holds it. Disposing twice, or after disconnect, is normal teardown.