What repainting is
Repainting is when a script's historical values change after the fact. The line you see today over old bars is not the line the script drew when those bars were live. A signal that looks like it fired one bar early in backtest fires one bar late in production, or never. The chart redraws itself once the future arrives, so your backtest is reading numbers that did not exist at the time.
That is the whole problem in one sentence: a repainting script lies about the past, so anything you measure on history, win rate, drawdown, signal timing, is fiction. You cannot trust a backtest you cannot reproduce.
Why it happens
Repainting comes from reading data that was not yet available at the bar you are computing. Two classic sources:
- An unclosed higher-timeframe candle. You are on a 1h chart and you ask for the 4h close. While the 4h candle is still forming, a naive lookup hands you its live, not-yet-final value. On history, the engine knows how that 4h candle ended, so it backfills the finished number. The live chart never had that number. The two disagree, and your signal moves.
- A future-leaking series. Any value that depends on a bar later than the one you are on. Centered smoothers, "highest of the next N bars," or a calculation that quietly indexes forward. On history the future is known, so the value resolves cleanly. Live, the future does not exist yet, so the value is wrong or absent.
In both cases the tell is the same: the script saw something during the backtest that it could not have seen in real time.
The good news: htf() is confirmed by default
kScript's higher-timeframe function does not have this trap. htf() returns the confirmed value: at any bar it exposes only the most recent higher-timeframe candle that has already closed by that bar. Rerun the script over history and it matches what it showed live, bar for bar. No look-ahead, no flag to remember, no lookahead=barmerge.lookahead_off incantation to get right. Confirmed is what you get for free, and confirmed is the safe default.
Here is the causal-prefix guarantee in one sentence: at bar t, every value htf() exposes is derived only from data that had finished by t, so the past never changes when the future arrives.
//@version=2
define(title="No-Repaint HTF", position="onchart", axis=false)
timeseries d = ohlcv(symbol=currentSymbol, exchange=currentExchange)
// htf() returns the last CONFIRMED higher-timeframe candle. At any bar it uses
// only 4h candles that already closed, so the line never repaints under you.
timeseries h4 = htf(d, "4h")
plotLine(value=h4.close, colors=["#7c3aed"], width=2, label=["4h close (confirmed)"], desc=["the most recent fully closed 4h candle, no look-ahead"])
plotLine(value=d.close, colors=["#94a3b8"], width=1, label=["Close"], desc=["the chart-timeframe close for comparison"])Read the staircase
The purple 4h close line is flat across four 1h bars, then steps to a new level, then holds flat again. That staircase is the visual signature of a correct, confirmed higher timeframe. The 4h value only changes when a 4h candle actually closes. Between closes there is no new confirmed information, so the line holds. If that line instead tracked the grey chart close tick by tick, you would be looking at a repainting (developing) value, and any cross built on it would not survive into production.
Requesting a live value is opt-in
Sometimes you genuinely want the forming bar: a live 4h close ticking inside the current period, shown in a readout. That is a deliberate choice, so you ask for it explicitly with developing mode:
timeseries h4Live = htf(source=d, timeframe="4h", opts={mode: "developing"})A developing value updates within the current higher-timeframe bucket as the bar fills. It is fine for a live dashboard number. It is the wrong thing for a confirmed signal, because it changes as the bar develops, so a cross or threshold built on it will not reproduce on history. That is exactly the repaint you are trying to avoid. Reach for developing mode only when you want to display the live edge, never when you want to act on it.
How to stay repaint-safe
A short checklist:
- Use confirmed
htf()for signals. It is the default. If you did not passopts={mode: "developing"}, you are already safe. - Do not act on the still-forming bar. The current bar's high, low, and close are still moving until it closes. Gate confirmed signals so they evaluate on settled data, not on a candle that is mid-flight.
- Be careful with values that depend on the last bar. Anything keyed off
isLastBaror the live edge is, by construction, in flux. Use it to draw or annotate, not as the basis of a historical signal you expect to backtest. - Avoid forward-looking indexing. Never read a bar that is later than the one you are computing. If a calculation needs the future to resolve, it will repaint.
Stick to confirmed sources and settled bars and your backtest will mean something: what you measured on history is what the script would have done live.
See also
- Multi-Timeframe for the full
htf(),ltf(), andrequest()story, including developing mode and the bars-inside-a-bar model. - Execution Model for how the bar loop and
isLastBarwork.