Edgoro
Strategy

Pine Script repainting: how to spot it, test for it and fix it

Why some TradingView indicators repaint, the usual causes, how to test any script on replay and live charts, and the Pine Script v6 fix for each one.

A repainting indicator shows signals on historical bars that you would not have seen at the time. The arrows on the chart are perfectly placed because they were drawn with hindsight. In real time, signals arrive late, change, or vanish. A backtest built on them measures a strategy nobody could have traded.

Repainting isn't always a bug, and not every kind is avoidable. What matters is knowing which kind a script has, and whether its historical signals match what it would have shown live. This guide covers the usual causes, a simple way to test any script, and the Pine Script v6 fix for each.

Not all repainting is the same

TypeWhat happensAvoidable?
Live-bar movementValues on the unfinished bar change with every tickNo, but you can wait for the close
Higher-timeframe requestsDeveloping HTF values live, confirmed values in historyYes
Future leakHistorical bars use data from later barsYes, and it must be
Late-confirmed patternsPivots and swings are only known some bars later, then drawn back in timePartly: the lag is real, the drawing is a choice
Plots shifted into the pastA negative offset draws current values on earlier barsYes, for anything used in signals
Strategy recalculation settingsLive calculations that history can't reproduceYes, by keeping defaults

1. The live bar

On the bar that's still forming, close is simply the latest price, and every indicator built on it moves with each tick. A cross can appear at minute 10 and be gone at minute 50. This isn't a flaw in the script; it's the nature of an unfinished bar.

The fix is to act only on closed bars. In code, require barstate.isconfirmed, which is true on every historical bar and on the final update of a live bar. For alerts, choose Once per bar close (see alertcondition() vs alert()).

2. Higher-timeframe requests

request.security() with default settings returns the developing higher-timeframe value in real time and the confirmed one in history. The two disagree, so the same bars look different after a reload. The fix is to request confirmed values with a one-bar offset and lookahead = barmerge.lookahead_on, explained step by step in multi-timeframe Pine Script without repainting.

3. Future leak

The worst kind. Historical bars use information from the future, so signals look flawless and backtests look extraordinary. The classic source is lookahead = barmerge.lookahead_on without the [1] offset. Another is logic that enters at a bar's open using a condition that is only known at that bar's close. If a strategy seems to buy the exact low again and again, look for a leak before celebrating.

4. Pivots and swing points

ta.pivothigh(high, 5, 5) confirms a swing high only after five lower highs have formed to its right. That's honest, but scripts often draw the pivot back on the bar where it formed, with a negative offset or a label placed five bars back. On historical bars the marker appears exactly at the top; live, it appears five bars later. Zigzag indicators work the same way and can also redraw their last leg until it's confirmed.

The lag is the price of the definition. Show the marker on the bar where it became known, or at least be clear that it's drawn retroactively, and never let a strategy act on a pivot before it's confirmed. The RSI divergence guide handles this explicitly.

5. Plots shifted into the past

plot(close, offset = -25) draws each bar's close 25 bars back. The Ichimoku Chikou span does exactly this by design. It's fine as a visual, but if a condition compares price with a series drawn into the past, you're comparing with values that weren't known on those bars.

6. Strategy recalculation settings

By default a strategy calculates once per closed bar, which history can reproduce. Settings like calc_on_every_tick = true make it recalculate on every live tick, which history can't, so live trades can differ from the backtest. Variables declared with varip, and logic that depends on timenow or barstate.isrealtime, behave differently in real time for the same reason. Keep the defaults unless you understand the trade-off.

A quieter effect: the earliest bar a script sees moves forward as new bars arrive, so results that depend on a long chain of var state can shift slightly from one day to the next.

A script that shows the difference

This indicator marks closes above the prior 20-bar high. With the default setting it only signals on closed bars, and an orange circle shows a breakout that is still tentative on the live bar. Switch "Only signal on closed bars" off and watch a live chart: triangles appear and disappear within the bar.

//@version=6
indicator("Repainting check: live vs confirmed signals", overlay = true)

len           = input.int(20, "Breakout length", minval = 2)
confirmedOnly = input.bool(true, "Only signal on closed bars")

priorHigh = ta.highest(high, len)[1]
rawBreak  = close > priorHigh

// On the live bar, close keeps moving: rawBreak can turn true, then false again, before the bar closes.
signal    = confirmedOnly ? rawBreak and barstate.isconfirmed : rawBreak
tentative = rawBreak and not barstate.isconfirmed

plot(priorHigh, "Prior high", color = color.new(color.gray, 0), style = plot.style_stepline)
plotshape(signal, "Breakout", shape.triangleup, location.belowbar, color.new(color.teal, 0), size = size.small)
plotshape(confirmedOnly and tentative, "Tentative (live bar)", shape.circle, location.belowbar, color.new(color.orange, 0), size = size.tiny)

if signal
    alert("Close above the " + str.tostring(len) + "-bar high on " + syminfo.ticker, alert.freq_once_per_bar_close)

Note the [1] on ta.highest(): the current bar's high is part of its own 20-bar high, so close could never be above it. Comparing with the previous bar's value is the correct definition, not a trick.

How to test any script for repainting

  1. Read the code for warning signs. Search for the items in the table below.
  2. Use Bar Replay. Replay the chart bar by bar and note when each signal first appears. Then jump to the end and compare with the signals drawn on history.
  3. Watch it live. Leave the script on a lower-timeframe chart for a session, take screenshots of signals as they appear, then reload the chart and compare.
  4. Log with alerts. Create an alert on every signal with the bar time in the message. The alert log is a record of what the script said in real time.
Look forWhy
request.securityCheck the offset and the lookahead setting
lookahead_onMust come with a [1] offset
ta.pivothigh, ta.pivotlow, zigzag logicLate confirmation, often drawn back in time
Negative offset values, labels placed at past barsValues drawn where they weren't known
calc_on_every_tick, varip, timenow, barstate.isrealtimeReal-time behaviour history can't reproduce

Why it matters for backtests

A strategy is only as honest as its signals. Fix repainting before you look at any results, then follow the rest of backtesting mistakes that inflate results.

Sources and further reading

Try it with Edgoro

Ask Edgoro to "make this indicator non-repainting: confirmed bars only, confirmed higher-timeframe values, and pivots shown when they're confirmed", paste your script, and it rewrites it in Pine Script v6 with each change explained. Fix your script with Edgoro.

Indicators describe the past; even a non-repainting script doesn't predict the future. Trading involves substantial risk of loss.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro