Build a Non-Repainting Indicator in Pine Script (v6)

jayadev rana Avatar

A non-repainting indicator in Pine Script is one whose signals, plots and alerts stay identical on historical and real-time bars — a confirmed marker never moves, shifts or disappears after the bar closes. As of July 2026, the core rule for building a non-repainting indicator in Pine Script (v6) is simple: only ever make a decision on confirmed data. For higher-timeframe values that means requesting them with a one-bar offset and lookahead on — request.security(syminfo.tickerid, tf, expr[1], lookahead = barmerge.lookahead_on) — and gating chart-timeframe signals with barstate.isconfirmed. Do that, and your history matches your live chart.

I’m Jayadev Rana, a TradingView-certified Pine Script developer. Over 8+ years I’ve written 7,700+ scripts for traders in 14+ countries, and if I had to name the single most common bug I fix during code audits, it’s repainting — usually a higher-timeframe filter that looked flawless in backtest but shifted its entries the moment it went live. This article is the practical, code-first companion to my explainer on why Pine Script indicators repaint: here we actually build one that doesn’t.

What “non-repainting” actually means

TradingView defines repainting as script behaviour that causes historical and real-time calculations or plots to behave differently. By that definition more than 95% of indicators repaint something — a bare close, RSI or MACD all flicker on the open bar and only freeze when it closes. That kind of intrabar movement is usually harmless once you understand it.

It helps to know why. On a historical bar Pine only had the final OHLC values, so a script runs once with confirmed numbers. On a real-time bar it re-executes on each tick, rolling back to the last committed state first — so close, your indicators and any HTF request track the live, unconfirmed price until the bar closes. Values from those fluid numbers can look one way live and another once the bar becomes history.

The repainting that hurts a trader is the kind that rewrites history. Three flavours matter to a builder:

  • Fluid-value repaint: a signal fires intrabar on high, low or close, then vanishes before the bar closes because price kept moving.
  • Higher-timeframe repaint: a request.security() call returns the developing value of the current HTF bar, which keeps changing until that bar closes.
  • Future leak: the worst case — data from bars that had not happened yet appears on historical bars, making a backtest look prophetic and impossible to reproduce live.

A genuinely non-repainting indicator avoids all three. The trick is to feed every decision with values that were already final at the moment the bar you are standing on had closed.

The one rule that stops higher-timeframe repainting

Most repaint reports I see trace back to a single line. When you pull higher-timeframe data, TradingView’s own documentation is explicit: to get values that are identical on historical and real-time bars, offset the expression by at least one bar with the history-referencing [] operator and set lookahead = barmerge.lookahead_on. The two are interdependent — remove either one and you break it.

Here is the reusable helper I drop into almost every multi-timeframe script:

//@version=6
// Non-repainting higher-timeframe request.
// The [1] offset and lookahead_on are interdependent — keep both.
f_htf(tf, expr) =>
    request.security(syminfo.tickerid, tf, expr[1], lookahead = barmerge.lookahead_on)

Why does this work? With lookahead_on and the [1] offset the call always returns the last confirmed higher-timeframe value — never the one still forming. On history and in real time it steps forward at the start of each new HTF period with a value that can no longer change. Using lookahead_on without the offset does the opposite: it leaks the finished HTF value onto bars before it was knowable, which is future bias and is not allowed in published scripts. I break the mechanics down further in my guide to request.security() and lookahead.

The repainting anti-pattern vs the fix

Let’s make it concrete. Say you want a daily-trend filter on an intraday chart: go long when the daily close crosses above its 20-period daily EMA. Here is the version I see constantly — and it repaints:

//@version=6
indicator("HTF signal - REPAINTS, do not use", overlay = true)

// Plain request.security(): returns the DEVELOPING daily close on real-time bars.
htfClose = request.security(syminfo.tickerid, "1D", close)
htfEma   = request.security(syminfo.tickerid, "1D", ta.ema(close, 20))

// The cross can appear intrabar and disappear before the bar closes.
longSignal = ta.crossover(htfClose, htfEma)
plotshape(longSignal, "Long", shape.triangleup, location.belowbar, color.new(color.green, 0))

Two defects: htfClose uses the default lookahead_off with no offset, so on the live daily bar it is the current, unconfirmed close that keeps moving; and the cross is evaluated intrabar, so a triangle can print and then vanish. The backtest looks clean because history only ever stored confirmed closes — live is a different animal.

Now the fix, using the helper and a confirmation gate:

//@version=6
indicator("HTF signal - non-repainting", overlay = true)

f_htf(tf, expr) =>
    request.security(syminfo.tickerid, tf, expr[1], lookahead = barmerge.lookahead_on)

htfClose = f_htf("1D", close)
htfEma   = f_htf("1D", ta.ema(close, 20))

// Only register the cross once the chart bar is closed.
longSignal = ta.crossover(htfClose, htfEma) and barstate.isconfirmed
plotshape(longSignal, "Long", shape.triangleup, location.belowbar, color.new(color.green, 0))

Same idea, but every input is a confirmed value and the signal is locked to a closed bar. Reload the chart and the non-repainting version’s markers stay exactly where they were; the first one’s markers move.

Want this pattern baked into an existing indicator, or a repaint audit on a script you already trade? Message me on WhatsApp at +91 77352 68199 and send the code — I’ll tell you exactly where it repaints.

A complete non-repainting HTF signal indicator

Here is a full, self-contained indicator you can paste straight into the Pine Editor. It plots a confirmed higher-timeframe EMA, colours it by trend, prints non-repainting long and short markers, and — importantly — only fires alerts once the bar has closed.

//@version=6
indicator("Non-Repainting HTF Trend Signal", "NR HTF", overlay = true)

// ---------- Inputs ----------
htfInput = input.timeframe("1D", "Higher timeframe")
emaLen   = input.int(20, "HTF EMA length", minval = 1)

// ---------- Non-repainting request helper ----------
// The [1] offset and lookahead_on are interdependent - keep both.
f_htf(tf, expr) =>
    request.security(syminfo.tickerid, tf, expr[1], lookahead = barmerge.lookahead_on)

// Guard: the requested timeframe must be at or above the chart timeframe.
if timeframe.in_seconds(htfInput) < timeframe.in_seconds()
    runtime.error("Choose a higher timeframe than the chart.")

// ---------- Confirmed higher-timeframe series ----------
htfClose = f_htf(htfInput, close)
htfEma   = f_htf(htfInput, ta.ema(close, emaLen))

// ---------- Signals: only on a closed chart bar ----------
trendUp  = htfClose > htfEma
longSig  = ta.crossover(htfClose, htfEma)  and barstate.isconfirmed
shortSig = ta.crossunder(htfClose, htfEma) and barstate.isconfirmed

// ---------- Plots ----------
plot(htfEma, "HTF EMA", trendUp ? color.teal : color.red, 2)
plotshape(longSig,  "Long",  shape.triangleup,   location.belowbar, color.new(color.teal, 0), size = size.small)
plotshape(shortSig, "Short", shape.triangledown, location.abovebar, color.new(color.red, 0),  size = size.small)

// ---------- Alerts fire only on bar close ----------
if longSig
    alert("Non-repainting LONG on " + syminfo.ticker, alert.freq_once_per_bar_close)
if shortSig
    alert("Non-repainting SHORT on " + syminfo.ticker, alert.freq_once_per_bar_close)

Design choices worth calling out:

  • f_htf() is the confirmed-value request from earlier; every higher-timeframe number flows through it, so nothing repaints.
  • The runtime.error() guard rejects a timeframe lower than the chart’s. Requesting a lower timeframe is a different problem entirely; for that you want request.security_lower_tf(), not this call.
  • Both signals carry and barstate.isconfirmed, so a marker only commits when the chart bar closes.
  • alert.freq_once_per_bar_close makes the alert wait for the bar close even if a condition flickered true mid-bar.

Non-repainting alerts and orders

Clean plots are only half the job. An indicator that draws confirmed markers but fires alerts intrabar will still send signals that later disappear. Two habits fix this: gate the condition with barstate.isconfirmed and set the alert frequency to alert.freq_once_per_bar_close. With webhooks, that is the difference between an order that matches your chart and one that doesn’t. For strategies the related trap is calc_on_every_tick = true, which re-runs order logic on every price update and can desync live fills from your backtest — I cover that in detail in my note on calc_on_every_tick.

Other repainting traps I check in every audit

Beyond higher-timeframe requests, a handful of built-ins quietly reintroduce repainting:

  • barstate.isnew: true on the close of a historical bar but on the open of a real-time bar — an instant historical-versus-live mismatch.
  • varip: it holds state across intrabar ticks that cannot be reproduced on OHLC-only history, so plots and alerts diverge.
  • timenow and lower-timeframe request.security(): both behave differently once real-time bars arrive.
  • Plotting into the past: pivots drawn five bars back look great on history but appear late in real time. If you offset drawings, make it an input the user opts into.

None of these are automatically “bad”; TradingView is clear that some repainting is acceptable if you know it is there. What matters is that a signal you act on was final when it printed. To harden this into a production tool, my custom indicator development and Pine Script development services exist for exactly that.

A quick note on Pine Script v6

One v6-specific change is worth knowing. As of v6, request.*() calls are dynamic by default: they can run inside loops, conditionals and exported library functions, and accept “series” arguments — things that were impossible in v5. That is powerful for scanners and dashboards, but it does not change the repainting rule at all. A dynamic request still returns the developing HTF value unless you apply the [1]-offset-plus-lookahead_on pattern. If you are still on older code, see my overview of the latest Pine Script version in 2026 before you migrate.

Frequently asked questions

Does adding barmerge.lookahead_on cause a future leak?

Only if you forget the offset. lookahead_on combined with an [1] offset returns the last confirmed HTF value and is safe. lookahead_on with no offset returns data before it was knowable — that is the future leak, and it is disallowed in published scripts.

Is barstate.isconfirmed enough on its own?

For chart-timeframe logic, yes — it forces evaluation on the bar’s final tick, and historical bars are always confirmed. But it does not fix a higher-timeframe request; that still needs the offset-plus-lookahead pattern. I use both together.

Will a non-repainting indicator make me more money?

No — and anyone promising that is selling something. Non-repainting signals are simply honest: they arrive a little later than a repainting version, but they are reproducible. This is about trusting your backtest, not guaranteed profit.

Why do my alerts still fire mid-bar?

Because the alert frequency defaults to firing intrabar. Set it to alert.freq_once_per_bar_close and gate the condition with barstate.isconfirmed so it can only trigger on a closed bar.

Can I use this pattern for lower-timeframe data?

No. This pattern is for higher timeframes. To pull data from a timeframe lower than your chart, use request.security_lower_tf(), which is designed to handle intrabars correctly.

Building a non-repainting indicator is mostly discipline: confirmed data in, confirmed signals out. If you would rather hand it off, or you want an existing script audited and fixed, message me on WhatsApp at +91 77352 68199 — send the code and the timeframe you trade, and I’ll take it from there.

Risk disclaimer: This article is for educational purposes only and is not financial advice. Trading and algorithmic strategies carry a substantial risk of loss. Non-repainting code makes signals reproducible, not profitable — always test thoroughly on your own data and never risk capital you cannot afford to lose.

Leave a Reply

Your email address will not be published. Required fields are marked *