Calculation & market data
Calculation & market data
Two independent opt-ins control how often your code runs: a calculation mode sets the
cadence of OnBarUpdate, and a separate flag delivers raw ticks. Plus depth, alerts and
sessions.
On this page
Two separate knobs
Calculate (a CalculationMode) and ProcessesMarketData (a bool)
are independent. The first chooses how often OnBarUpdate fires; the second opts into raw
OnMarketData tick delivery. Pick either, both, or neither — an indicator that opts
into nothing pays zero per-tick dispatch overhead.
- An indicator wanting per-tick OHLC recalculation:
OnEachTick+ leaveProcessesMarketData = false. - An orderflow indicator wanting raw bid/ask trades:
OnBarClose+ProcessesMarketData = true. - A footprint indicator wanting both: set both.
Set them in your constructor (the setters are protected); the value must be stable for the indicator's lifetime.
Calculation mode
| Mode | Fires OnBarUpdate
|
|---|---|
OnBarClose (default) |
Once per closed bar. Lowest CPU; the right choice for SMA/EMA/RSI and other closed-bar smoothers. |
OnPriceChange |
Whenever the bar's close changes (skips repeat-price ticks). |
OnEachTick |
On every tick affecting OHLC. Highest CPU; for tick-precision VWAP, footprint, volume-reactive logic. |
Regardless of mode, the closing OnBarUpdate for a bar always fires exactly once; the
intra-bar modes simply add updates leading up to it. Inside the callback, read
ctx.IsBarClosed to branch close vs intra-bar logic, ctx.IsFirstTickOfBar to
roll your per-bar accumulators, and ctx.IsFirstBarOfSession to reset daily state.
a per-tick indicator[IndicatorInput(IndicatorInputKind.Bars)]
public sealed class MyVwap : IndicatorBase
{
public MyVwap(IIndicator? parent = null) : base(parent)
{
Calculate = CalculationMode.OnEachTick; // recompute on every tick
AddPlot(new Plot("VWAP", _vwap, PlotStyle.Line, new ChartColor(255, 193, 7), 1.5));
}
public override void OnBarUpdate(IIndicatorContext ctx)
{
if (ctx.IsFirstBarOfSession) // reset session accumulators at the open
_cumPV = _cumVol = 0;
Bar b = ctx.Bars(0)[0];
double typical = (b.High + b.Low + b.Close) / 3.0;
_cumPV += typical * b.Volume;
_cumVol += b.Volume;
double vwap = _cumVol > 0 ? _cumPV / _cumVol : double.NaN;
if (ctx.IsFirstTickOfBar) _vwap.Append(vwap); // new bar -> new slot
else _vwap.UpdateLast(vwap); // same bar -> rewrite the tail
}
}
If you write a custom gate, CalculationCadence.ShouldFire(mode, isClosed, closeChanged) is
the single source of truth the runtime itself uses — reuse it rather than re-deriving the rules.
Raw market data
Set ProcessesMarketData = true to receive OnMarketData(in Tick tick,
IIndicatorContext ctx) on every live tick. A Tick (from
SabrTrader.Pipeline.Ticks) carries Price, Size,
ExchangeTimestampUtc and a Flags bitmask — filter on it, because a tick
can be a trade, a bid/ask quote, or an informational print.
handling raw tickspublic MyTape(IIndicator? parent = null) : base(parent)
{
Calculate = CalculationMode.OnBarClose;
ProcessesMarketData = true; // opt into OnMarketData
}
public override void OnMarketData(in Tick tick, IIndicatorContext ctx)
{
if ((tick.Flags & TickFlags.Trade) == 0) return; // ignore pure quote updates
bool buyAggressor = (tick.Flags & TickFlags.AtAsk) != 0;
_delta += buyAggressor ? tick.Size : -tick.Size;
}
OrderFlowBar via OnDataLoaded instead (see
Orderflow). On the live path this fires at the full tick rate
(thousands per second on a busy instrument), so keep it allocation-free and never Print
per tick.Depth & market-by-order
Depth and trades are separate streams. Set ProcessesMarketDepth = true to receive the
order-book callbacks: OnMarketDepth(in DepthUpdate update, ctx) (aggregated Level 2),
OnMarketByOrder(in MboEvent evt, ctx) (Level 3), OnMarketByOrderLoss(long lostEvents, ctx)
and OnOrderBookLifecycle(OrderBookLifecycleEvent evt, ctx). The chart has already applied each
event to the shared ctx.OrderBook before your callback runs, so a read of the book inside the
callback sees consistent state. All four are default no-ops, so a depth-only indicator implements just
the ones it needs. The callbacks are serialised per indicator: no two of them ever run at the same time.
What is delivered, and what is not
The shared book is complete. The callbacks are not. On a live chart the host drains the instrument's
queued MBO events once per worker pass and hands your indicator only the newest one; the events it
skipped were still applied to the book. Every skipped event is announced through
OnMarketByOrderLoss before the event that follows it, with an exact count for the
skipped events plus anything the delivery ring itself discarded under load (that part may be attributed
one pass early, never twice). Under a quiet market most passes deliver one event and no loss notice;
under a busy one you will see loss notices constantly. That is the design, not a fault: read the book
(ctx.ReadOrderBook) for state, and treat OnMarketByOrder as the cue to do so.
Two consequences follow. An indicator can support sampled-flow and current-book studies from the
callbacks alone. It cannot certify complete order lifecycles or queue persistence from them, and no
amount of waiting makes the deltas of the book's MboAddCount / MboModifyCount /
MboCancelCount line up with its callback count: those counters record apply outcomes on a book
shared by every consumer of the instrument (see IOrderBook).
For the complete record use the recorded tape through ctx.RecordedDepth
(IRecordedDepthCatalog), which carries
every event the recorder received plus the markers that say where a reset, a venue re-image or a capture
gap sits.
One more shape to know: an indicator that joins an instrument already streaming first receives the
current book replayed as MboAction.Add events stamped with the join time. Those are the orders
already resting, not wire events; they are never applied to the book (it already holds them) and they
move none of its counters.
Book lifecycle
State you derive from the callbacks is only as good as the book it came from, and the book is cleared
and rebuilt during a session: on a feed reset, and whenever the venue re-images the instrument after a
recovery. OnOrderBookLifecycle tells you when. Reset means the book was cleared
and nothing is promised; RebuildStarted means it was cleared and the venue's fresh image is on
its way; RebuildCompleted means the image is applied and the book is consistent again, which is
the moment to re-derive from ctx.ReadOrderBook; DepthModeChanged means
ctx.OrderBook.DepthMode flipped between PerOrder and Aggregated, and
per-order features stop describing the venue's book on the way down. The notice runs on the same
serialised path as the event callbacks but is not ordered against events still queued for delivery, so a
few pre-transition events may still arrive after RebuildStarted; that is why re-deriving at
RebuildCompleted beats tracking the image events yourself.
a DOM indicatorpublic MyDom(IIndicator? parent = null) : base(parent)
{
ProcessesMarketDepth = true; // opt into the order-book callbacks
}
public override void OnMarketByOrder(in MboEvent evt, IIndicatorContext ctx)
{
// The newest event of this pass. The book already has every event applied.
ctx.ReadOrderBook(book => Sample(book));
}
public override void OnMarketByOrderLoss(long lostEvents, IIndicatorContext ctx)
{
_skippedSinceOpen += lostEvents; // honest denominator for anything counted from callbacks
}
public override void OnOrderBookLifecycle(OrderBookLifecycleEvent evt, IIndicatorContext ctx)
{
switch (evt)
{
case OrderBookLifecycleEvent.Reset:
case OrderBookLifecycleEvent.RebuildStarted:
_derived.Clear(); // built from a book that no longer exists
break;
case OrderBookLifecycleEvent.RebuildCompleted:
ctx.ReadOrderBook(book => Rebuild(book)); // the image is in; start again from the book
break;
case OrderBookLifecycleEvent.DepthModeChanged:
_perOrder = ctx.OrderBook?.DepthMode == OrderBookDepthMode.PerOrder;
break;
}
}
Alerts
Raise a user-facing alert with ctx.Alert(message) for the common case, or the full
overload for control over de-duplication and sound:
alertsctx.Alert("RSI crossed above 70"); // simple; message is also the rearm id
ctx.Alert(
id: "rsi-overbought", // rearm key — throttles repeats
message: "RSI overbought",
soundPath: "Alert2.wav", // bare filename resolves to the host sounds folder
severity: AlertSeverity.Warning, // Info / Warning / Critical
rearmSeconds: 60); // don't re-fire this id for 60s
ctx.IsLive yourself, though doing so for other live-only side effects is good
practice.Trading hours & sessions
For session-aware indicators (daily VWAP, opening range, initial balance) the context exposes the
instrument's trading calendar. ctx.TradingHours is the bound
TradingHoursTemplate (or null = 24/7), and ctx.CreateSessionIterator()
returns a stateful ISessionIterator — the equivalent of NinjaTrader's
new SessionIterator(Bars). Create it once in OnInit/OnDataLoaded
and keep it for the series' life.
session iterationusing SabrTrader.Pipeline.Indicators.TradingHours;
private ISessionIterator? _sessions;
public override void OnInit(IIndicatorContext ctx)
{
base.OnInit(ctx);
_sessions = ctx.CreateSessionIterator(); // null on a stub context; production always returns one
}
public override void OnBarUpdate(IIndicatorContext ctx)
{
var t = ctx.Bars(0)[0].Start;
if (_sessions is not null && _sessions.IsNewSession(t, includesEndTimestamp: false))
{
_sessions.GetNextSession(t, includesEndTimestamp: false);
ResetDailyAccumulators();
}
}
ctx.IsFirstBarOfSession is the simpler signal when all you need is "reset at the open".
Chart session iterators take Bar.Start / Bar.End (the chart timeline).
ctx.DisplayTimeZone is the zone used to stamp that timeline and to convert an
external UTC instant via ChartDisplayTime.FromUtc.