Orderflow & depth
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.
On this page
- OrderFlowBar · OrderFlowBarStats · OrderFlowResolution · OrderFlowWindowRequest · VolumeProfileWindow · IOrderFlowWindowSource
- IMarketDepthFeed · IMarketByOrderFeed · IOrderBook · OrderBook · OrderBookSnapshot · PriceLevel · OrderBookEntry
- DepthUpdate · DepthAction · MboEvent · MboAction · Side · OrderBookDepthMode · OrderBookLifecycleEvent
- IRecordedDepthCatalog · DepthTapeMarker · Tape example
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. |
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;
}
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.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. |
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);
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. |
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.
}