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
TradeStrike.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
OnMarketDepth(in DepthUpdate update, ctx) (aggregated Level 2) and
OnMarketByOrder(in MboEvent evt, ctx) (Level 3). The chart has already applied each
event to the shared ctx.OrderBook before your callback runs, so a render-time read of the
book sees consistent state. Both are default no-ops, so a depth-only indicator implements just the one
it needs.
a DOM indicatorpublic MyDom(IIndicator? parent = null) : base(parent)
{
ProcessesMarketDepth = true; // opt into depth / MBO
}
public override void OnMarketDepth(in DepthUpdate update, IIndicatorContext ctx)
{
IOrderBook? book = ctx.OrderBook; // already updated for this event
if (book is null) return;
// ... read the book; tick size is ctx.TickSize, point value ctx.PointValue ...
}
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 TradeStrike.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].StartUtc;
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".
ctx.DisplayTimeZone gives the chart's display zone for any time-of-day bucketing.