Tutorial

Building a Multi-Timeframe Indicator That Does Not Repaint

The most common way to add higher-timeframe context to a Pine Script indicator repaints. Here is the correct pattern, built step by step.

12 min read

Multi-timeframe analysis is one of the most valuable things you can add to a TradingView indicator. Knowing whether the daily trend agrees with your 15-minute entry signal is genuinely useful information. The problem is that the most intuitive way to fetch that data in Pine Script repaints, and most indicators built this way have been quietly giving traders false confidence in their backtests for years.

This tutorial builds a multi-timeframe RSI strategy from scratch in Pine Script v6. We will look at exactly why the naive implementation repaints, fix it with the confirmed-bar pattern, and add a trend filter so the final strategy only takes entries in the right direction. Every line of code is explained.

Why multi-timeframe indicators repaint

When you call request.security() to fetch a higher-timeframe value, Pine Script can give you one of two things: the value of the current, still-open higher-timeframe bar, or the value of the last completed, closed bar. The difference sounds small. The consequences are significant.

If you use the current open bar, the value changes on every tick as that higher-timeframe bar develops. A daily RSI that sits at 28 at 9am may be at 42 by 4pm. A signal that appeared to fire at the open of a past lower- timeframe bar, when the daily RSI was 28, will show a completely different daily RSI if you look at that same historical bar today, because the entire day has now closed and the RSI has settled at its final value. This is repainting. The signal you see on the chart now was not the signal that would have been available when that bar was live.

Here is the broken pattern that most tutorials teach:

pine
//@version=6
indicator("MTF RSI — repaints", overlay=false)

// BROKEN: reads the current, unfinished daily bar.
// This value changes on every tick, so historical signals
// will differ from what fired live.
htfRsi = request.security(syminfo.tickerid, "D", ta.rsi(close, 14))
plot(htfRsi, "Daily RSI", color=color.orange)

This looks correct. It compiles. TradingView will not warn you. But the daily RSI it plots on historical bars is always the closed daily value, because those days are finished. On the current live day, it updates continuously. The historical chart and the live experience are showing you different things.

The confirmed-bar pattern

The fix is two characters: [1] added inside therequest.security expression, and lookahead set explicitly to off. The [1] tells Pine to read the previous bar of the higher-timeframe series, which is always a closed, confirmed bar. Adding it inside the expression (before the closing parenthesis of the call) is what makes it work correctly.

pine
//@version=6
indicator("MTF RSI — confirmed bars only", overlay=false)

// FIXED: reads the PREVIOUS fully closed daily bar.
// [1] offsets to the last completed bar so the value never
// changes after that bar has closed — no repainting.
htfRsi = request.security(
    syminfo.tickerid,
    "D",
    ta.rsi(close, 14)[1],
    lookahead=barmerge.lookahead_off
)

plot(htfRsi, "Confirmed daily RSI", color=color.aqua)

Now the daily RSI value never changes after a daily bar closes. What you see on historical bars is what any indicator built this way would have shown live on those days. The backtest and the live chart agree.

If you want to check an existing indicator for this pattern before reading all the code manually, the Pine Script repainting checker scans for lookahead_on, bare request.security calls without a [1] offset, and five other common repainting patterns.

Building the full strategy

With the confirmed-bar pattern in hand, we can build something complete. The strategy below uses the daily RSI as a higher-timeframe filter and the current-timeframe RSI as the entry trigger, with a 200-period EMA as a trend filter. Entries only fire when all three conditions agree.

PineScripter generating and verifying multi-timeframe Pine Script
pine
//@version=6
strategy("MTF trend + RSI entry", overlay=true)

// ── Inputs ──────────────────────────────────────────────────────────────────
htfTimeframe = input.timeframe("D", "Higher timeframe")
rsiLen       = input.int(14,  "RSI length",         minval=2)
rsiOversold  = input.int(30,  "RSI oversold level", minval=1, maxval=49)
emaLen       = input.int(200, "Trend EMA length",   minval=10)

// ── Higher-timeframe RSI (confirmed bars only) ───────────────────────────────
// [1] ensures we read the last *closed* HTF bar. lookahead_off is the default
// but stated explicitly so the intent is clear to any future reader.
htfRsi = request.security(
    syminfo.tickerid,
    htfTimeframe,
    ta.rsi(close, 14)[1],
    lookahead=barmerge.lookahead_off
)

// ── Current-timeframe trend filter ──────────────────────────────────────────
trendEma     = ta.ema(close, emaLen)
aboveTrend   = close > trendEma

// ── Entry: current-bar RSI crosses above oversold while above trend ──────────
// ta.crossover is hoisted to its own variable first. In Pine Script v6,
// 'and' evaluates lazily — the right side can be skipped on some bars if the
// left side is already false, which would corrupt ta.crossover's internal state.
currentRsi     = ta.rsi(close, rsiLen)
rsiCrossUp     = ta.crossover(currentRsi, rsiOversold)
longCondition  = rsiCrossUp and aboveTrend and htfRsi < 50

// ── Exit: RSI crosses back above 60 ─────────────────────────────────────────
exitCondition  = ta.crossover(currentRsi, 60)

if longCondition
    strategy.entry("Long", strategy.long)

if exitCondition
    strategy.close("Long")

// ── Visual aids ──────────────────────────────────────────────────────────────
plot(trendEma, "Trend EMA", color=color.gray, linewidth=1)
bgcolor(longCondition ? color.new(color.green, 90) : na)

A few details worth noting in this code. The htfTimeframe is an input so you can change the higher timeframe from the script settings without editing the code. The ta.crossover() call is assigned to its own variable before the and expression. This is important in Pine Script v6: the and operator evaluates lazily, which means it can skip evaluating the right side of an expression if the left side is already false. Functions like ta.crossover() need to run on every bar to maintain their internal state correctly. If you put ta.crossover() directly inside an and expression, it may be skipped on some bars and produce wrong results. Hoisting it to a separate variable first guarantees it executes every bar.

Verifying the fix

There are two ways to confirm the indicator is not repainting. The first is visual: look at a bar where a signal appeared, note the higher-timeframe value shown on that bar, then wait for a few more bars to close and check whether the signal changed. If the signal stayed in place, the confirmed-bar pattern is working. If it moved or disappeared, repainting is still present.

The second is using the repainting checker on the source code. Paste the script and it will flag any remaining patterns. For a complete explanation of all five kinds of repainting in Pine Script, including ones that cannot be caught by static analysis, see the full repainting guide.

Building this faster

Writing and debugging multi-timeframe Pine Script by hand is where most errors happen, because the confirmed-bar pattern is easy to get subtly wrong and nothing in the Pine Editor will warn you. Describing the logic in plain English to PineScripter produces code that uses the confirmed-bar pattern by default and checks its own compile errors in a loop, so you can focus on the strategy logic rather than the syntax. For the full range of what is possible with multi-timeframe requests in Pine Script v6, including dynamic symbol fetching and multi-symbol scanners, see the guide to dynamic requests in Pine Script v6.


Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.