◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / NinjaScript Repainting: Why OnEachTick Strategies Lie in Backtests
NinjaScript

NinjaScript Repainting: Why OnEachTick Strategies Lie in Backtests

Repainting is when a NinjaScript signal appears, disappears, or moves as new ticks arrive inside a still-forming bar. Under Calculate.OnEachTick a live strategy can fire and un-fire an entry mid-bar, yet the Strategy Analyzer replays only closed-bar data and never reproduces it. This article is educational only and is not trading advice; nothing here implies any signal is profitable.

What is repainting in NinjaScript?

Repainting is when a value your strategy already showed changes as later ticks arrive within the same, still-forming bar. In NinjaScript this happens because OnBarUpdate() can run many times on the current bar before that bar closes. If your logic reads the developing price of CurrentBar and acts on it, the condition can be true on one tick and false on the next. A signal drawn or acted on at 10:00:03 may be gone by 10:00:59 when the bar finally closes.

The developing bar is not final data. Under Calculate.OnEachTick, Close[0], High[0], and Low[0] all mutate tick by tick until the bar closes. An indicator that plots off Close[0] will redraw its most recent point on every tick; a strategy that enters off it will act on a price that has not settled. That mutation is the mechanical root of repainting.

The honest framing: repainting is not automatically a bug. A live dashboard that updates intrabar is doing exactly what a trader asked for. It becomes a measurement problem when your backtest cannot see the same intrabar churn your live order flow will experience. You can validate whether a specific strategy repaints with the free [/ninjascript](/ninjascript) validator before you trust any backtested result.

Why does Calculate.OnEachTick let a signal fire and un-fire?

Calculate.OnEachTick calls OnBarUpdate() on every incoming tick. Consider an entry that checks whether the current close has crossed above a moving average:

// WRONG: evaluates a developing bar every tick
protected override void OnBarUpdate()
{
    if (CurrentBar < 20) return;
    // Close[0] is the LIVE, un-closed price under OnEachTick
    if (Close[0] > SMA(20)[0])
        EnterLong();
}

At 10:00:05 price ticks above the SMA, the condition is true, and EnterLong() submits. Ten seconds later price ticks back below the average. On the next OnBarUpdate() the condition is now false. The signal that appeared has effectively un-fired: the state that justified the entry no longer exists, and if you were merely drawing an arrow you would erase it.

This is repainting in its purest form. The bar has not closed, so nothing about it is committed. Only when the bar closes does Close[0] stop moving. Any decision made before that instant was made on provisional data. See [/learn/repainting](/learn/repainting) for the broader taxonomy of how this manifests across platforms.

Why can't the Strategy Analyzer reproduce it?

The Strategy Analyzer backtests on historical bar data by default. For each historical bar it typically calls OnBarUpdate() once, with Close[0] already equal to the bar's final settled close. There is no intrabar sequence of ticks to replay, so the mid-bar wobble that fired and un-fired your live entry simply never happens in the test.

The result is a backtest that only ever sees the clean, closed-bar version of every condition. Your live strategy acted on a fleeting intrabar tick; your backtest acted on the tidy final value. They are measuring two different strategies. The backtest can look stable and orderly while the live version is entering and reversing on noise you never modeled.

This is a specific, mechanical case of the general problem covered in [/learn/why-most-backtests-fail](/learn/why-most-backtests-fail): the test environment does not reproduce the live decision surface. It is distinct from look-ahead bias, but related — both come from the backtest seeing information in a different form than live execution does. Tick Replay in the Analyzer can narrow the gap by feeding historical ticks through OnBarUpdate(), but it is off by default and many published results never used it.

How do I tell whether my strategy actually repaints?

Ask one question: does any entry, exit, or plotted value depend on Close[0], High[0], or Low[0] of the still-forming bar? If yes, and you run Calculate.OnEachTick, it repaints. The fix is not to abandon tick updates — it is to commit your decisions to closed bars while still letting the rest of your code update live.

The standard gate is IsFirstTickOfBar, which is true only on the first tick of a new bar. That first tick carries the just-closed prior bar's final values in Close[1], High[1], and Low[1], so a condition evaluated there is reading committed data:

protected override void OnBarUpdate()
{
    if (CurrentBar < 20) return;
    if (IsFirstTickOfBar)
    {
        // Close[1] is the just-closed bar: settled, will not change
        if (Close[1] > SMA(20)[1])
            EnterLong();
    }
}

The cleaner alternative is Calculate.OnBarClose, which only calls OnBarUpdate() after a bar closes. Both approaches, and the trade-offs between them, are covered in the companion articles. Before publishing any backtested result, run the strategy through the free [/ninjascript](/ninjascript) validator to confirm the live and historical decision surfaces match. This is educational material, not a recommendation to trade any particular strategy.

Key takeaways

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

Catch the bug that compiles.Run auditGet Pro