◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / Why Your NinjaTrader Backtest Doesn't Match Live
NinjaScript

Why Your NinjaTrader Backtest Doesn't Match Live

A NinjaTrader backtest and live trading diverge for a short list of concrete reasons: intrabar repaint, running with or without Tick Replay, optimistic fill assumptions, and code that behaves differently in the Historical versus Realtime state. This guide is a diagnostic checklist for finding which one is biting you. It is educational material about code and testing correctness, and makes no claim about profitability or expected results.

Why doesn't my NinjaTrader backtest match live trading?

When a backtest looks great and live trading does not, the cause is almost always one of four things: the strategy repaints intrabar, the backtest used different tick granularity than live, the fill engine assumed prices you would not actually get, or the code runs a different path in the Historical state than in Realtime. None of these are mysterious — each has a specific fingerprint you can check for.

The mindset that helps: a backtest is a simulation of decisions, and any place where the simulation had more information, better fills, or different code than live is a place where the two will part ways. Your job is to close those gaps until the historical run is a faithful dry run of the live one. This does not make a strategy profitable; it makes your test honest, which is the only thing that lets you evaluate the strategy at all. The [why-most-backtests-fail](/learn/why-most-backtests-fail) overview frames the general problem; this is the NinjaTrader-specific checklist.

Culprit 1: intrabar repaint and Calculate mode

If your strategy runs Calculate.OnEachTick or Calculate.OnPriceChange, OnBarUpdate fires many times per bar and Close[0] is the forming bar's current price. Logic that reads Close[0] as if it were final will make decisions intrabar that it could never make on a closed bar — and a standard backtest (without Tick Replay) only ever sees one price per historical bar, so it silently disagrees with live.

// WRONG: treats the forming bar as if it were closed on every tick
protected override void OnBarUpdate()
{
    if (Close[0] > High[1]) // fires repeatedly intrabar in realtime
        EnterLong();
}

Gate bar-close logic so it evaluates only once, on completed data:

// CORRECT: only act on the first tick of a new bar => prior bar is closed
protected override void OnBarUpdate()
{
    if (CurrentBar < BarsRequiredToTrade) return;
    if (Calculate != Calculate.OnBarClose && !IsFirstTickOfBar) return;

    if (Close[1] > High[2])
        EnterLong();
}

IsFirstTickOfBar is true on the first tick of the newly forming bar, meaning the previous bar ([1]) just sealed. Reference [1] for confirmed values in that block. This is the [repainting](/learn/repainting) problem in NinjaScript form: it makes a chart look prescient and a backtest look better than the live fills that follow.

Culprit 2: Tick Replay vs no Tick Replay

Standard historical backtesting feeds your strategy one OHLC value per bar — it does not replay the intrabar tick sequence. Live trading, and Tick Replay backtesting, feed the actual tick stream. So any strategy whose behavior depends on the path the price took inside a bar (intrabar stops, trailing logic, OnEachTick signals) will score differently in a plain backtest than live.

The order in which a bar's high and low were reached matters enormously for whether a stop or a target hit first. Without Tick Replay, the backtester has to assume an order, and that assumption is optimistic more often than not.

Practical steps: if your strategy uses Calculate.OnEachTick or intrabar exits, enable Tick Replay on the data series and backtest with it on, so the historical run sees the same granularity live will. Confirm your data source actually has tick data for the period. And be explicit about it in code — if the strategy genuinely needs tick granularity to be correct, it should not be evaluated without it. A strategy that looks good on bar data but falls apart under Tick Replay was relying on the coarse simulation, not on a real effect.

Culprit 3: fill assumptions and order handling

The backtest fill engine makes assumptions your broker will not honor. Market orders in a historical run fill at a clean bar price; live, you get slippage, and in fast markets a lot of it. Limit orders are the sharper trap: the default backtest fills a limit order if price merely touched the limit, but live your order sits in a queue and may never fill even though price traded there.

Make the simulation pessimistic on purpose. Set a realistic slippage value in the strategy/backtest settings rather than trusting frictionless fills. Prefer the more conservative fill resolution, and if your logic hinges on limit fills, treat a touch as a maybe, not a yes. Account for commission explicitly. Size stops and targets so that a tick or two of slippage does not flip the outcome.

// Managed stop/target: define exits explicitly rather than
// relying on the engine to fill you at an idealized price
protected override void OnStateChange()
{
    if (State == State.Configure)
    {
        SetStopLoss(CalculationMode.Ticks, 20);
        SetProfitTarget(CalculationMode.Ticks, 40);
    }
}

Explicit, conservative exits close the gap between what the engine imagined and what your broker will actually do. None of this improves a strategy's edge — it stops your test from inventing one.

Culprit 4: Historical vs Realtime code paths

A strategy transitions through states: State.Historical while processing past bars, then State.Realtime once it reaches the live edge. State.Transition marks the crossover. Code that branches on this — or that only initializes in one path — can behave differently before and after the handoff, which is exactly the seam where backtest and live diverge.

Watch for logic that keys off State == State.Realtime, connection or account calls that return nothing historically, and one-time setup placed in the wrong state block. A frequent bug is initializing a variable only in State.Realtime (or only in State.DataLoaded) so the historical portion runs with a different starting condition than live.

protected override void OnBarUpdate()
{
    // If you special-case realtime, make sure historical stays consistent
    if (State == State.Realtime)
    {
        // realtime-only branch -- audit this against the historical path
    }
}

Work through the four culprits as a checklist: repaint/Calculate mode, Tick Replay parity, fill realism, then state-path consistency. The free [/ninjascript validator](/ninjascript) flags the static ones — intrabar Close[0] reads, missing bar-close gates, and future-index access — before you burn a backtest chasing them. Fixing these makes your backtest a truthful rehearsal of live execution; it does not, and cannot, guarantee any particular result. This is educational material about testing correctness only, not trading advice.

Key takeaways

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

Catch the bug that compiles.Run auditGet Pro