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— usuallysyminfo.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.gapsandlookahead— 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.
| Call | History | Real time | Verdict |
|---|---|---|---|
request.security(sym, tf, close) | Last closed HTF value, updating at each HTF bar's last chart bar | Developing HTF value, changes every tick | Repaints |
... close, lookahead = barmerge.lookahead_on | The HTF bar's final value from its first chart bar | Developing value | Future leak in backtests: never use |
... close[1] (lookahead off) | The previous HTF bar, one bar later than live | The previous HTF bar | History lags real time: inconsistent |
... close[1], lookahead = barmerge.lookahead_on | The previous, closed HTF bar | The previous, closed HTF bar | Consistent: 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_onreturnsnaexcept 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 uselookahead = barmerge.lookahead_on. - Never use
lookahead_onwithout 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
- Pine Script User Manual: Other timeframes and data —
request.security(),lookaheadandgapsin detail. - Pine Script v6 reference: request.security() — the full signature.
- Pine Script User Manual: Repainting — the non-repainting pattern for higher-timeframe data.
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.