◆ New — Strategy Validator, Code Fixer & Prop Compliance Checker are live. Try one free →
Home / Learn / request.security() Repainting in Pine Script: Causes and the Fix
Pine Script

request.security() Repainting in Pine Script: Causes and the Fix

A precise walkthrough of why request.security() repaints on historical bars, the look-ahead trap that makes backtests look unrealistically good, and the exact patterns that pull a confirmed higher-timeframe value instead. Educational only — validate before trading; not financial advice, and trading carries risk of loss.

What "repaint" actually means for a higher-timeframe call

Start with the symptom. You pull an hourly EMA onto a 5-minute chart with request.security(), and on historical bars the line looks clean and early — it seems to know the hourly close before the hour finished. Then you go live and the signals arrive later and worse. Nothing about your logic changed. What changed is that history and real time were never evaluated the same way.

Here is the mechanic. On a higher timeframe (HTF), a bar is only finalized at its close. A 5-minute bar that falls inside the 11:00–12:00 hourly bar cannot, in real time, know that hour's close — the hour has not ended yet. But on historical data the whole hourly bar already exists in the dataset. If your request.security() call reaches for that hourly value the naive way, every intraday bar inside the hour gets stamped with the completed hourly close. History shows a value that would not have been knowable at that moment live.

That gap between the historical stamp and what was actually knowable in real time is repainting. The plotted line silently rewrites itself once you understand it was drawn with future information. Any backtest, alert, or signal built on that line inherits the distortion — it looks decisive on the chart and degrades the instant you trade it, because live bars only ever get confirmed data.

The fix is not a flag you toggle blindly. It is understanding which value the call returns on a historical bar versus a realtime bar, and forcing both to agree on "the last thing that was genuinely confirmed." Everything below is about making those two timelines return the same answer.

The look-ahead trap (the ❌ wrong version)

This is the single most common HTF mistake, and it is worse than ordinary repainting because it is genuine look-ahead bias. Here is the ❌ wrong version — do not ship this:

// ❌ WRONG — look-ahead bias, do not use
htfClose = request.security(syminfo.tickerid, "60", close, lookahead = barmerge.lookahead_on)

What barmerge.lookahead_on does: when the HTF bar is still forming, it tells request.security() to return the value from the end of that HTF bar. On live data that is impossible — the hour has not closed — so TradingView just gives you the currently-forming value. But on historical data the completed bar already exists, so lookahead_on hands you the hourly close on the very first 5-minute bar of the hour. Your history is built on a number that, in real time, arrives up to 55 minutes later.

The damage is specific. A backtest that references this series can "enter" at the open of an hour using that hour's confirmed close, effectively trading on information from the future. The equity curve looks unusually smooth and the entries look uncannily well-timed. None of it survives live trading, because live bars never receive the future value. This is not a subtle edge — it is the model reading tomorrow's newspaper.

The rule to internalize: never use barmerge.lookahead_on without also offsetting the series by [1]. Lookahead_on paired with a [1] offset is a legitimate, well-known pattern for referencing prior confirmed HTF bars precisely. Lookahead_on alone, pulling the current unclosed bar, is the trap. Treat a bare lookahead_on in any script you inherit as a red flag to investigate first.

The correct non-repainting pattern

The reliable fix offsets the requested series by [1] and keeps lookahead off:

//@version=6
indicator("Confirmed HTF close", overlay = true)
// Educational only — validate before trading; not financial advice.

htfClose = request.security(syminfo.tickerid, "60", close[1], lookahead = barmerge.lookahead_off)
plot(htfClose, "Prev HTF close", color = color.teal)

Walk through why this returns the last confirmed HTF value. barmerge.lookahead_off tells request.security() to only use HTF data up to the current point in time — no reaching into a bar that has not closed. The [1] offset then steps back one HTF bar from whatever the current HTF bar is. So while the 11:00–12:00 hourly bar is still forming, close[1] refers to the 10:00–11:00 bar, which has definitively closed. That value is identical whether you compute it on historical data or in real time, which is exactly the property you want: no rewriting when live bars roll in.

The tradeoff is honest and worth stating plainly. You are always one HTF bar behind. Your intraday chart reacts to the previous completed hour, not the one in progress. That is the correct behavior — it is the only version of the value that was actually knowable at that moment. If a data vendor or a strategy promises you the current hour's close mid-hour, it is promising something that does not exist in real time.

Some developers prefer the lookahead_on plus close[1] form, which resolves to the same confirmed prior-bar value. Both are non-repainting. The lookahead_off plus [1] version is the one I reach for first because it fails safe: if you ever forget the offset, off is still off, and the worst case is a value that lags rather than one that cheats.

The intrabar subtlety even lookahead_off doesn't solve

Turning lookahead off is necessary but not automatically sufficient. There is a second source of repainting that has nothing to do with the lookahead flag: the current forming HTF bar updates on every tick.

Suppose you write this, expecting it to be safe because lookahead is off:

// Still repaints intrabar — reads the FORMING htf bar
htfClose = request.security(syminfo.tickerid, "60", close, lookahead = barmerge.lookahead_off)

With lookahead off there is no future bias — good. But close here refers to the current HTF bar's close, and until the hour actually ends, that "close" is just the latest price and keeps moving. On a live 5-minute chart, this value wobbles up and down for the whole hour and only settles at the top of the next hour. On history, of course, it is frozen at the final print. So the historical line and the realtime line disagree again — a signal that fires at 11:20 might un-fire by 11:40. That is repainting, even with lookahead_off.

The cure is the same [1]: read the confirmed prior bar rather than the forming one. Offsetting to close[1] steps you off the moving bar and onto the settled one. This is why the recommended pattern combines both ideas — lookahead off and a [1] offset. Lookahead off blocks future data; the offset blocks the still-moving current bar. You need both.

One more guardrail: if a confirmed HTF value ever feeds an alert, plotshape, plotchar, or a strategy entry, gate the trigger with and barstate.isconfirmed so it can only act on a closed intraday bar. That closes the last small window where an intrabar tick could produce a signal that later disappears.

A reusable non-repainting HTF snippet

Here is the pattern I keep on hand for pulling a clean higher-timeframe moving average. It is validator-safe: //@version=6, an indicator() because it is visual-only, every built-in namespaced (ta.ema, request.security, input.timeframe), and the requested series offset by [1] with lookahead off.

//@version=6
indicator("Non-repainting HTF EMA", overlay = true)
// Educational only — validate before trading; not financial advice.

htf   = input.timeframe("60", "Higher timeframe")
len   = input.int(50, "EMA length")

// Confirmed HTF value: offset the series by [1] and keep lookahead OFF.
htfEma = request.security(syminfo.tickerid, htf, ta.ema(close, len)[1], lookahead = barmerge.lookahead_off)

plot(htfEma, "HTF EMA", color = color.orange, linewidth = 2)

Notice the [1] is applied to the whole ta.ema(close, len) expression, not to close inside it. You want the confirmed EMA of the prior HTF bar, so the offset goes on the finished series that request.security returns. Computing ta.ema on the HTF and then taking [1] gives you exactly that.

If you later add a crossover signal from this line, keep it honest:

crossUp = ta.crossover(close, htfEma)
plotshape(crossUp and barstate.isconfirmed, "Cross up", shape.triangleup, location.belowbar)

The and barstate.isconfirmed guard means the marker only prints on a closed bar, so what you see in the backtest is what you would have seen live. Drop this snippet in, change the timeframe input, and you have an HTF reference that behaves identically across history and real time — which is the entire point. As always: this is educational, validate it on your own data and broker feed before relying on it, and remember that trading carries real risk of loss.

Key takeaways

Educational only — not financial advice. Trading involves substantial risk of loss.

Catch the bug that compiles.Run auditGet Pro