Test Your Strategy

Home / Blog / Pine Script repainting

How to stop your Pine Script from repainting

A repainting script shows signals in history that it could never have shown live. The backtest looks superb, the alerts fire and vanish, and the account disagrees with the chart. There are three common causes and each has a specific fix.

What repainting actually means

A Pine script recalculates on every tick of the current, unclosed bar. If your signal depends on values that are still moving — the close of a bar that hasn't closed — then the arrow appears, disappears and reappears as price wobbles. When the bar finally closes, the script keeps only the last state, and history shows a clean signal that in reality flickered for minutes.

The consequence is that your Strategy Tester results describe a script that could see slightly into the future.

Cause 1 — evaluating on an unclosed bar

The most frequent version. Your condition uses close, which on the live bar is simply "price right now".

// Repaints: `close` is still moving on the live bar
longSignal = ta.crossover(close, ta.sma(close, 20))
plotshape(longSignal, style=shape.triangleup)

Fix: evaluate on confirmed bars only. barstate.isconfirmed is true only when the bar has closed.

// Stable: only fires once the bar is confirmed
raw        = ta.crossover(close, ta.sma(close, 20))
longSignal = raw and barstate.isconfirmed
plotshape(longSignal, style=shape.triangleup)

For alerts, the equivalent is setting the alert to trigger Once Per Bar Close rather than Once Per Bar. Even a correctly written script will appear to repaint if the alert is configured to fire intrabar.

Cause 2 — careless request.security() calls

Pulling a higher timeframe into a lower one is the second big source. Requesting daily data on a 5-minute chart, without care, gives you today's still-forming daily close — a value your live script could not know until the day ends.

// Repaints: returns the in-progress daily value
dailyMA = request.security(syminfo.tickerid, "D", ta.sma(close, 50))

Fix: request the previous completed value, and use lookahead-off explicitly.

// Stable: last *closed* daily bar only
dailyMA = request.security(syminfo.tickerid, "D",
                           ta.sma(close, 50)[1],
                           lookahead = barmerge.lookahead_off)

The [1] offset is what does the real work — it asks for the value as of the last completed higher-timeframe bar. Never use barmerge.lookahead_on in a strategy you intend to trade; it exists for plotting convenience and it genuinely reads the future.

Cause 3 — mutable variables that rewrite history

Using var state that gets reassigned during the live bar, or drawing objects that move as price moves, produces charts that look different when reloaded. Anchored VWAPs, pivot-based support lines and zigzag structures are all prone to this — a pivot high is only confirmed after N bars have passed, so a live pivot is provisional by definition.

Fix: accept the confirmation lag rather than hiding it. If your pivot needs 5 bars to the right, then live you learn about it 5 bars late. Backtesting it as though you knew immediately is simply not the same strategy.

How to prove your script is clean

  1. The reload test. Screenshot the last 50 bars of signals. Refresh the chart. If any signal moved or vanished, it repaints.
  2. The replay test. Use TradingView's Bar Replay and step forward bar by bar. Signals should appear at bar close and never change afterwards.
  3. The alert test. Set the alert to Once Per Bar Close and log it for a week. Compare the log against what the chart shows. They should agree exactly.

The replay test is the one that matters most — it's the closest simulation of what your script knew, when.

The trade-off nobody mentions

A non-repainting script is slower. You enter one bar later than the flickering version suggested was possible, and on a 5-minute chart that can be a meaningful chunk of the move. Traders often see this, decide the non-repainting version is worse, and go back to the one that lies to them.

It isn't worse. It is the only version you could actually have traded. The faster entry never existed.

Want your script audited or rebuilt?

Send the Pine source. You'll get a straight answer on whether it repaints and what it takes to fix.

💬 WhatsApp