Most NinjaScript backtests fail quietly. The code compiles, the strategy runs, an equity curve appears — and none of it describes what would have happened in the market. This article walks through five concrete mistakes that corrupt backtest results: intrabar repaint, missing stops, empty catch blocks, unsynced multi-series access, and missing CurrentBar guards. Each comes with the fix. It is educational only and is not trading advice; nothing here implies any profitability or result.
The backtests that lie almost always fail for one of a small set of reasons: the strategy reads intrabar data historically that it could not have known in real time (repaint), it has no protective stop so historical fills are unrealistically forgiving, it swallows exceptions in an empty catch so errors never surface, it reads a secondary data series that is not aligned to the primary bar, or it accesses barsAgo history that does not exist yet and either throws or gets silently clamped. Every one of these compiles and runs. That is what makes them dangerous — there is no red text telling you the numbers are fiction. The rest of this article takes them one at a time. For the broader theory of why validated systems fall apart, see [why most backtests fail](/learn/why-most-backtests-fail).
Repaint happens when your logic makes a decision using data that was not final at the moment of that decision — most commonly by reading the current, still-forming bar under Calculate.OnEachTick and treating it as settled. On historical data the bar is already complete, so the backtest sees the finished value; live, that value moves tick by tick. The backtest looks clean and live does not match it.
// WRONG: decides on the current forming bar's close under OnEachTick
protected override void OnBarUpdate()
{
if (High[0] > High[1] && Close[0] > Open[0])
EnterLong(); // historically Close[0] is final; live it is not
}The fix is to make decisions on closed bars. Use Calculate.OnBarClose, or if you need OnEachTick for order management, gate the entry decision on the prior, completed bar and use IsFirstTickOfBar:
// CORRECT: decide on the last closed bar
protected override void OnBarUpdate()
{
if (CurrentBar < 1) return;
if (High[1] > High[2] && Close[1] > Open[1])
EnterLong();
}The distinction between repaint and honest calculation is covered in depth in [repainting](/learn/repainting), and reading a bar's final value before it is final is a form of [look-ahead bias](/learn/look-ahead-bias).
A strategy with no protective stop is not just riskier — it produces a misleading backtest. Without a stop, adverse excursions that would have taken you out in live trading instead ride until your logical exit, so the historical trade list shows outcomes a real account with real drawdown limits would never have reached. The equity curve is smoother than reality.
Set stops and targets from the strategy, ideally in State.Configure when they are static, or per-entry when dynamic:
protected override void OnStateChange()
{
if (State == State.Configure)
{
SetStopLoss(CalculationMode.Ticks, 20);
SetProfitTarget(CalculationMode.Ticks, 40);
}
}For an ATR-based stop, set it before or at the moment you submit the entry so the protective order is registered against that position:
protected override void OnBarUpdate()
{
if (CurrentBar < 15) return;
double stopTicks = atr[0] / TickSize * 2.0;
SetStopLoss(CalculationMode.Ticks, stopTicks);
if (Close[0] > ema[0])
EnterLong();
}A stop is not only risk management; it is what makes the historical fill logic resemble the live fill logic.
This one hides all the others. An empty or silent catch means an exception thrown mid-bar — a null indicator, a bad index, a divide-by-zero — vanishes, and the strategy limps forward as if nothing happened. Your backtest completes, but some bars silently skipped their logic.
// WRONG: the error disappears and so does your logic on that bar
try
{
double ratio = someValue / divisor;
if (ratio > threshold) EnterLong();
}
catch { }The fix is to not wrap trading logic in a catch-all at all. Guard the actual precondition instead, and if you must catch, log it via Print so it appears in the NinjaScript Output window:
// CORRECT: guard the real condition, surface anything unexpected
if (divisor != 0)
{
double ratio = someValue / divisor;
if (ratio > threshold) EnterLong();
}If an exception is genuinely possible and recoverable, catch the specific type and Print it. A backtest that ran to completion while silently eating exceptions is worse than one that crashed — the crash at least tells you the truth.
These two travel together because both are about accessing data that is not there. When you add a secondary series with AddDataSeries, OnBarUpdate fires for every series, and you must route logic with BarsInProgress so you do not treat a secondary-series update as a primary-series bar:
// CORRECT: only act on the primary series; read secondary by index
protected override void OnBarUpdate()
{
if (BarsInProgress != 0) return; // primary only
if (CurrentBars[0] < 20 || CurrentBars[1] < 20) return; // both loaded
double primaryClose = Close[0];
double secondaryClose = Closes[1][0]; // secondary via Closes[series]
if (primaryClose > secondaryClose) EnterLong();
}Two failures hide here. First, without the BarsInProgress != 0 guard, code meant for the primary bar runs on secondary updates too, corrupting counts and signals. Second, without a CurrentBar (or CurrentBars[i]) guard, a barsAgo reference like Close[20] on bar 3 reaches into history that does not exist:
// WRONG: reaches back further than the data goes
protected override void OnBarUpdate()
{
if (Close[0] > SMA(Close, 50)[0]) EnterLong(); // throws early in the series
}The guard if (CurrentBar < 50) return; fixes it. Different timeframes also do not align bar-for-bar, so never assume Closes[1][0] corresponds to the same clock time as Close[0]; the free [NinjaScript validator](/ninjascript) checks both the BarsInProgress routing and the CurrentBar guards for exactly these reasons.
The pattern across all five mistakes is the same: the code runs, so nothing warns you. Repaint and no-stop inflate historical performance; empty catches and missing guards corrupt it silently. None of them announce themselves in a green compile.
A workable process: run with Calculate.OnBarClose first and only move to OnEachTick when you understand exactly why; always attach a stop before trusting a historical trade list; never wrap trading logic in a bare catch; guard every barsAgo access with a CurrentBar check and every multi-series script with BarsInProgress routing. Then run the strategy through the free [NinjaScript validator](/ninjascript), which statically flags intrabar repaint, missing stops, silent catches, unsynced series, and missing guards before you commit capital to a number that was never real.
This article is educational only. It describes NinjaScript API behavior and common defects; it does not constitute trading advice and makes no claim about profitability, win rate, or returns.
Educational only — not financial advice. Trading involves substantial risk of loss.