/ Notes

The one-bar repaint: why a higher-timeframe value changes after the fact

An indicator that looked right yesterday shows different markers today. You did not touch the code. Nothing was recompiled. The arrows simply moved.

The most common cause I find is one line long: a request for a higher-timeframe value. It is also the easiest to miss, because the code reads exactly like what you meant.

What the line actually does

Say you are on a 15-minute chart and want a daily EMA as a trend filter. The obvious call is:

request.security(syminfo.tickerid, "D", ta.ema(close, 12))

With Pine’s default setting, lookahead = barmerge.lookahead_off, that call behaves differently on the two kinds of bar your chart has.

  • On historical bars, the daily value only changes on the chart bar where the day closes. Every 15-minute bar earlier in the day carries the previous, finished day’s value. History only ever sees closed daily candles.
  • On the live bar, the same call returns the day that is still forming. The daily close it uses is the current price, and it moves with every tick.

So the live bar is computed from information that history never had. When the day closes and you reload the chart, those bars are recomputed as history, from the finished candle, and any marker that depended on the forming value can appear, disappear or shift. That is the repaint. It is one higher-timeframe bar wide, which is why it is easy to dismiss and hard to notice.

Two fixes that don’t work

Both of these are common, and both look reasonable.

1. Turn lookahead on. lookahead = barmerge.lookahead_on with no offset makes the problem worse in the opposite direction. On historical bars, the first 15-minute bar of each day now receives that day’s final value, computed from a close nobody could have known at 00:15. The backtest looks remarkable because it is reading the future. Live, it cannot, and the result does not carry over.

2. Offset the result outside the call. Requesting the daily EMA and then writing htf[1] shifts the series by one chart bar, fifteen minutes, not one day. On the live bar you are still reading the forming daily value, one bar late.

The fix: offset inside the call

The idiom that holds up puts the offset inside the request and turns lookahead on:

request.security(syminfo.tickerid, "D", ta.ema(close, 12)[1], lookahead = barmerge.lookahead_on)

The [1] is evaluated on the daily series, so it means the last closed day. Lookahead then lines that value up with every chart bar of the following day. History and the live bar now receive the same thing: the last confirmed daily value, and nothing from the day in progress.

This is the pattern the first template in my No-Repaint HTF pack is built around, and the two broken versions above are written into that file as comments, labelled as the mistakes they are.

What the fix costs

It is not free, and it is worth saying so plainly. The filter now lags by up to one full higher-timeframe bar. On a 15-minute chart of a market that trades 24 hours, a daily value can be up to 96 chart bars old before it updates.

That lag is the honest version of the indicator. The repainting version did not have less lag; it borrowed information from bars that had not finished and gave it back on reload. A filter that is slower but identical on history and live can be tested. One that changes after the fact cannot.

The check, in the order it costs least

  1. Search the script for request.security. For each call, ask: is the series offset by [1] inside the call, and is lookahead on? If the offset is missing, or sits outside the call, assume it repaints until shown otherwise.
  2. Run the reload test. Screenshot the markers on a clean chart, wait for at least one higher-timeframe bar to close, reload, and compare. Any marker that moved was computed from a candle that had not closed.
  3. Be suspicious of a backtest that looks better on a higher-timeframe filter than the filter could plausibly explain. Lookahead without an offset is the usual reason.

None of this needs the strategy’s logic, only the one line that reaches across timeframes.

To see the difference on your own chart, I published both calls side by side as a free, open-source TradingView script: Repaint Demo: Higher-Timeframe EMA, Both Ways. Leave it running on a live 15-minute chart for a few hours, then reload. The blue line does not move; the red line’s live path is replaced by the finished days.

If you want the ten checks I run before I believe any backtest, they fit on one page.

Get the Backtest Verification Checklist

Historical simulations are not live results. Nothing on this site is investment advice.