◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / Look-Ahead Bias in NinjaScript (CurrentBar + N and Beyond)
NinjaScript

Look-Ahead Bias in NinjaScript (CurrentBar + N and Beyond)

Look-ahead bias in NinjaScript happens when your strategy reads data from bars that had not yet closed at the historical moment it claims to trade. This guide shows the exact API patterns that leak future data — future indexing, negative barsAgo, intrabar peeking — and how to detect and remove them. It is educational material about code correctness, not trading advice, and nothing here implies any strategy will be profitable.

What is look-ahead bias in NinjaScript?

Look-ahead bias is when your strategy uses information that would not have existed yet at the point in time it is supposedly acting on. In NinjaScript the classic form is reaching forward in the price series — reading a bar that, historically, had not printed when the current bar closed. Your backtest then trades on the future, and the equity curve it produces is fiction.

The series indexing convention is where this starts. Close[0] is the current bar, Close[1] is one bar ago, Close[barsAgo] steps backward as the index grows. barsAgo is meant to be zero or positive. Any construction that resolves to a negative offset, or that indexes an absolute bar number greater than CurrentBar, is a peek into the future.

Here is the shape of the mistake:

// WRONG: Bars.GetClose(CurrentBar + 1) reads the NEXT bar's close
protected override void OnBarUpdate()
{
    if (CurrentBar < 1) return;
    double nextClose = Bars.GetClose(CurrentBar + 1); // future data
    if (nextClose > Close[0])
        EnterLong();
}

That code "knows" the next bar closes higher before it decides to buy. It is not a strategy; it is a time machine. The [look-ahead bias primer](/learn/look-ahead-bias) covers the concept across engines — this article is the NinjaScript-specific version.

How does CurrentBar + N leak the future?

CurrentBar is the absolute index of the bar currently being processed, counting from 0 at the first bar of the series. Bars.GetClose(int barIndex), Bars.GetOpen, Bars.GetHigh, Bars.GetLow, and Bars.GetTime all take an absolute index, not a barsAgo offset. So Bars.GetClose(CurrentBar) is today's close and Bars.GetClose(CurrentBar - 3) is three bars back — both legitimate.

The moment you write CurrentBar + anything, you are asking for a bar that, during a historical run, does not exist yet. On the last processed bar it will throw or return stale data; on interior bars it silently returns a value the strategy could never have known.

// WRONG: absolute index beyond CurrentBar
double future = Bars.GetHigh(CurrentBar + 5);

// CORRECT: only reach backward
if (CurrentBar >= 5)
{
    double fiveBarsBack = Bars.GetHigh(CurrentBar - 5);   // absolute
    double sameThing    = High[5];                        // barsAgo form
}

High[5] and Bars.GetHigh(CurrentBar - 5) are equivalent and both safe. The rule is mechanical: with barsAgo-style accessors (High[n], Close[n]), n must be >= 0; with Bars.Get* absolute accessors, the index must be <= CurrentBar. Nothing you legitimately compute on the current bar requires a larger index.

What about negative barsAgo and other subtle peeks?

A negative barsAgo is the same bug wearing a different hat. Close[-1] asks for one bar in the future. It usually surfaces indirectly, through a computed offset that goes negative:

// WRONG: offset can resolve to a future bar
int offset = signalBar - CurrentBar; // negative when signalBar is ahead
double px = Close[offset];

Two more common leaks:

Using MAX, MIN, Swing, or similar over a window that includes bars not yet closed. Built-in indicators do not look ahead, but your wrapper around them can if you shift the output the wrong direction. A Swing(5).SwingHigh[0] value is only confirmed several bars after the pivot; treating the pivot bar itself as tradable at pivot time back-dates knowledge you did not have.

Reading a completed daily or session value on the intraday bars that built it. If you pull today's session high while today's session is still forming, you are using the finished value on bars that predate it. That is the multi-series case, covered in [Multi-Series Look-Ahead in NinjaScript](/ninjascript-multi-series-lookahead).

The safe mental model: on the bar you are processing, you may read that bar and every bar before it, and nothing else.

How do I detect look-ahead bias in my own code?

Three checks catch the large majority of cases before you ever run a backtest.

First, grep the source. Search for CurrentBar +, for [-, and for any index arithmetic feeding a series accessor. Every Bars.Get* call should be sanity-checked against CurrentBar. Every bracket index should be provably non-negative.

Second, guard your bar counts and let exceptions surface leaks. A proper guard reads backward only:

protected override void OnBarUpdate()
{
    if (CurrentBar < BarsRequiredToTrade) return;
    // safe: strictly backward-looking
    double momentum = Close[0] - Close[10];
    if (momentum > 0)
        EnterLong();
}

Third, run a shifted-data test. Compute your signal, then re-run with every input lagged by one bar. If results barely change, your logic is causal. If a one-bar lag destroys the edge, your "edge" was reading a bar it should not have.

The free [/ninjascript validator](/ninjascript) automates the static half of this — it flags future indexing, negative offsets, and intrabar-repaint patterns in your .cs file without you having to trace every accessor by hand. It is a linter for time-travel bugs, not a promise about performance. See also [Why Most Backtests Fail](/learn/why-most-backtests-fail) for how these leaks inflate results.

How do I fix it without breaking the strategy?

Fixing look-ahead bias almost always means delaying the trade by one bar, not deleting the signal. If your logic genuinely needs the next bar's close to confirm, then the honest version acts on the bar after that close is known.

// CORRECT: decide on the closed bar, act on the next OnBarUpdate
protected override void OnBarUpdate()
{
    if (CurrentBar < BarsRequiredToTrade) return;

    // Close[0] is the just-closed bar; this is fully known now
    bool breakout = Close[0] > High[1];
    if (breakout)
        EnterLong(); // fills on the next bar's open by default
}

With Calculate.OnBarClose, OnBarUpdate fires once per completed bar and Close[0] is that finished bar — a market order placed here fills on the next bar. That one-bar gap is not a tax to avoid; it is the truth of when you actually knew the signal.

If you must run Calculate.OnEachTick or OnPriceChange for responsiveness, gate any bar-close logic behind IsFirstTickOfBar so you only evaluate completed data, and never read Close[0] as if the forming bar were final. Get the causality right first, then optimize. A backtest that matches reality is worth more than a pretty one that cannot be traded. Everything here is educational and about code correctness only; none of it implies a given strategy makes money.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro