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.
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.
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.
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.
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.
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.
Educational only — not financial advice. Trading involves substantial risk of loss.