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
| Type | What happens | Avoidable? |
|---|---|---|
| Live-bar movement | Values on the unfinished bar change with every tick | No, but you can wait for the close |
| Higher-timeframe requests | Developing HTF values live, confirmed values in history | Yes |
| Future leak | Historical bars use data from later bars | Yes, and it must be |
| Late-confirmed patterns | Pivots and swings are only known some bars later, then drawn back in time | Partly: the lag is real, the drawing is a choice |
| Plots shifted into the past | A negative offset draws current values on earlier bars | Yes, for anything used in signals |
| Strategy recalculation settings | Live calculations that history can't reproduce | Yes, 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
- Read the code for warning signs. Search for the items in the table below.
- 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.
- 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.
- 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 for | Why |
|---|---|
request.security | Check the offset and the lookahead setting |
lookahead_on | Must come with a [1] offset |
ta.pivothigh, ta.pivotlow, zigzag logic | Late confirmation, often drawn back in time |
Negative offset values, labels placed at past bars | Values drawn where they weren't known |
calc_on_every_tick, varip, timenow, barstate.isrealtime | Real-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
- Pine Script User Manual: Repainting — TradingView's reference page on the subject.
- Pine Script User Manual: Execution model — realtime bars, rollback and why the live bar recalculates.
- Pine Script User Manual: Bar states —
barstate.isconfirmedand related variables. - Pine Script User Manual: Other timeframes and data — higher-timeframe requests.
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.