Orderflow & depth

Reference

Orderflow & depth

The contracts a footprint / volume-profile / DOM plugin consumes: the per-bar orderflow model in SabrTrader.Pipeline.OrderFlow, and the live Level-2 / Level-3 market-depth surface in SabrTrader.Pipeline.MarketDepth.

Orderflow — SabrTrader.Pipeline.OrderFlow

The per-bar orderflow model footprint-class indicators read, plus the volume-profile-window seam a drawing tool requests. Add using SabrTrader.Pipeline.OrderFlow; to reach these types.

OrderFlowBar

A readonly record struct: the OHLCV Bar plus parallel per-price-level bid/ask volume and trade-print counts. Prices are discretized to integer ticks; level i sits at (LowTicks + i) × TickSize + PriceOffset. Designed for allocation-free reads in tight render loops.

Member Description
Bar Bar The OHLCV bar this snapshot corresponds to.
long LowTicks Integer tick offset of level index 0.
double TickSize Price increment used for level indexing.
double PriceOffset Constant added to every level price (0 for ordinary bars; non-zero only on derived / tick-grouped bars).
int LevelCount Number of populated price levels (0 for an empty bar).
long TotalBidVolume / TotalAskVolume Cross-level side totals. O(1) (precomputed in ctor).
long TotalTradeCount Total trade prints across all levels. O(1).
long Delta TotalAskVolume - TotalBidVolume (positive = net buying).
long TotalUpTickVolume / TotalDownTickVolume Sierra-style uptick / downtick volume (Stats?.UpTickVolume / DownTickVolume, else 0). Distinct from aggressor ask/bid volume — same-price trades inherit the prior tick-rule direction.
long LocalDeltaHigh / LocalDeltaLow Intra-bar running-delta extremes with a 0-open floor (high ≥ 0, low ≤ 0). Cum-delta candle high/low = entering_cum + these.
OrderFlowBarStats? Stats Optional tick-derived statistics; null when the bar wasn't built from a tick stream.
bool HasStats True when Stats is populated.
bool HasTradeCountSplit True when per-side trade-print counts are available.
OrderFlowLevel GetLevel(int index) Read one level by index (0 = lowest price). Throws on out-of-range.
long GetBidTradeCount(int index) / GetAskTradeCount(int index) Per-side trade-print count at a level; 0 when the split wasn't tracked.
bool TryGetLevelAtPrice(double price, out OrderFlowLevel level) Look up the level at a price; true only when it lands on a tick within range.
bool TryGetPoc(out double pocPrice, out long pocVolume) Point of Control = highest-total-volume level (lower price wins on a tie).
OrderFlowBar WithChartDisplay(TimeZoneInfo tz) Stamps the wrapped Bar's StartDisplay / EndDisplay without copying the level arrays. Identity when the bar stamp is a no-op (UTC zone, day-and-longer bars, or a default-struct with no levels).
OrderFlowBar WithoutChartDisplay() Drops chart-zone stamps on the wrapped bar so a cache write cannot persist display wall-clock. Ladder arrays, stats, and UTC identity are unchanged.

Constructors. Three overloads, additive:

OrderFlowBar.cs// Full-fidelity: bid/ask/trade-count arrays + running-delta extremes (validated).
public OrderFlowBar(Bar bar, long lowTicks, double tickSize,
    long[] bidVolumes, long[] askVolumes, long[] tradeCounts,
    long localDeltaHigh, long localDeltaLow,
    long[]? bidTradeCounts = null, long[]? askTradeCounts = null,
    OrderFlowBarStats? stats = null, double priceOffset = 0);

// Defaults the running-delta extremes to 0 (empty / no-tick bars).
public OrderFlowBar(Bar bar, long lowTicks, double tickSize,
    long[] bidVolumes, long[] askVolumes, long[] tradeCounts);

// Volume-only: every level's TradeCount = 0 (seeding, fixtures).
public OrderFlowBar(Bar bar, long lowTicks, double tickSize,
    long[] bidVolumes, long[] askVolumes);

OrderFlowLevel — one price row, a readonly record struct returned by GetLevel / TryGetLevelAtPrice:

Member Description
double Price Price at this level.
long BidVolume / AskVolume Volume credited to bids (sell-aggressor) / asks (buy-aggressor).
long TradeCount Number of distinct trade prints at this price (Sierra-style).
long TotalVolume BidVolume + AskVolume.
long Delta AskVolume - BidVolume.

OrderFlowBarStats

A readonly record struct of per-bar quantities that can only be computed during tick aggregation (they depend on tick order). Carried by OrderFlowBar.Stats; null when no tick aggregator ran.

OrderFlowBarStats.cspublic readonly record struct OrderFlowBarStats(
    long RunningDeltaMax,
    long RunningDeltaMin,
    long AskVolumeSinceHigh,
    long BidVolumeSinceHigh,
    long AskVolumeSinceLow,
    long BidVolumeSinceLow,
    double VolumePerSecond,
    bool? LastTickIsBid = null,
    long FirstTickVolume = 0,
    System.TimeSpan? TradeSpan = null,
    long UpTickVolume = 0,
    long DownTickVolume = 0,
    double? LastTradePrice = null,
    int LastTickRuleDirection = 0,
    bool? DeltaHighSetAfterLow = null,
    DateTime? FirstTradeUtc = null,
    DateTime? LastTradeUtc = null);
Member Description
long RunningDeltaMax / RunningDeltaMin Highest / lowest the intra-bar running delta reached (no 0 floor).
long AskVolumeSinceHigh / BidVolumeSinceHigh Volume traded since the bar last made its high.
long AskVolumeSinceLow / BidVolumeSinceLow Volume traded since the bar last made its low.
double VolumePerSecond Short-window volume rate.
bool? LastTickIsBid Whether the bar's last classified tick hit the bid (sell-aggressor). Null when untracked.
long FirstTickVolume Size of the bar's first classified trade.
System.TimeSpan? TradeSpan Elapsed time from the bar's first to last print (LastTradeUtc − FirstTradeUtc). Null when untracked. The endpoints are FirstTradeUtc / LastTradeUtc — do not reconstruct them from this span plus the bar window.
long UpTickVolume / DownTickVolume Tick-rule uptick / downtick volume for the bar (Sierra Chart convention). Surfaced on the bar as TotalUpTickVolume / TotalDownTickVolume.
double? LastTradePrice Last trade price in the bar — used to continue the tick-rule across historical→live seed without a discontinuity.
int LastTickRuleDirection Last established tick-rule direction at bar end: +1 up, −1 down, 0 none yet.
bool? DeltaHighSetAfterLow Which running-delta extreme updated last: true = high, false = low. Null when no exclusive update has happened yet (empty bar, or the opening print where max == min). Same >/< rule as the extremes themselves. Sierra Chart Finish AskVol BidVol Diff is barDelta − (DeltaHighSetAfterLow ? RunningDeltaMax : RunningDeltaMin); when this is null the finish is 0. Distinct from AskVolumeSinceHigh/SinceLow, which are volume since the bar's price high/low.
DateTime? FirstTradeUtc / LastTradeUtc Exchange timestamp of the bar's first / last trade print — true venue UTC, always. Null when untracked. These are the endpoints of TradeSpan; they cannot be recovered from the span plus the bar window — the first print is not necessarily the bar open. They are never rewritten to a display-zone clock. Overlay them on the chart with ChartDisplayTime.FromUtc(stats.FirstTradeUtc.Value, ctx.DisplayTimeZone) and compare to Bar.Start / Bar.End, not StartUtc. TradeSpan is a duration and does not change with the display zone.
long CotHigh AskVolumeSinceHigh - BidVolumeSinceHigh (commitment-of-traders at the high).
long CotLow AskVolumeSinceLow - BidVolumeSinceLow.
static OrderFlowBarStats Empty All-zero stats.

Stats is filled on tick-built bars and on Databento / CQG / Tradovate / crypto 1-minute profiled ladders (those replay an aggressor-flagged tape). Rithmic and dxFeed profiled ladders leave it null — they have per-price volume, not print order.

OrderFlowResolution

Backfill granularity an orderflow consumer requests. Coarser = far less data = faster backfill, at the cost of aggressor fidelity. The value round-trips by name, so the numeric assignments are free to change.

Value Meaning
Auto = 0 Default. Concrete resolution derived centrally from the chart's primary bar period. Not a point on the fidelity scale.
Tick = 1 Full tick data: exact per-trade aggressor, exact volume-at-price and delta. Heaviest backfill.
Second = 2 Native 1-second OHLCV; aggressor estimated per second. Bid/ask approximate.
Minute = 3 Native 1-minute OHLCV; aggressor estimated per minute. Coarsest approximation.
Note: at Second/Minute, volume-at-price (Volume Profile / TPO) stays accurate but delta is estimated with an uptick/downtick rule. The current forming bar is always built from live ticks and is exact.

OrderFlowWindowRequest

An immutable readonly record struct describing the volume-at-price profile a tool wants for a time window. Value equality makes it double as the host's cache key — same window + parameters resolve to the same cached profile; dragging an anchor produces a distinct key and a fresh build.

OrderFlowWindowRequest.cspublic readonly record struct OrderFlowWindowRequest(
    DateTime FromUtc,
    DateTime ToUtcExclusive,
    OrderFlowResolution Resolution,
    int TickAggregation,
    double ValueAreaPercentage,
    bool EnableSmoothing,
    int SmoothingPeriod,
    int HighVolumeNodeCount,
    int LowVolumeNodeCount,
    int HighVolumeRegionSize,
    int LowVolumeRegionSize);

bool HasWindow — true only when ToUtcExclusive > FromUtc.

VolumeProfileWindow

A finished volume-at-price profile for one window, a plain sealed record free of engine types so a contracts-only drawing tool can render it. Produced host-side and handed back via IOrderFlowWindowSource.

VolumeProfileWindow.cspublic sealed record VolumeProfileWindow(
    IReadOnlyList<VolumeProfileBin> Bins,
    double BucketStep,
    double MaxVolume,
    double MaxAbsDelta,
    double TotalVolume,
    double Poc,
    double ValueAreaHigh,
    double ValueAreaLow,
    double HighPrice,
    double LowPrice,
    IReadOnlyList<double> HighVolumeNodePrices,
    IReadOnlyList<double> LowVolumeNodePrices,
    DateTime FromUtc,
    DateTime ToUtcExclusive);

bool IsEmpty — true when the window enclosed no traded volume. Each row is a VolumeProfileBin (readonly record struct):

Member Description
double Price Bucketed price of this row.
double TotalVolume Total traded volume at the bucket.
double Delta Signed delta (ask − bid volume).
bool InValueArea Whether the bucket falls inside the value area.

IOrderFlowWindowSource

Host-provided seam that resolves an OrderFlowWindowRequest to a VolumeProfileWindow. Surfaced to drawing tools through IDrawingToolRenderContext.OrderFlowWindows.

IOrderFlowWindowSource.cspublic interface IOrderFlowWindowSource
{
    // Returns the cached profile when ready, or null while a build is in flight.
    // On a cache miss, kicks an async build and repaints when done. Never blocks.
    VolumeProfileWindow? GetOrRequest(in OrderFlowWindowRequest request);
}

Call it from the synchronous render loop: it returns immediately, so a tool paints a "calculating" placeholder on null and the real profile on the next frame after the async build completes.

Market depth — SabrTrader.Pipeline.MarketDepth

The live order-book surface. A provider may offer aggregated Level-2 (IMarketDepthFeed), per-order Level-3 / MBO (IMarketByOrderFeed), or both; the OrderBook unifies whichever arrives. Add using SabrTrader.Pipeline.MarketDepth;.

IMarketDepthFeed

Live aggregated Level-2 depth feed. Emits DepthUpdate per price-level change. Events may arrive on any thread; the feed reference-counts per instrument so subscribers share one upstream connection.

IMarketDepthFeed.cspublic interface IMarketDepthFeed : IDisposable
{
    IDisposable Subscribe(string instrumentId, Action<DepthUpdate> onUpdate);
    // Also hear about updates the feed dropped before the next one it delivers.
    // Default forwards to the two-argument form: a synchronous fan-out never drops.
    IDisposable Subscribe(string instrumentId, Action<DepthUpdate> onUpdate, Action<long> onLoss);
    event Action? SubscriptionReset;   // drop state; default no-op
}

SubscriptionReset fires when consumers must drop all depth state (typically a connection loss). Disposing the token from Subscribe unsubscribes; the upstream sub is torn down when the last subscriber drops.

IMarketByOrderFeed

Live Market-By-Order (Level-3) feed. Emits MboEvent per resting-order lifecycle event (Add / Modify / Cancel). MboEvent.OrderId must be the venue's exchange-supplied id, stable across Modify (including a price move).

IMarketByOrderFeed.cspublic interface IMarketByOrderFeed : IDisposable
{
    IDisposable Subscribe(string instrumentId, Action<MboEvent> onEvent);
    // Also hear about events the feed dropped before the next one it delivers: the
    // count arrives before that event. Default forwards to the two-argument form
    // (a synchronous fan-out never drops); the hosted feed's per-instrument drain
    // pump overrides it, and the recorder subscribes this way.
    IDisposable Subscribe(string instrumentId, Action<MboEvent> onEvent, Action<long> onLoss);
    event Action? SubscriptionReset;   // clear book on connection loss; default no-op
    // One instrument's book was reset, re-imaged (RebuildStarted / RebuildCompleted)
    // or changed authority. Args: instrumentId, transition. Default no-op.
    event Action<string, OrderBookLifecycleEvent>? BookLifecycleChanged;
}
Loss heals itself, on the tape too. After the hosted feed reports a loss to a consumer that asked for it (the recorder), it asks the venue for a fresh image of the instrument, storm-guarded per instrument (5 s cooldown, doubling to 60 s). The image arrives as BookLifecycleChanged RebuildStarted / RebuildCompleted around the image events, and the recorder writes those as ReimageBegin / ReimageEnd, so a tape reads Gap, a few live deltas, then a bracketed image: the book is certifiably consistent again from the ReimageEnd. Plain consumers falling behind trigger nothing; the shared book they read never missed an event.
How a venue serves this feed. It creates a hosted feed from IMarketDepthHost (on its session context), injects raw book events into it, and exposes that same object as IDataProvider.Mbo. Sequencing, application to the shared book, late-subscriber seeding and consumer fan-out all happen behind Inject — identically for every venue, at every rate. See the hosted depth plane and the venue-seam reference.

IOrderBook

Read-only view onto a live OrderBook — the surface exposed via IIndicatorContext.OrderBook. The mutation API stays on the concrete OrderBook; indicator code only reads. Callers that need a frozen copy use OrderBookSnapshot.Capture.

Member Description
string InstrumentId The instrument this book tracks.
double BestBidPrice / BestAskPrice Top of book, or double.NaN when the side is empty.
double Spread Ask − bid, or NaN when either side is empty.
int OrderCount Total per-order count across both sides. Zero in L2-only mode.
int BidLevelCount / AskLevelCount Distinct price levels per side.
IEnumerable<PriceLevel> EnumerateBids() Bid levels top of book first (highest price). Live view.
IEnumerable<PriceLevel> EnumerateAsks() Ask levels top of book first (lowest price). Live view.
PriceLevel? GetLevel(Side side, double price) Level at an exact price; null when absent.
bool TryGetEntry(string orderId, out OrderBookEntry? entry) Look up a single resting order by id (MBO mode); false when absent.
event Action<OrderBookEntry, DateTime>? OrderRemoved Raised when an MBO order leaves the book (Cancel / consumed), with its last state and the event time.
OrderBookDepthMode DepthMode Which feed currently authors the book: PerOrder while the host has marked a market-by-order stream authoritative, Aggregated otherwise. Can change mid-session; announced through OrderBookLifecycleEvent.DepthModeChanged.
long MboAddCount / MboModifyCount / MboCancelCount Monotonic counts of what applying an event did to this book instance. Not wire actions: an Add for an id already resting counts as a Modify, a Modify that drains the size to zero removes the order but counts as a Modify, and an event for an id the book never saw counts only as a miss. Never reset, and on a live chart the book is shared by every consumer of the instrument. Their deltas are not meant to match one indicator's callback count; count your own callbacks when you need that.
long MboDuplicateAddCount / MboModifyMissCount / MboCancelMissCount Miss / duplicate diagnostics. The health read is MboAddCount far above the two miss counts; sustained misses mean Modify/Cancel events whose Add never landed (image/live race or an upstream symbol-key mismatch).
int PeakOrderCount / PeakLevelCount Peak observed counts since construction.
Thread-safety: the live book is mutated under a feed-side lock. Read it only inside the indicator callback for the current event, or capture a snapshot to hand off to a render thread.

OrderBook

The concrete sealed class OrderBook : IOrderBook for one instrument. Accepts both aggregated L2 and per-order MBO events into a unified per-level surface. Not internally synchronised — feed code holds the lock.

Member Description
OrderBook(string instrumentId) Construct for an instrument (throws on blank id).
void ApplyDepth(in DepthUpdate update) Apply one aggregated L2 event. Set creates/replaces a level's size; Delete (or size 0) removes it.
void ApplyMbo(in MboEvent evt) Apply one MBO event (Add / Modify / Cancel). Unknown id on Modify/Cancel is a silent no-op (a price-move Modify resurrects the order).
void Clear() Drop every level + per-order entry (used on reconnect).
bool TryGetOrder(string orderId, out Side side, out double price) Locate a resting order's side and price by id; false when absent.
Plus the full IOrderBook read surface above (including TryGetEntry and OrderRemoved).

OrderBookSnapshot

Immutable, render-safe copy of a book at one moment — flat Bids / Asks arrays (index 0 = top of book) so iteration is allocation-free and rows match visual ladder order. Per-order detail is dropped; each row is aggregated only.

Member Description
string InstrumentId Instrument the source book tracks.
DateTime CapturedAtUtc Wall-clock time the snapshot was taken (UTC).
IReadOnlyList<Row> Bids / Asks Rows, top of book first.
double BestBidPrice / BestAskPrice Bids[0].Price / Asks[0].Price, or NaN when empty.
record struct Row(double Price, long Size, int OrderCount) One snapshot row (OrderCount informational, 0 in L2 mode).
static OrderBookSnapshot Capture(IOrderBook book, DateTime capturedAtUtc, int maxLevelsPerSide = int.MaxValue) Capture a flat copy under the feed lock. maxLevelsPerSide caps depth per side.
static OrderBookSnapshot Empty(string instrumentId, DateTime capturedAtUtc) A "no data yet" default for renderers.

PriceLevel

One price level inside an OrderBook (a sealed class). Carries the aggregated Size and, when MBO-fed, an ordered per-order queue. A level constructed only from L2 reports OrderCount == 0.

Member Description
double Price Price of this level (immutable while in the book).
Side Side Which side this level is on.
long Size Aggregated total size (sum of the per-order queue when MBO-fed).
int OrderCount Per-order entries at this level. 0 in L2-only mode.
IEnumerable<OrderBookEntry> EnumerateOrders() Per-order queue front to back. Empty in L2-only mode. Live view.

OrderBookEntry

One resting order in an MBO-mode book (a sealed class; identity is OrderId).

Member Description
string OrderId Venue-supplied id, stable for the order's lifetime.
Side Side Side the order rests on.
double Price Current price (changes on a price-move Modify).
long Size Current size (changes on Modify).
ulong QueuePriority Vendor queue position (smaller = front; 0 = preserve arrival order).
DateTime FirstSeenUtc When the order was first added to the book.
DateTime? LastChangedUtc When the order was last modified; null if never changed since add.
bool IsAggressive Whether the order has been flagged as an aggressor.
bool IsIceberg Whether the order shows iceberg (hidden-size refresh) behaviour.

DepthUpdate

One aggregated Level-2 depth event, a readonly record struct passed by value. Feed sources pass SequenceNumber = 0; the framework's sequencer stamps it during fan-out.

DepthUpdate.cspublic readonly record struct DepthUpdate(
    long SequenceNumber,
    DateTime ExchangeTimestampUtc,
    string InstrumentId,
    Side Side,
    DepthAction Action,
    double Price,
    long Size);

DepthAction

Kind of update a DepthUpdate carries (an enum : byte).

Value Meaning
Set = 0 Replace the size at this price level; creates the level if absent.
Delete = 1 Remove the price level entirely (carried Size ignored).

MboEvent

One Market-By-Order event, a readonly record struct passed by value. Carries the individual resting order's identity, price, size and queue priority. On a price-move Modify, PrevPrice holds the old price (double.NaN when unchanged).

MboEvent.cspublic readonly record struct MboEvent(
    long SequenceNumber,        // platform delivery stamp: venues leave it 0
    DateTime ExchangeTimestampUtc,
    string InstrumentId,
    string OrderId,
    Side Side,
    MboAction Action,
    double Price,
    long Size,
    ulong QueuePriority,
    double PrevPrice,
    ulong VenueSequence);       // the venue's own wire counter, 0 when it has none

// Ten-argument constructor: VenueSequence = 0.
public MboEvent(long SequenceNumber, DateTime ExchangeTimestampUtc, string InstrumentId,
    string OrderId, Side Side, MboAction Action, double Price, long Size,
    ulong QueuePriority, double PrevPrice);
Two sequence numbers, two owners. SequenceNumber is stamped by the host's sequencer at fan-out and orders events across every instrument on the same sequencer shard, so one instrument's stream is never contiguous and a gap in it proves nothing. VenueSequence is forwarded untouched from the wire when the venue supplies one: Databento's per-record sequence, and Rithmic's per-frame sequence_number over R|Protocol (every row of a frame shares the frame's value). It is 0 on venues that carry none (Rithmic R|API+, CQG, IQFeed) and on dxFeed, whose order sequence only disambiguates orders with the same timestamp. Its scope and stride are the venue's; a jump between values the venue defines as consecutive is the only wire-level evidence of rows this process never received. The recorded tape keeps VenueSequence and stores SequenceNumber as 0.

MboAction

Lifecycle of an MBO event (an enum : byte). Mirrors Rithmic DepthByOrder NEW / CHANGE / DELETE.

Value Meaning
Add = 0 A new resting order appeared. The book must not already contain the id.
Modify = 1 An order changed size and/or moved price. Size 0 is NOT a delete here.
Cancel = 2 Order is gone; the book removes it by OrderId.

Side

Side of the book a depth or order event applies to (an enum : byte). Two-valued — every depth event is sided at the source (aggressor classification on a trade is separate, via TickFlags).

Value Meaning
Bid = 0 Resting buy order. Stacks under best bid in descending price order.
Ask = 1 Resting sell order. Stacks above best ask in ascending price order.

OrderBookDepthMode

Which feed currently authors an IOrderBook (an enum : byte, read through IOrderBook.DepthMode). A venue that grants both streams starts per-order and falls back to aggregated when its market-by-order book cannot be recovered; the change is announced as OrderBookLifecycleEvent.DepthModeChanged.

Value Meaning
Aggregated = 0 Aggregated Level-2 updates author the price levels. Per-order detail may be present but is advisory: an aggregated update that touches a level replaces that level's per-order queue with its total.
PerOrder = 1 A market-by-order stream is authoritative: every level is the sum of its resting orders and aggregated updates are not applied. TryGetEntry and PriceLevel.EnumerateOrders describe the venue's book.

OrderBookLifecycleEvent

A transition of the shared book that invalidates state a consumer derived from earlier depth or MBO callbacks (an enum : byte). Delivered through IIndicator.OnOrderBookLifecycle; each value names what has already happened to the book when the callback runs.

Value Meaning
Reset = 0 The book was cleared and nothing is promised to follow: the feed subscription was invalidated (a connection loss is the usual cause). Drop derived state and rebuild it from the events that follow.
RebuildStarted = 1 The book was cleared because the venue is sending a fresh image of every resting order. The events that follow are that image, then live deltas. Derived state is stale until RebuildCompleted.
RebuildCompleted = 2 The image has been applied and the book is consistent again. Re-derive state from ReadOrderBook here rather than from the image events, which reach a consumer through the same sampled delivery as live events.
DepthModeChanged = 3 IOrderBook.DepthMode changed. Per-order features stop describing the venue's book once the mode is Aggregated.
CaptureGap = 4 The recorded stream has a hole of unknown size here (the recorder wrote a Gap marker): the book was cleared and is incomplete until the next RebuildCompleted. Raised only when a tape is replayed (strategy backtests, market replay); on a live chart delivery loss is counted through OnMarketByOrderLoss instead.

Recorded tapes — SabrTrader.Pipeline.Storage

The platform's recorder writes an instrument's L2 / MBO stream to disk together with the control markers a reader needs to rebuild the book honestly. Live indicator callbacks are sampled; the tape is the record. Recording is started from the platform's recording window, not from a plugin. Add using SabrTrader.Pipeline.Storage;.

IRecordedDepthCatalog

Read seam over the recorded tapes, reached from an indicator through IIndicatorContext.RecordedDepth (null when the host records none). Tapes are keyed by DepthStoreKey (venue provider key + instrument); a null cursor means the store has no coverage of the window. Reads never block a live feed.

IRecordedDepthCatalog.cspublic interface IRecordedDepthCatalog
{
    IMboCursor? OpenMbo(DepthStoreKey key, DateTime fromUtc, DateTime toUtcExclusive);
    IDepthCursor? OpenDepth(DepthStoreKey key, DateTime fromUtc, DateTime toUtcExclusive);
    IHeatmapSidecarCursor? OpenHeatmap(DepthStoreKey key, DateTime fromUtc, DateTime toUtcExclusive);

    IReadOnlyList<DepthStoreKey> EnumerateMboKeys();     // every key with an MBO tape
    IReadOnlyList<DepthStoreKey> EnumerateDepthKeys();   // every key with an L2 tape
    DepthStoreKeyInfo? DescribeMbo(DepthStoreKey key);       // coverage, or null
    DepthStoreKeyInfo? DescribeDepth(DepthStoreKey key);
}

public readonly record struct DepthStoreKey(string ProviderKey, string InstrumentId);

public interface IMboCursor : IDisposable
{
    bool MoveNext();          // false when the window is exhausted
    MboTapeItem Current { get; }
}

public readonly record struct MboTapeItem(
    DepthTapeMarker Marker,
    DateTime ExchangeTimestampUtc,   // the marker time when Marker is not Event
    MboEvent Event);                 // default when Marker is not Event

DepthTapeMarker

Control records on a tape (an enum : byte). Events are the payload; the rest bracket reconnects, venue images and capture loss so a reader never invents fills.

Value Meaning
Event = 0 A venue MboEvent / DepthUpdate follows.
Reset = 1 The live subscription was reset. Drop reconstructed book state.
Gap = 2 Events are missing here: the recorder's writer overflowed and capture stopped, or the feed dropped events on the way to the recorder and capture continues. Either way the book is incomplete from here until the next ReimageBegin.
ReimageBegin = 3 The venue began a snapshot / re-image. Subsequent events replace the book.
ReimageEnd = 4 The venue finished the snapshot / re-image. The book is consistent again.
What a tape can and cannot certify. The recorder subscribes with the loss-reporting overload, so the tape carries a Gap for every hole the platform can observe: its own writer falling behind, and events the hosted feed's drain pump discarded before they reached the recorder. Every venue image and per-instrument reset, whether at recording start, after a loss, or from the platform's own book recovery, is bracketed by ReimageBegin / ReimageEnd or marked Reset through IMarketByOrderFeed.BookLifecycleChanged. What no consumer can mark is a row the venue never sent; a stretch of tape between markers that carries a venue sequence (MboEvent.VenueSequence) can be audited against that sequence, and a stretch without one is complete as far as the platform saw. The markers are written when the platform learns of the transition, so a marker can sit a few queued events early relative to the image rows; the book is consistent from ReimageEnd either way.

Example — reading the book from an indicator

DepthImbalance.csusing SabrTrader.Pipeline.Indicators;
using SabrTrader.Pipeline.MarketDepth;

// Inside an indicator: read top-of-book imbalance each calculation.
protected override void OnCalculate()
{
    IOrderBook book = Context.OrderBook;
    if (book is null) return;

    double bid = book.BestBidPrice, ask = book.BestAskPrice;
    if (double.IsNaN(bid) || double.IsNaN(ask)) return;   // one side empty

    PriceLevel? bestBid = book.GetLevel(Side.Bid, bid);
    PriceLevel? bestAsk = book.GetLevel(Side.Ask, ask);
    long bidSize = bestBid?.Size ?? 0, askSize = bestAsk?.Size ?? 0;

    // Imbalance in [-1, 1]: positive = bid-heavy.
    long total = bidSize + askSize;
    Imbalance[0] = total == 0 ? 0 : (double)(bidSize - askSize) / total;
}

// To hand the book to a render thread, snapshot it under the feed callback:
var snap = OrderBookSnapshot.Capture(Context.OrderBook, DateTime.UtcNow, maxLevelsPerSide: 20);

Example — rebuilding order lifecycles from the tape

QueueAudit.csusing SabrTrader.Pipeline.MarketDepth;
using SabrTrader.Pipeline.Storage;

// Off the hot path (a toolbar action, a background task): replay one hour of an
// instrument's MBO tape into a private book and measure how long orders rested.
void Audit(IIndicatorContext ctx, DateTime fromUtc)
{
    IRecordedDepthCatalog? tapes = ctx.RecordedDepth;
    if (tapes is null) return;                        // this host records nothing

    var key = tapes.EnumerateMboKeys()
        .FirstOrDefault(k => k.InstrumentId == "MNQU6");
    if (key == default) return;                       // never recorded

    using IMboCursor? cursor = tapes.OpenMbo(key, fromUtc, fromUtc.AddHours(1));
    if (cursor is null) return;                       // no coverage of that window

    var book = new OrderBook(key.InstrumentId);
    bool complete = true;                             // false once a Gap sits inside the window
    while (cursor.MoveNext())
    {
        var item = cursor.Current;
        switch (item.Marker)
        {
            case DepthTapeMarker.Event:
                var evt = item.Event;
                book.ApplyMbo(in evt);                // OrderRemoved fires with the entry's resting time
                break;
            case DepthTapeMarker.Reset:
            case DepthTapeMarker.ReimageBegin:
                book.Clear();                         // everything before is superseded
                break;
            case DepthTapeMarker.Gap:
                complete = false;                     // lifecycles across this point are unknowable
                break;
        }
    }
    // complete == true: every order that appeared in the window has a certain fate.
}