Live ticks & depth

Venue plugins

Live ticks & depth

Streaming trades and quotes through the tick source, and delivering L2 / market-by-order books into the host's one depth ingest plane.

The tick stream

Live trades ride ITickSource (namespace SabrTrader.Pipeline.Runtime), published as IDataProvider.LiveTicks. One subscription per instrument, disposed by the consumer. Quotes ride the same stream, distinguished by TickFlags.

ITickSource.cspublic interface ITickSource : IDisposable
{
    IDisposable Subscribe(string instrumentId, Action<Tick> onTick);

    // Exchange-routed venues override this; single-feed venues inherit the default.
    IDisposable Subscribe(string instrumentId, string? exchange, Action<Tick> onTick)
        => Subscribe(instrumentId, onTick);
}
  • Deliver one instrument's ticks from one thread at a time — per-instrument ordering follows your delivery. Venue dispatchers keyed by symbol satisfy this by construction.
  • Ref-count your venue-side subscription: several charts may subscribe to the same instrument, and the last dispose is what releases the venue stream.
  • The handler runs on your thread. It is the platform's job to be fast, but never hold a venue lock across the callback.

Quotes, summaries & open interest

Bid/ask quotes are ticks with the quote flags set — there is no second surface for them. Daily statistics have one: IMarketSummaryFeed is THE surface for settlement, previous close, official open and the daily OHLCV a venue publishes. Open interest and fundamentals have their own optional feeds.

Surface Serves
IMarketSummaryFeed MarketSummary Settlement / previous close / official daily statistics. Declared as VenueCapabilities.MarketSummary.
IOpenInterestFeed OpenInterest The venue's outstanding-contract count, a scalar per instrument. Stamped onto the forming bar's OI channel.
IFundamentalsFeed Fundamentals Beta / EPS / dividends / market cap. Market Analyzer columns render blank without it.
Settlement is summary-only. There is no settlement tick flag. A venue that receives settlement or previous-close on its tick wire routes it into IMarketSummaryFeed; the consumer precedence chain reads summary first and derives from bars only when the venue publishes nothing.

The hosted depth plane

Depth is the one place the seam inverts. A venue does not build an order book: it injects raw book events into a feed the host creates, and exposes that same object as its Depth / Mbo surface. Sequencing, the shared canonical book, shard pumps and subscriber fan-out all happen behind Inject, identically for every venue.

Venues never see rings, sequencers or the book hub. That is deliberate: it is the same ingest path at every rate, from a slow REST-polled book to an exchange-rate MBO storm, so there is no "standard path" and "fast path" to choose between.

IMarketDepthHost.csnamespace SabrTrader.Pipeline.MarketDepth;

public interface IMarketDepthHost
{
    IHostedDepthFeed CreateDepthFeed(IDepthUpstream upstream);
    IHostedMboFeed   CreateMboFeed(IMboUpstream upstream);
}
graph LR
  W["Venue wire
(socket / REST)"] -->|Inject(in evt)| F["IHostedDepthFeed
IHostedMboFeed"] F -->|sequence · apply · fan out| BK["Shared book + subscribers"] F -->|OnSubscribe / OnUnsubscribe| U["IDepthUpstream
IMboUpstream"] F -->|Reimage / FailClosed| U U --> W

The plane arrives as context.Depth. It is nullable — a headless host runs without a depth plane — and a venue whose context.Depth is null simply exposes no Depth/Mbo surfaces.

Aggregated L2 depth

Create one feed per venue book stream, implement the upstream, and publish the feed as IDataProvider.Depth. The host calls OnSubscribe when the FIRST platform consumer for an instrument arrives and OnUnsubscribe when the LAST one leaves — the ref-counting is done for you.

Hosted L2public interface IHostedDepthFeed : IMarketDepthFeed
{
    void Inject(in DepthUpdate update);   // host stamps SequenceNumber; leave it 0
    void Reset();                         // scoped to THIS feed's instruments
}

public interface IDepthUpstream
{
    void OnSubscribe(string instrumentId);
    void OnUnsubscribe(string instrumentId);
}
MyVenueDepth.cs_depthFeed = context.Depth!.CreateDepthFeed(new DepthUpstream(this));

// …on the wire thread. SequenceNumber stays 0 — the host stamps it.
_depthFeed.Inject(new DepthUpdate(
    SequenceNumber: 0,
    ExchangeTimestampUtc: timestampUtc,
    InstrumentId: instrumentId,
    Side: Side.Bid,
    Action: DepthAction.Update,
    Price: price,
    Size: size));

private sealed class DepthUpstream(MyVenueSession owner) : IDepthUpstream
{
    // Host threads: queue venue I/O, never perform it inline.
    public void OnSubscribe(string instrumentId)   => owner.EnqueueSubscribeDepth(instrumentId);
    public void OnUnsubscribe(string instrumentId) => owner.EnqueueUnsubscribeDepth(instrumentId);
}

Market-by-order

An L3 feed adds the image bracket and the directional health calls. MboEvent.OrderId must be the venue's exchange order id, stable across Modify including a price move — the book's identity depends on it.

Hosted MBOpublic interface IHostedMboFeed : IMarketByOrderFeed
{
    void Inject(in MboEvent evt);

    void BeginReimage(string instrumentId);                  // clear + HOLD presentation
    void CompleteReimage(string instrumentId);               // image complete, resume
    void FailImage(string instrumentId, string reason);      // image failed venue-side

    void Reset();                                            // scoped to THIS feed
}

public interface IMboUpstream
{
    void OnSubscribe(string instrumentId);
    void OnUnsubscribe(string instrumentId);
    void Reimage(string instrumentId);                       // host asks for a fresh image
    void FailClosed(string instrumentId, string reason);     // host gave up on this book
}
No member has a default implementation, on purpose. A silently-defaulted Reimage is precisely how heal requests went unanswered before this seam existed. A venue that genuinely cannot re-image writes the empty method where a reviewer can see it — and documents why.

The re-image protocol

Any time the venue is about to replay a book image — the initial subscribe snapshot, a reconnect rebuild, or an honoured Reimage request — bracket it. Between BeginReimage and CompleteReimage the host clears the book and HOLDS presentation, so consumers never render a half-built ladder. The bracket is re-entrant: calling BeginReimage while a rebuild is already running keeps the partial image rather than re-clearing it.

Image bracketpublic void OnSubscribe(string instrumentId)
{
    // Bracket BEFORE the stream starts, so the first event is inside the image.
    _mboFeed.BeginReimage(instrumentId);
    _client.SubscribeMbo(instrumentId);
}

// …when the venue signals its snapshot finished:
_mboFeed.CompleteReimage(instrumentId);

// …when the venue's rebuild fails:
_mboFeed.FailImage(instrumentId, error.Code);

FailImage is the escape hatch, and it matters more than it looks: it releases the rebuild hold AND stops the host treating MBO as authoritative for that instrument, so the venue's aggregated depth (if it has any) can serve the book instead. Without it, a failed image leaves a wedged DOM. A venue with no snapshot-complete signal at all should not open a bracket it cannot close — document that instead, and let the book converge.

Reset & fail-closed

Reset() invalidates everything built from this feed: it clears the shared books of this feed's instruments (empty IS the truth after a real disconnect) and raises SubscriptionReset to this feed's consumers. It is scoped — it can never touch another venue's books. Call it on connection loss, on a venue-issued book clear, or on any event that voids resting levels.

FailClosed is the opposite direction: the host telling you it has given up on an instrument's MBO book and already stopped presenting it. Stop the MBO stream if you can, and fall back to aggregated depth for that instrument.

The perf contract

Normative, not advisory. Inject is synchronous, returns void, takes the event by in, allocates nothing, never blocks, and is safe from any venue thread. No LINQ, no closures, no string formatting, no logging on the injection path.
  • Per-instrument ordering follows the caller: deliver one instrument's events from one thread at a time.
  • A stalled platform consumer can never backpressure your receive thread. Overload is absorbed by the host's coalescing rings, and sustained loss surfaces as an IMboUpstream.Reimage request — never as a blocked Inject.
  • Upstream calls (OnSubscribe, Reimage, …) arrive on host threads and must return quickly. Queue the venue I/O; never perform it inline.
  • Dispose your feeds on session teardown.

Flow diagnostics

Two diagnostic sinks stayed on the venue side of the seam on purpose, because a wedged book is almost always diagnosed from the venue outward. Both are optional, both are wired by the host into the activity log, and neither is on any per-event path.

Type Reports
MboFlowMonitor Live MBO flow per layer (sequencer → hub → feed → view) and per UI consumer. Logs consumer-stall when a consumer that was receiving data goes quiet — so a post-connect freeze is visible without per-event noise.
MboConnectionDiagnosticLog The end-to-end MBO connection lifecycle, venue subscribe through to the SuperDOM and chart consumers.