Calculation & market data

Indicators

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.

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 + leave ProcessesMarketData = 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;
}
OnMarketData never fires during backfill. Even when the historical load includes tick data, raw ticks are not replayed — orderflow indicators receive the pre-aggregated 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
Alerts are live-only by design. The host routes them through its sinks only in a live context, so a historical/backtest pass never replays thousands of alerts — you don't need to gate them on 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.