Repainting, explained and avoided
Lesson 15 · about 12 min
"Repainting" is the word for a script whose past looks different from its present: a signal that was there at 10:03 and gone by 10:05, a higher-timeframe line that shifts once the day ends, an arrow that appears five bars after the bar it points at. Every one of these has a specific cause in the execution model, and every one has a fix or an honest label. This lesson names them so you can recognise which one you are looking at.
Cause 1: the live bar is not finished
On historical bars the script sees final values. On the live bar it re-runs on every tick, and close is whatever the last trade was. A condition like ta.crossover(close, ema) can be true at one tick and false at the next. The shape flickers, the alert fires, then the bar closes below the EMA and history shows no signal at all.
The fix is to require confirmation:
//@version=6
indicator("Repaint-safe signal", overlay=true)
rsi = ta.rsi(close, 14)
raw = ta.crossover(rsi, 30)
confirmed = raw and barstate.isconfirmed
plotshape(raw, "Raw", style=shape.circle, location=location.belowbar, color=color.gray, size=size.tiny)
plotshape(confirmed, "Confirmed", style=shape.triangleup, location=location.belowbar, color=color.green, size=size.small)
alertcondition(raw, "RSI cross 30", "RSI crossed above 30 on {{ticker}}")
On history both shapes coincide, because historical bars are always confirmed. Live, the gray circle can appear and vanish; the green triangle appears only at the close and stays. For alerts, the dialog setting "Once per bar close" does the same job for alertcondition(), and alert.freq_once_per_bar_close does it for alert(). Choose one of those unless you have a specific reason to want intrabar notifications, and if you do, understand that history will not show them.
The cost is latency: you learn about the cross when the bar ends, not when it happens. On a daily chart that is the close; on a 5-minute chart it is at most five minutes. That is the price of a signal you can backtest.
Cause 2: higher-timeframe lookahead
Module 3 covered this. request.security() with lookahead_on and no offset shows historical bars a value from the future; with lookahead_off the live bar shows a developing value that history never showed. Both make a backtest untrustworthy. The expr[1] plus lookahead_on idiom returns only completed higher-timeframe values and is the fix.
Cause 3: lag drawn in the past
ta.pivothigh(high, 5, 5) confirms a pivot five bars after it happened, and a label placed at bar_index - 5 sits on the pivot bar. Scrolling history, every pivot label looks like it was there in time to trade. It was not; it appeared five bars later. This is not the script lying about values, it is the drawing placing a late fact at an early location. The same applies to zigzags, some divergence detectors, and any offset less than zero.
The honest version either draws at the confirmation bar, or draws at the pivot bar and says "confirmed n bars later" in the description. If you use such a signal in a strategy, enter on the confirmation bar, never on the pivot bar.
Cause 4: varip and intrabar state
varip keeps values across ticks within the live bar, which lets a script count ticks or accumulate up-volume in real time. Historical bars have one tick each, so anything built on varip cannot be reproduced on history. Scripts that use it are live-only tools; do not backtest them.
Cause 5: strategy settings
calc_on_every_tick=true makes a strategy evaluate on every live tick, so orders can be submitted from unconfirmed bars while the backtest only ever saw confirmed bars. calc_on_order_fills=true recalculates after each fill, which can chain entries within a bar in ways history handles differently. Leave both at their defaults unless you are deliberately modelling intrabar execution and understand the mismatch.
Repainting that is not a problem
Some things change and should. A moving average's last point moves with the live bar. A VWAP updates all session. A developing daily candle changes shape. None of those are dishonest; the values are current and history shows the final versions. "Repainting" is only a problem when a signal, something you act on, is shown on history where it could not have been acted on.
Non-repainting does not mean profitable, either. A confirmed, lookahead-free signal can still have zero edge. It only means the backtest is at least measuring the same thing you would trade.
Key idea: A signal repaints when history shows something that was not knowable at that moment. Guard live signals with
barstate.isconfirmedor once-per-bar-close alerts, use[1]withlookahead_onfor higher timeframes, and enter on the bar where lagging signals confirm.
A checklist before trusting a signal
- Does it use
request.security? If so, is the expression offset by[1]? - Does the alert fire once per bar close?
- Are any shapes or labels placed with a negative offset or at a past
bar_index? - Does the script use
varip,calc_on_every_tickorcalc_on_order_fills? - Have you watched it on a live bar for an hour and compared with the same bars afterwards?
Try it: Take any signal script you have written and add a
plotshapewithbarstate.isconfirmednext to the raw one. Watch a live 1-minute chart for fifteen minutes and count how many gray circles appear and then vanish. Then set an alert on the raw condition with "Once per bar" and another with "Once per bar close", and compare the notifications.
Recap
- Live bars recalculate every tick; guard signals with
barstate.isconfirmedand use once-per-bar-close alert frequencies. - Higher-timeframe data needs the
[1]pluslookahead_onidiom to match history and real time. - Pivots and other lagging signals confirm late; draw or act on the confirmation bar.
varip,calc_on_every_tickandcalc_on_order_fillscreate live behaviour that history cannot reproduce.- Changing current values are fine; signals shown where they were not knowable are not, and a clean signal still needs an edge.
See it drawn
Original diagrams for the ideas on this page. Illustrative, not real market data.