Reading bars & series

Indicators

Reading bars & series

How you read price data inside a callback: bars-ago indexing, the Bar fields, the cached price projections that save you materialising whole bars, and how to read safely during warmup.

The context is your data window

Every lifecycle hook receives an IIndicatorContext. It is your window onto the data: ctx.Bars(0) is the primary bar series, ctx.SeriesCount tells you how many series are subscribed, and ctx.BarsInProgress tells you which series fired the current callback. For convenience, IndicatorBase also exposes the resolved primary as the Bars property and the resolved scalar source as Input — both defaulted for you in base.OnInit.

Bars-ago indexing

Series use NinjaTrader-style bars-ago indexing: this[0] is the current bar (in-progress during live, most-recent-closed during history), this[1] the bar before, and so on. Count is the total number of bars ever observed.

reading barspublic override void OnBarUpdate(IIndicatorContext ctx)
{
    IBarSeries bars = ctx.Bars(0);

    Bar current  = bars[0];        // the bar that just closed
    Bar previous = bars[1];        // the one before it

    double range = current.High - current.Low;
    bool   up    = current.Close >= previous.Close;

    int lastIndex = ctx.CurrentBar;            // == bars.Count - 1, or -1 before any bar
}

ctx.CurrentBar is the index of the most recent bar on the firing series — equal to Bars(BarsInProgress).Count - 1 — and returns -1 when the series has no bars yet (warmup). For multi-series indicators, ctx.CurrentBars[n] gives the last-bar index of any local series independently (see Multi-timeframe).

The Bar struct

A Bar is a readonly record struct — allocation-free, passed by value. Note there is no single Time field: the open and close instants are split into StartUtc and EndUtc, and TickCount is first-class (load-bearing for tick and orderflow bars).

Bar.cspublic readonly record struct Bar(
    DateTime StartUtc,   // bar open time
    DateTime EndUtc,     // moment the bar closed (closing tick for non-time bars)
    double Open,
    double High,
    double Low,
    double Close,
    long Volume,
    long TickCount);
Timeline. Bars reaching an indicator are already normalized to the chart's display zone — their timestamps carry display-zone wall-clock re-tagged as DateTimeKind.Utc. If you ingest an external absolute-UTC source, convert it into that same timeline using ctx.DisplayTimeZone before comparing against bar times.

Cached price projections

Most indicators don't need a whole Bar — they want one column. IBarSeries exposes cached ISeries<double> views over each component, so you read a scalar stream without materialising a bar per access. Each view is a single cached instance per (bars, component) pair, so reference equality holds and your code reads naturally.

Projection Value
Close / Open / High / Low The raw OHLC components.
Volume Per-bar volume (projected to double for arithmetic).
Median (High + Low) / 2 — aka HL2.
Typical (High + Low + Close) / 3 — aka HLC3.
Weighted (High + Low + 2×Close) / 4.
OHLC4 (Open + High + Low + Close) / 4.
using a projection// Run on the median price instead of the close:
var sma = new Sma(this, input: ctx.Bars(0).Median, period: 20);

// Read a projection directly:
double typical0 = ctx.Bars(0).Typical[0];

IndicatorBase.Input defaults to Bars(0).Close — so an indicator whose maths reads Input automatically follows whatever the user picks in the "Input series" editor, including another indicator's output. That is exactly the projection mechanism under the hood.

Reading safely during warmup

Early in a session, or near the start of the backfill, a lookback can exceed the bars available. There are two tools. First, the platform-wide NaN convention: any series of double holds double.NaN at positions that aren't computed yet, the renderer skips NaN, and NaN propagates cleanly through arithmetic into a gap in the plot. Second, the non-throwing TryGet accessor.

warmup-safe readspublic override void OnBarUpdate(IIndicatorContext ctx)
{
    var close = ctx.Bars(0).Close;

    // Guard a lookback explicitly...
    if (close.Count < Period) { _output.Append(double.NaN); return; }

    // ...or read tentatively with TryGet.
    if (close.TryGet(Period - 1, out double oldest))
        _output.Append((close[0] - oldest) / Period);
    else
        _output.Append(double.NaN);
}

Zero-copy column spans

For vectorised or SIMD-style work over one column, the span accessors expose the underlying contiguous arrays with no per-bar materialisation. Because the series is backed by a ring buffer, a slice may wrap — so each accessor returns up to two spans in chronological order, and you concatenate them logically.

span accessors// Sum the last `n` closes with no allocations and no per-element bounds checks.
if (bars.TryReadCloseSpan(fromBarsAgo: 0, count: n,
        out ReadOnlySpan<double> first, out ReadOnlySpan<double> second))
{
    double sum = 0;
    foreach (double c in first)  sum += c;
    foreach (double c in second) sum += c;   // empty when the slice didn't wrap
}

The full set is TryReadCloseSpan, TryReadOpenSpan, TryReadHighSpan, TryReadLowSpan (all ReadOnlySpan<double>) and TryReadVolumeSpan (ReadOnlySpan<long>). Each returns false when the requested range exceeds the bars currently in the ring.