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

Why Your Pine Script Backtest Doesn't Match Live Trading

A backtest that looks clean in the Strategy Tester but behaves differently live almost always traces to a handful of well-understood causes: repainting signals, look-ahead bias, intrabar recalculation, and unrealistic fill assumptions. This guide walks through each mechanism and its fix, then gives a non-repainting baseline you can build on. Educational only — nothing here is financial advice, no strategy is implied to be profitable, and trading always carries the risk of loss.

The gap is a modelling gap, not bad luck

When a strategy shines in the Strategy Tester and then disappoints live, the instinct is to blame variance or a bad market regime. Usually the truth is more boring and more fixable: the backtest and the live engine were never computing the same thing. TradingView runs your script over historical bars one way and over the live, forming bar another way. If your logic quietly depends on information that only exists in hindsight — a bar's final close before it has closed, a higher-timeframe value that hasn't printed yet, an intrabar tick that history smooths away — the tester rewards you for a decision you could never actually make in real time.

That is the core idea to hold onto. A backtest is a simulation, and a simulation is only as honest as its assumptions. The Strategy Tester will happily report a curve based on fills you would never get and signals that would have shifted after the fact. None of that is TradingView being wrong; it is your script asking the engine to do something the live market cannot honour.

The good news is that the divergence has a short list of usual suspects. Repainting from the forming bar, look-ahead bias through request.security, intrabar recalculation via calc_on_every_tick, and optimistic fill assumptions cover the vast majority of cases. Each has a clear mechanism and a concrete fix. Work through them and your tester and your live chart start to agree — which is the necessary precondition for a backtest to mean anything at all. It is still never a promise of future results.

Repainting: reading a bar before it has closed

The most common cause is acting on the forming bar. Live, the current bar's close, high, and low change with every tick until the bar closes. In history, every bar is already finished, so those values look stable. A signal that fires mid-bar can therefore appear and disappear live, then show up as a clean, permanent entry in the backtest.

Here is the wrong version. This plots and could trigger an alert on the still-forming bar:

//@version=6
// WRONG - repaints: signal evaluated on the forming bar.
indicator("Repainting cross", overlay = true)
basis = ta.sma(close, 20)
longSignal = ta.crossover(close, basis)
plotshape(longSignal, style = shape.triangleup, location = location.belowbar)

While the bar is open, close wanders, so longSignal can turn true, then false, then true again. The backtest never sees that flicker because history only keeps the final values.

The fix is to gate the signal on a confirmed bar. barstate.isconfirmed is true only on the bar's final tick, so the tester and live agree:

//@version=6
// Educational only - validate before trading; not financial advice.
indicator("Confirmed cross", overlay = true)
basis = ta.sma(close, 20)
longSignal = ta.crossover(close, basis)
plotshape(longSignal and barstate.isconfirmed, style = shape.triangleup, location = location.belowbar)

The trade-off is honest: you act one bar later than the flickering version pretended to. That delay is real; the earlier fill was fiction. Accept the delay and your alerts stop repainting.

Look-ahead bias through request.security

Higher-timeframe data is the second big trap. When you pull, say, a 4-hour value onto a 15-minute chart, the question is whether you are allowed to see the 4-hour bar before it has finished. barmerge.lookahead_on lets the historical series peek at the completed higher-timeframe bar — data that, live, would not exist yet. The backtest looks uncannily good because it is trading on the future.

The wrong version:

//@version=6
// WRONG - look-ahead bias: sees the HTF close before it prints.
indicator("HTF lookahead", overlay = true)
htfClose = request.security(syminfo.tickerid, "240", close, lookahead = barmerge.lookahead_on)
plot(htfClose)

With lookahead_on and no offset, on historical bars htfClose already knows the final 4-hour close from the first 15-minute bar inside it. Live, it cannot. Same trap with a negative index like close[-1], which literally reads the next bar and is never valid.

The fix is to request the confirmed, prior higher-timeframe value: keep lookahead_off and offset the series by [1] so you only ever use a bar that has already closed:

//@version=6
// Educational only - validate before trading; not financial advice.
indicator("HTF no-lookahead", overlay = true)
htfClose = request.security(syminfo.tickerid, "240", close[1], lookahead = barmerge.lookahead_off)
plot(htfClose)

The close[1] with lookahead_off pairing is the standard non-repainting way to consume higher-timeframe data. It costs you the freshest value, but everything it gives you is information you would genuinely have had at the time.

Intrabar recalculation and calc_on_every_tick

The third divergence is subtle because it changes behaviour only in the live session. By default, calc_on_every_tick = false, which means live your script recomputes once per bar close — matching how history is evaluated. Flip it to true and live the strategy recalculates on every incoming tick, so orders can be evaluated intrabar. But in history there are no intraday ticks to replay at that resolution, so the same script fills on bar close. The result is two different execution paths from one code base.

//@version=6
// WRONG - intrabar live, on-close in history: paths diverge.
strategy("Tick-sensitive", overlay = true, calc_on_every_tick = true)
basis = ta.sma(close, 20)
if ta.crossover(close, basis)
    strategy.entry("Long", strategy.long)

The crossover can fire mid-bar live and never in the backtest, or vice versa, so your live entries land at prices the tester never modelled.

Unless you have a specific, deliberate reason to react intrabar, leave it at the default and gate any per-bar logic on a confirmed bar:

//@version=6
// Educational only - validate before trading; not financial advice.
strategy("Bar-close consistent", overlay = true, calc_on_every_tick = false)
basis = ta.sma(close, 20)
if ta.crossover(close, basis) and barstate.isconfirmed
    strategy.entry("Long", strategy.long)

If you genuinely need intrabar reactions, accept that you must validate them differently — the standard bar-based backtest cannot faithfully represent them, and you should not treat its curve as evidence for that behaviour.

Unrealistic fills: commission, slippage, spread and liquidity

Even a perfectly non-repainting strategy will overstate itself if the fill model is fantasy. By default the Strategy Tester fills market orders at the next bar's open with zero commission, zero slippage, and no spread, and it assumes your default_qty executes in full regardless of available liquidity. Live, you pay commission, you cross a spread, your fill slips against you, and a large order can move the book. Every one of those is a cost the default backtest ignores.

The fix is to make the tester pessimistic on purpose. Declare realistic commission and slippage in the strategy() call, and size positions so they are plausible for the instrument's real liquidity rather than an idealised infinite fill:

//@version=6
// Educational only - validate before trading; not financial advice.
strategy("Realistic costs", overlay = true,
     commission_type = strategy.commission.percent, commission_value = 0.04,
     slippage = 3, calc_on_every_tick = false)

len = input.int(20, "Length")
risk = input.float(2.0, "Stop %") / 100.0
basis = ta.sma(close, len)

if ta.crossover(close, basis) and barstate.isconfirmed
    strategy.entry("Long", strategy.long)
    strategy.exit("Stop", "Long", stop = close * (1.0 - risk))

Note the stop input is actually wired into strategy.exit(stop = ...) — a risk input you declare but never use is a dead giveaway of an unvalidated script. Set slippage in ticks to a level your instrument realistically experiences, and prefer a commission that errs high. A strategy that still looks reasonable after honest costs is far more informative than one that only survives under free, instant, perfect fills. It is still not a guarantee.

A non-repainting baseline and honest framing

Pull the pieces together and you get a baseline that computes the same thing in history and live: confirmed-bar signals, no look-ahead, on-close evaluation, and realistic costs.

//@version=6
// Educational only - validate before trading; not financial advice.
strategy("Non-repainting baseline", overlay = true,
     commission_type = strategy.commission.percent, commission_value = 0.02,
     slippage = 2, calc_on_every_tick = false)

len = input.int(20, "Length")
basis = ta.sma(close, len)

longSignal = ta.crossover(close, basis)

// Act only on confirmed bars so the tester and live agree.
if longSignal and barstate.isconfirmed
    strategy.entry("Long", strategy.long)

if strategy.position_size > 0 and ta.crossunder(close, basis) and barstate.isconfirmed
    strategy.close("Long")

Two more practical notes. Your plan limits how many historical bars the tester loads, so a strategy that trades rarely may show too few samples to say anything at all — a small backtest is a weak backtest. And the data feed behind the tester can differ from your live broker feed in spread, timestamps and even which ticks printed, so expect some residual gap even after everything above is correct.

Hold the framing honestly. A clean, non-repainting backtest with realistic costs is a necessary check: it tells you the strategy is at least internally consistent and not cheating on future data. It is never a promise of live results. Past behaviour on historical bars does not predict future performance, this article implies nothing about profitability or win rate, and all trading carries the risk of loss. Validate independently, forward-test on paper, and treat the tester as a filter for broken logic — not as evidence that you will make money.

Key takeaways

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

Catch the bug that compiles.Run auditGet Pro