Understand why scripts look different on historical bars vs live bars — and how barstate.isconfirmed and the [1] offset protect your strategies from misleading signals.
Repainting is one of the most important — and most frequently misunderstood — concepts in Pine Script. A repainting script shows different values on a completed historical bar than it showed during that bar's formation in real time. In a backtest, every bar is already complete, so the script always sees the final values. In live trading, the current bar is still forming, and values change with every tick.
The practical consequence is significant: a strategy that looks excellent in backtesting may generate entirely different signals when you run it live. The entry that appeared on the historical chart fired at the close of a confirmed bar. The live version fires mid-bar, before the close, based on values that may still change before the bar ends.
Pine Script gives you precise control over this. The three tools that eliminate repainting problems are `barstate.isconfirmed`, the `[1]` offset on signal conditions, and the `lookahead` parameter on `request.security()`.
When a script evaluates conditions using the current bar's `close`, those conditions are checked on every tick while the bar is still open. A crossover that appears true at 10:05 AM may be false by 10:10 AM as the close moves. In backtesting, TradingView evaluates each bar only once — at its final close — so the crossover either happened or it didn't. In live trading, the crossover fires mid-bar and may reverse before the bar closes.
The fix is `barstate.isconfirmed`. This boolean is `true` only on the last execution of a bar's lifetime — the confirmed close. Adding it to any signal condition ensures the signal only fires after the bar is fully baked:
`confirmed_long = ta.crossover(fastEMA, slowEMA) and barstate.isconfirmed`
Note: on historical bars in a backtest, `barstate.isconfirmed` is effectively always `true` because each bar is evaluated at its close. The difference only becomes visible in real time.
When you use `request.security()` to fetch data from a higher timeframe, the `lookahead` parameter controls when that data becomes available to your script.
With `barmerge.lookahead_on`, the higher timeframe bar's final value (its close) becomes visible to your script as soon as that HTF bar opens. In backtesting, this allows your script to "know" the final daily close before the day has ended — generating signals based on future data that was never actually available at that moment. Performance looks better because your system effectively cheats.
With `barmerge.lookahead_off` (the default), the HTF value only updates after the HTF bar has fully closed. This is the correct setting for backtesting.
A subtler form of repainting occurs when you compare current-bar indicator values to past-bar prices (or vice versa). The standard defensive pattern is to use `[1]` offsets to ensure you are always comparing fully confirmed values:
`signal = close[1] > ta.ema(close, 20)[1]`
Both values are from completed bars — no mid-bar ambiguity. The trade-off is that the signal fires one bar later than the "raw" version.
For strategies that enter on the next bar's open after a signal, this is actually the correct behaviour: you confirm the signal at bar N's close, then enter at bar N+1's open.
The clearest test is to add your signal to a chart and watch it in real time on a live market. Does the signal appear mid-bar and then disappear? That is repainting. Does it only appear (and stay) after a bar closes? That is confirmed behaviour.
For strategies, compare the backtest result before and after adding `barstate.isconfirmed`. A significant drop in apparent performance after adding it is a sign that the original signals were exploiting intrabar data that is not available at the close.
Not all repainting is a bug. Indicators like VWAP are designed to update continuously throughout the session. A live candle's VWAP reading does change as the candle progresses — that is correct behaviour for an intraday tool. The problem arises only when a strategy entry or exit depends on a value that changes mid-bar in ways that differ between the backtest and live environments.
//@version=6
indicator("Bar Confirmation Demo", overlay=true)
fastLen = input.int(9, "Fast EMA")
slowLen = input.int(21, "Slow EMA")
fastEMA = ta.ema(close, fastLen)
slowEMA = ta.ema(close, slowLen)
// ❌ REPAINTS: fires on every tick — may appear and disappear before bar close
live_long = ta.crossover(fastEMA, slowEMA)
live_short = ta.crossunder(fastEMA, slowEMA)
// ✅ CONFIRMED: only fires once the bar has fully closed — value is permanent
confirmed_long = ta.crossover(fastEMA, slowEMA) and barstate.isconfirmed
confirmed_short = ta.crossunder(fastEMA, slowEMA) and barstate.isconfirmed
// ── request.security() lookahead comparison ───────────────────────────
// ❌ lookahead_on: can "see" the daily close before the day ends
htf_lookahead = request.security(syminfo.tickerid, "D", close,
lookahead=barmerge.lookahead_on)
// ✅ lookahead_off (default): daily value only updates after day closes
htf_safe = request.security(syminfo.tickerid, "D", close,
lookahead=barmerge.lookahead_off)
plot(fastEMA, "Fast EMA", color=color.blue, linewidth=1)
plot(slowEMA, "Slow EMA", color=color.orange, linewidth=1)
plot(htf_lookahead, "HTF Lookahead (⚠️)", color=color.new(color.red, 40), linewidth=1)
plot(htf_safe, "HTF Safe ✓", color=color.new(color.green, 40), linewidth=1)
// Both signal sets — identical on history, different in real-time
plotshape(live_long, style=shape.circle, location=location.belowbar,
color=color.new(color.yellow, 30), size=size.tiny, title="Live (repaints)")
plotshape(confirmed_long, style=shape.triangleup, location=location.belowbar,
color=color.green, size=size.small, title="Confirmed Long ✓")
plotshape(live_short, style=shape.circle, location=location.abovebar,
color=color.new(color.yellow, 30), size=size.tiny, title="Live (repaints)")
plotshape(confirmed_short, style=shape.triangledown, location=location.abovebar,
color=color.red, size=size.small, title="Confirmed Short ✓")