request.security and multi-timeframe data
Lesson 8 · about 12 min
request.security() fetches a series from another symbol or another timeframe and lines it up with the bars of your chart. A daily moving average on a 15-minute chart, the S&P 500 close on a single stock's chart, a weekly RSI as a filter. It is powerful, it is the source of most "this backtest looked incredible and then failed live" stories, and the difference between the two comes down to one argument.
The call
request.security(symbol, timeframe, expression, gaps, lookahead)
symbol: a string such as"NASDAQ:AAPL"or, most often,syminfo.tickeridfor the chart's own symbol.timeframe: a string in TradingView's format:"1","5","60"(minutes),"D","W","M".""means the chart's timeframe. This must besimple, so it comes from a literal orinput.timeframe().expression: what to evaluate in that context. It can beclose,ta.sma(close, 20), a tuple[open, high, low, close], or a user function.gaps:barmerge.gaps_off(default, repeat the last value) orbarmerge.gaps_on(naon chart bars with no new higher-timeframe bar).lookahead:barmerge.lookahead_off(default) orbarmerge.lookahead_on.
//@version=6
indicator("Daily SMA on any chart", overlay=true)
htf = input.timeframe("D", "Higher timeframe")
dailySma = request.security(syminfo.tickerid, htf, ta.sma(close, 20))
plot(dailySma, "Daily SMA 20", color=color.blue, linewidth=2)
On a 15-minute chart this draws a step-shaped line that changes once a day. Which value is shown during each day is the subject of the rest of the lesson.
The lookahead problem
Think about a 15-minute bar at 10:30 on Tuesday. The daily bar for Tuesday has not closed. What should "Tuesday's daily close" be on that bar?
With lookahead=barmerge.lookahead_on and a plain close, historical bars show Tuesday's final close at 10:30 on Tuesday. The script sees the future. A strategy using this as a filter will look wonderful in the tester because it is filtering on information that did not exist yet. In real time the same script sees only the developing close, so the live behaviour does not match the backtest at all.
With the default lookahead_off, historical bars show the previous completed daily bar's value until the new daily bar closes, but in real time the expression is evaluated on the developing daily bar, so the live value updates all day and then gets replaced when the day ends. History and real time still disagree, just less dangerously.
The repaint-safe form
Offset the expression by one bar in the higher timeframe, then turn lookahead on:
//@version=6
indicator("Confirmed daily SMA", overlay=true)
htf = input.timeframe("D", "Higher timeframe")
confirmedSma = request.security(syminfo.tickerid, htf, ta.sma(close, 20)[1], lookahead=barmerge.lookahead_on)
developingClose = request.security(syminfo.tickerid, htf, close)
plot(confirmedSma, "Daily SMA 20 (last completed day)", color=color.blue, linewidth=2)
plot(developingClose, "Daily close (developing)", color=color.gray)
ta.sma(close, 20)[1] is the average as of the last completed daily bar. lookahead_on makes the historical bars show it from the first intraday bar of the day rather than only after the day ends. Every bar, historical or live, now shows a value that was genuinely known at that moment, and the plot never changes after the fact. This [1] plus lookahead_on pattern is the standard non-repainting idiom, and the reason the reference manual warns about lookahead_on is that people use it without the [1].
Key idea:
request.security(..., expr[1], lookahead=barmerge.lookahead_on)gives you the last completed higher-timeframe value on every bar with no future leak.lookahead_onwithout the[1]is the single most common cause of impossible backtests.
Other symbols
The symbol argument can be anything TradingView charts. A market-breadth filter:
spxUp = request.security("SP:SPX", "D", close[1] > ta.sma(close, 50)[1], lookahead=barmerge.lookahead_on)
bgcolor(spxUp ? color.new(color.green, 92) : color.new(color.red, 92))
Note the expression is a boolean computed inside the other context; both close[1] and the SMA are evaluated on SPX daily bars. If a symbol does not exist the script errors; ignore_invalid_symbol=true returns na instead.
Limits and costs
- A script may make at most 40
request.*calls, and each one costs compute time. Fetch a tuple[o, h, l, c]in one call rather than four calls. - The requested timeframe should be higher than or equal to the chart's. Requesting a lower timeframe with
request.securitygives you one sampled value per chart bar, which is rarely what you want;request.security_lower_tfreturns an array of all lower-timeframe values inside each chart bar. - Data starts where the other symbol's history starts, so the first bars may be
na. - Sessions matter: a daily bar for a futures contract and for its cash index end at different times. Check what
"D"means for the symbol you are requesting.
Try it: On a 5-minute chart, plot both
request.security(syminfo.tickerid, "D", close)and the[1]+lookahead_onversion. Watch the live bar for a few minutes and see which line moves. Then set the first one tolookahead_onwithout[1]and look at where it changes on historical days.
Recap
request.security(symbol, timeframe, expression)evaluates the expression in another context and aligns it to chart bars.- Timeframe strings are
"1","5","60","D","W","M"and must besimple. lookahead_onwith an unoffset expression leaks future values into history;lookahead_offstill differs between history and real time.expr[1]withlookahead_onshows the last completed higher-timeframe value on every bar and never repaints.- Bundle requests into tuples; at most 40 calls; use
request.security_lower_tffor intrabar data.
See it drawn
Original diagrams for the ideas on this page. Illustrative, not real market data.