Edgoro
Guides

Multi-timeframe Pine Script with request.security(), without repainting

Use higher-timeframe data in Pine Script v6 with request.security(): the non-repainting pattern, tuples, timeframe checks, gaps and lower-timeframe limits.

Checking the higher timeframe before taking a trade is one of the oldest habits in trading: trade the hourly chart, but only in the direction of the 4-hour or daily trend. In Pine Script that means request.security(), and it's also where most repainting bugs come from.

This guide explains how request.security() returns data, why the default call behaves differently in history and in real time, and the pattern that keeps higher-timeframe values honest. It ends with a complete indicator you can paste.

How request.security() works

request.security(symbol, timeframe, expression, gaps, lookahead)
  • symbol — usually syminfo.tickerid, the chart's own symbol.
  • timeframe — a string such as "240" (4 hours), "D" or "W".
  • expression — what to calculate on that timeframe: close, ta.ema(close, 50), or a tuple of several values.
  • gaps and lookahead — how the result is aligned to the chart's bars (more below).

The expression is evaluated on the requested timeframe, and the result is mapped back onto your chart's bars. Each 4-hour value spans sixteen 15-minute bars, for example.

Why the default call repaints

Consider request.security(syminfo.tickerid, "D", close) on a 1-hour chart.

  • On historical bars, the daily close only becomes visible on the last hourly bar of each day, once the day is complete. The earlier hours of that day show the previous day's value.
  • On the live chart, the daily bar hasn't closed yet, so the function returns the developing daily close, which changes with every tick.

What you see in real time is not what the same bars show after a reload. Signals built on it can appear live and then be gone from history, or the other way round. That's repainting.

The non-repainting pattern

The fix is to request only confirmed higher-timeframe values: offset the expression by one bar and set lookahead = barmerge.lookahead_on. Each chart bar then sees the last closed higher-timeframe bar, both in history and live.

CallHistoryReal timeVerdict
request.security(sym, tf, close)Last closed HTF value, updating at each HTF bar's last chart barDeveloping HTF value, changes every tickRepaints
... close, lookahead = barmerge.lookahead_onThe HTF bar's final value from its first chart barDeveloping valueFuture leak in backtests: never use
... close[1] (lookahead off)The previous HTF bar, one bar later than liveThe previous HTF barHistory lags real time: inconsistent
... close[1], lookahead = barmerge.lookahead_onThe previous, closed HTF barThe previous, closed HTF barConsistent: the recommended pattern

The second row deserves emphasis: lookahead_on without the [1] offset lets historical bars see the end of a higher-timeframe bar that hadn't finished yet. Backtests built on it look spectacular and are impossible to reproduce. The [1] is not optional.

A complete example: higher-timeframe trend filter

This indicator marks EMA crosses on your chart only when they agree with the trend on a higher timeframe, defined as the higher-timeframe close being above or below its own 50-period EMA:

//@version=6
indicator("Higher-timeframe trend filter (non-repainting)", shorttitle = "HTF filter", overlay = true)

htf     = input.timeframe("240", "Higher timeframe")
htfLen  = input.int(50, "HTF EMA length", minval = 1)
fastLen = input.int(9, "Chart fast EMA", minval = 1)
slowLen = input.int(21, "Chart slow EMA", minval = 2)

if timeframe.in_seconds(timeframe.period) > timeframe.in_seconds(htf)
    runtime.error("Choose a higher timeframe that is equal to or above the chart's timeframe.")

// Confirmed values only: the expression is offset by one HTF bar and read with lookahead on, so every chart
// bar sees the last closed HTF bar, in history and in real time alike.
[htfEma, htfClose] = request.security(syminfo.tickerid, htf, [ta.ema(close, htfLen)[1], close[1]], lookahead = barmerge.lookahead_on)

htfBull = htfClose > htfEma
htfBear = htfClose < htfEma

fast        = ta.ema(close, fastLen)
slow        = ta.ema(close, slowLen)
crossUp     = ta.crossover(fast, slow)
crossDown   = ta.crossunder(fast, slow)
longSignal  = crossUp and htfBull
shortSignal = crossDown and htfBear

plot(htfEma, "HTF EMA (confirmed)", color = color.new(color.gray, 0), linewidth = 2, style = plot.style_stepline)
plot(fast, "Fast EMA", color = color.new(color.teal, 0))
plot(slow, "Slow EMA", color = color.new(color.orange, 0))
bgcolor(htfBull ? color.new(color.teal, 93) : htfBear ? color.new(color.red, 93) : na, title = "HTF bias")
plotshape(longSignal, "Long signal", shape.triangleup, location.belowbar, color.new(color.teal, 0), size = size.small)
plotshape(shortSignal, "Short signal", shape.triangledown, location.abovebar, color.new(color.red, 0), size = size.small)

alertcondition(longSignal, "Long with HTF trend", "{{ticker}}: bullish EMA cross with the higher-timeframe trend up ({{interval}})")
alertcondition(shortSignal, "Short with HTF trend", "{{ticker}}: bearish EMA cross with the higher-timeframe trend down ({{interval}})")

The higher-timeframe EMA is drawn as a step line: it changes once per higher-timeframe bar, which is exactly what a confirmed value should do. The price you pay for honesty is lag: the filter reflects the last completed 4-hour bar, not the one in progress.

Requesting several values at once

The example requests two values in one call with a tuple: [ta.ema(close, htfLen)[1], close[1]]. Each request.security() call costs time, and scripts have a limit on the number of requests, so group values from the same symbol and timeframe into one tuple. Remember to offset every element with [1].

You can request any expression, including your own functions. If the function uses ta.* calls, they run on the higher timeframe's bars, which is what you want: a daily RSI is computed from daily closes, not from hourly ones.

Gaps and alignment

The gaps parameter controls what the function returns on chart bars where no new higher-timeframe value arrived:

  • barmerge.gaps_off (default) repeats the last value on every chart bar. Best for filters and comparisons.
  • barmerge.gaps_on returns na except on the bar where a new value arrives. Useful when you want to mark the moment a higher-timeframe bar completes, or plot dots instead of a line.

Lower timeframes

Requesting a lower timeframe than the chart with request.security() only gives you one value per chart bar (by default the last intrabar), which is rarely what people expect. For all the intrabars, use request.security_lower_tf(), which returns an array per chart bar. The history available at lower timeframes is limited, so don't expect years of 1-minute data behind a daily chart.

Strategies and higher-timeframe data

Everything above matters most in strategies. A backtest that uses unconfirmed or look-ahead higher-timeframe data can show entries at prices the strategy could never have known about. If a multi-timeframe strategy's results look too good, check the request.security() calls first. The other usual suspects are in backtesting mistakes that inflate results and Pine Script repainting explained.

Checklist

  • Offset every requested value with [1] and use lookahead = barmerge.lookahead_on.
  • Never use lookahead_on without the offset.
  • Check that the requested timeframe is not below the chart's.
  • Group values from the same timeframe into one tuple.
  • Watch the script live for a few higher-timeframe bars before trusting it.

The same filter works with any indicator: try a daily Supertrend direction or a 4-hour ADX threshold in place of the EMA.

Sources and further reading

Try it with Edgoro

Ask for "a 15-minute EMA crossover indicator that only signals in the direction of the daily 200 EMA, using confirmed daily values so it doesn't repaint" and Edgoro writes the Pine Script v6 code with the request set up correctly. Build your multi-timeframe indicator with Edgoro.

Indicators describe the past and can't predict the future. Trading involves substantial risk of loss; test before you trade.

Build it with Edgoro

Describe your strategy in plain English and get a TradingView indicator in Pine Script v6 or a MetaTrader 5 Expert Advisor in minutes. Every first purchase is covered by a 7-day money-back guarantee.

Build your indicator with Edgoro