How NinjaScript's barsAgo indexing works (0 = current forming bar, 1 = previous bar), how CurrentBar gives the absolute position, and the off-by-one and future-index traps that quietly corrupt backtests. This article is educational and describes API behavior only; it makes no claim about profitability and is not trading advice.
In NinjaScript, barsAgo is a relative offset into a price series counted backward from the bar currently being processed. Close[0] is the current bar, Close[1] is the bar immediately before it, Close[2] two bars before, and so on. The index is not a fixed calendar position; it slides forward every time a new bar forms. So Close[1] inside OnBarUpdate always means "the bar just before the one I am evaluating right now," whatever the clock says.
Every standard series is indexed this way: Open[barsAgo], High[barsAgo], Low[barsAgo], Close[barsAgo], Volume[barsAgo], and Time[barsAgo]. The critical subtlety is bar 0. When Calculate is OnEachTick or OnPriceChange, Close[0] is the live, still-forming bar, and its value changes tick by tick until the bar closes. When Calculate is OnBarClose, OnBarUpdate only fires after a bar has finished, so Close[0] is effectively the last completed bar. Knowing which mode you are in tells you whether bar 0 is settled or still moving, and that single distinction is the root of most indexing bugs.
CurrentBar is the absolute index of the bar being processed, counted from the first bar loaded (bar 0) forward. It increments by one each time a new primary bar forms and never slides. Where barsAgo answers "how far back from now," CurrentBar answers "which bar number am I on out of the whole series."
The two are related by a simple identity: the absolute index of the bar n bars ago is CurrentBar - n. This matters because barsAgo can only look backward within the data that exists. If you are on CurrentBar == 3 and you ask for Close[10], you are requesting a bar that was never loaded, and NinjaScript throws an index-out-of-range error that halts the script.
That is why guarding on CurrentBar is the standard defensive pattern:
protected override void OnBarUpdate()
{
// Need at least 20 completed bars before referencing Close[20]
if (CurrentBar < 20)
return;
double twentyBarsBack = Close[20];
}Always gate your deepest lookback with a CurrentBar check so early bars in the series cannot crash the strategy.
barsAgo is defined only for zero and positive integers. A negative offset like Close[-1] would mean "one bar into the future," which does not exist at the moment OnBarUpdate runs, so NinjaScript rejects it. There is no legitimate way to read a future bar during live processing, and any code that appears to do so is either an error or a look-ahead leak.
The dangerous version of this bug is subtle. In backtests, all historical data is already present on disk, so a developer may be tempted to reach forward by mixing series or mis-indexing. If your logic ever depends on information that would not have been available at decision time, you have introduced [look-ahead bias](/learn/look-ahead-bias), and your results describe a strategy that could never have traded. The following is exactly what you must never write:
// WRONG: attempts to use a future bar's close to decide the current bar
if (Close[0] < Close[-1]) // Close[-1] is invalid; conceptually a future bar
EnterLong();Decisions on the current bar may reference bar 0 and older only. Reaching forward — even accidentally, through a misaligned secondary series — is the classic mechanism behind results that cannot be reproduced live.
The most common indexing mistake is treating Close[0] as a completed bar while running tick-by-tick. Under Calculate.OnEachTick, bar 0 is still forming; its high, low, and close move on every tick. A condition such as if (Close[0] > High[1]) can be true mid-bar, fire an entry, and then become false again before the bar closes as price retreats. On the chart afterward, the signal seems to "disappear" — the textbook symptom of [repainting](/learn/repainting).
The fix is to decide on closed data. Evaluate signals against bar 1 and older, or gate the work to the first tick of a new bar so bar 1 is the just-completed bar:
protected override void OnBarUpdate()
{
if (CurrentBar < 1)
return;
// Act once per bar, on confirmed (closed) values only
if (IsFirstTickOfBar)
{
// Close[1] is the bar that just completed
if (Close[1] > High[2])
EnterLong();
}
}IsFirstTickOfBar is true on the opening tick of each new bar, which is precisely when the previous bar has finalized. Anchoring signals to Close[1] there gives you a stable, non-repainting reference regardless of Calculate mode.
When you add a secondary series with AddDataSeries, each series keeps its own independent barsAgo count and its own CurrentBars element. Inside OnBarUpdate, BarsInProgress tells you which series triggered the current call: 0 is the primary series, 1 is the first added series, and so on. A frequent bug is reading Closes[1][0] from the secondary series while processing the primary series and assuming the two bars are time-aligned — they are not necessarily closed at the same instant.
protected override void OnStateChange()
{
if (State == State.Configure)
AddDataSeries(BarsPeriodType.Minute, 15); // becomes BarsInProgress == 1
}
protected override void OnBarUpdate()
{
// Guard each series independently
if (CurrentBars[0] < 1 || CurrentBars[1] < 1)
return;
if (BarsInProgress == 0)
{
// Primary bar logic; Closes[1][0] is the current 15-min bar,
// which may still be forming relative to this primary bar
double higherTfClose = Closes[1][0];
}
}Use CurrentBars[seriesIndex] (plural) to guard each series, and be explicit about BarsInProgress so you never cross-reference a still-forming higher-timeframe bar as though it were settled. This is one of the quieter sources of results that look great in a bar-based test but fall apart tick-by-tick — a pattern explored further in [why most backtests fail](/learn/why-most-backtests-fail).
The reliable way to know whether your indexing survives real time is to run the same logic two ways and compare. Under Calculate.OnBarClose, bar 0 is a completed bar, so a strategy that references only Close[0] and older should produce identical entries whether tested on historical bars or replayed tick by tick. If the tick-by-tick run diverges, some part of your code is reading the live bar 0 as though it were final.
A quick self-audit checklist: never place an order based on an unconfirmed Close[0] under OnEachTick; guard every lookback with a CurrentBar or CurrentBars[i] check; never use a negative index; and treat secondary-series bar 0 as potentially unfinished. When any of these is violated, the strategy typically prints signals that later move or vanish.
Because this class of bug is invisible to the naked eye, ForexCodes provides a free static analyzer that scans NinjaScript for exactly these patterns — unconfirmed bar-0 references, future-index access, and series-misalignment — before you ever fund an account. You can paste your code into the [free NinjaScript validator](/ninjascript) to flag indexing that would repaint. Nothing here is a promise about performance; correct indexing only ensures your test describes a strategy that could actually have traded.
Educational only — not financial advice. Trading involves substantial risk of loss.