TradingView's built-in screener filters markets on fields TradingView already calculates. Sometimes you want something else: your own condition, on your own list of symbols, visible on the chart you're already looking at, with one alert that covers the whole list. A Pine Script indicator can do that by requesting data from other symbols and laying the results out in a table.
This guide builds a complete multi-symbol screener in Pine Script v6: RSI, trend and a custom "setup" flag for eight symbols, a colour-coded table, and an alert that names every symbol that qualifies. The example condition is a placeholder to replace with your own; the screener shows information, it doesn't recommend trades.
What the screener does
- Scans eight symbols you choose in the settings, on a timeframe you choose (daily by default), from any chart.
- For each one, shows the last completed bar's close, percentage change, RSI and whether the close is above a trend EMA.
- Flags a setup you define in one small function: here, "above the 50 EMA with RSI below 40", a pullback in an uptrend.
- Sends one alert listing every symbol that shows the setup when a new bar opens on the screening timeframe.
The script
//@version=6
indicator("Multi-symbol screener (v6)", overlay = true)
// ─── Inputs ─────────────────────────────────────────────
tf = input.timeframe("D", "Screening timeframe")
rsiLen = input.int(14, "RSI length", minval = 2)
emaLen = input.int(50, "Trend EMA length", minval = 2)
rows = input.int(8, "Symbols to show", minval = 1, maxval = 8)
s1 = input.symbol("OANDA:EURUSD", "Symbol 1", group = "Symbols")
s2 = input.symbol("OANDA:GBPUSD", "Symbol 2", group = "Symbols")
s3 = input.symbol("OANDA:USDJPY", "Symbol 3", group = "Symbols")
s4 = input.symbol("OANDA:XAUUSD", "Symbol 4", group = "Symbols")
s5 = input.symbol("BINANCE:BTCUSDT", "Symbol 5", group = "Symbols")
s6 = input.symbol("BINANCE:ETHUSDT", "Symbol 6", group = "Symbols")
s7 = input.symbol("NASDAQ:AAPL", "Symbol 7", group = "Symbols")
s8 = input.symbol("NASDAQ:NVDA", "Symbol 8", group = "Symbols")
// ─── Runs in each requested symbol's own context ───────
// Returns the last completed bar's values, so they don't change while a bar is forming.
screen() =>
r = ta.rsi(close, rsiLen)
e = ta.ema(close, emaLen)
chg = (close - close[1]) / close[1] * 100
[close[1], chg[1], r[1], e[1]]
// The setup to flag: change this to your own condition
isSetup(float c, float r, float e) =>
c > e and r < 40
// ─── Requests: one per symbol, at the top level so they run on every bar ───
[c1, g1, r1, e1] = request.security(s1, tf, screen(), lookahead = barmerge.lookahead_on)
[c2, g2, r2, e2] = request.security(s2, tf, screen(), lookahead = barmerge.lookahead_on)
[c3, g3, r3, e3] = request.security(s3, tf, screen(), lookahead = barmerge.lookahead_on)
[c4, g4, r4, e4] = request.security(s4, tf, screen(), lookahead = barmerge.lookahead_on)
[c5, g5, r5, e5] = request.security(s5, tf, screen(), lookahead = barmerge.lookahead_on)
[c6, g6, r6, e6] = request.security(s6, tf, screen(), lookahead = barmerge.lookahead_on)
[c7, g7, r7, e7] = request.security(s7, tf, screen(), lookahead = barmerge.lookahead_on)
[c8, g8, r8, e8] = request.security(s8, tf, screen(), lookahead = barmerge.lookahead_on)
symbols = array.from(s1, s2, s3, s4, s5, s6, s7, s8)
closes = array.from(c1, c2, c3, c4, c5, c6, c7, c8)
changes = array.from(g1, g2, g3, g4, g5, g6, g7, g8)
rsis = array.from(r1, r2, r3, r4, r5, r6, r7, r8)
emas = array.from(e1, e2, e3, e4, e5, e6, e7, e8)
// ─── Table: created once, filled on the last bar only ──
var table board = table.new(position.top_right, 6, 9, bgcolor = color.new(color.black, 80), border_width = 1)
fmtPrice(float x) =>
na(x) ? "n/a" : str.tostring(x, x >= 10 ? "#.##" : "#.#####")
if barstate.islast
headers = array.from("Symbol", "Close", "Chg %", "RSI", "Trend", "Setup")
for col = 0 to 5
table.cell(board, col, 0, array.get(headers, col), text_color = color.white, text_size = size.small)
for i = 0 to rows - 1
c = array.get(closes, i)
chg = array.get(changes, i)
r = array.get(rsis, i)
e = array.get(emas, i)
row = i + 1
name = array.get(str.split(array.get(symbols, i), ":"), -1)
trendUp = c > e
rsiBg = r >= 70 ? color.new(color.red, 60) : r <= 30 ? color.new(color.teal, 60) : na
setupBg = isSetup(c, r, e) ? color.new(color.yellow, 50) : na
table.cell(board, 0, row, name, text_color = color.white, text_size = size.small, text_halign = text.align_left)
table.cell(board, 1, row, fmtPrice(c), text_color = color.white, text_size = size.small)
table.cell(board, 2, row, na(chg) ? "n/a" : str.tostring(chg, "#.##"), text_color = chg >= 0 ? color.lime : color.red, text_size = size.small)
table.cell(board, 3, row, na(r) ? "n/a" : str.tostring(r, "#.#"), text_color = color.white, bgcolor = rsiBg, text_size = size.small)
table.cell(board, 4, row, trendUp ? "Up" : "Down", text_color = trendUp ? color.lime : color.red, text_size = size.small)
table.cell(board, 5, row, isSetup(c, r, e) ? "Yes" : "", text_color = color.white, bgcolor = setupBg, text_size = size.small)
// ─── One alert for every symbol ────────────────────────
// Values change when a new bar opens on the screening timeframe
if timeframe.change(tf)
hits = ""
for i = 0 to rows - 1
if isSetup(array.get(closes, i), array.get(rsis, i), array.get(emas, i))
hits += array.get(symbols, i) + " "
if hits != ""
alert("Setups on " + tf + ": " + hits, alert.freq_once_per_bar_close)
Add it to any chart as a new indicator; the table appears in the top-right corner. If you haven't used the Pine Editor before, how to add a Pine Script indicator to TradingView covers the steps.
How it works
Calculations run in each symbol's own context
The key line is request.security(s1, tf, screen(), ...). The function passed as the third argument is evaluated on the requested symbol's bars, not on the chart's. ta.rsi(close, rsiLen) inside screen() is the RSI of EUR/USD's daily closes, calculated over EUR/USD's own history, even if your chart shows Apple on a 5-minute timeframe.
That's the most common mistake in hand-written screeners: requesting each symbol's close and then calculating the RSI on the chart. The result is an RSI of a series sampled at the chart's bars, which is a different number from the one you'd see on the symbol's own chart.
screen() returns several values at once as a tuple, and [c1, g1, r1, e1] = ... unpacks them. Fetching four values for a symbol costs one request, not four.
Completed bars only
Each value in screen() is taken one bar back (close[1], r[1]) and the request uses lookahead = barmerge.lookahead_on. Together, that returns the last completed bar on the screening timeframe, on historical bars and in real time alike. Without it, the daily RSI would move with every tick until the day closes, and an alert could fire on a value that's gone by the close. The reasoning behind this pattern is covered in multi-timeframe Pine Script with request.security().
The trade-off: during the day, the table shows yesterday's completed values. For a screener that's usually what you want, since a setup on a finished bar doesn't disappear.
Requests at the top level
Pine Script v6 allows request.security() inside loops, which looks like the natural way to scan a list. In our test, calling it in a for loop with screen() produced the warning that the function "should be called on each calculation for consistency". Eight explicit lines at the top level compile cleanly and run on every bar, which is why the script uses them.
The table
table.new() is called once, thanks to var; the cells are filled only when barstate.islast is true. Tables only display on the last bar anyway, so filling them on every historical bar would be wasted work. A few details:
str.split(symbol, ":")with index-1keeps the ticker and drops the exchange prefix. Negative array indices, counting from the end, are new in v6.fmtPrice()shows two decimals for prices of 10 and above and five below, so EUR/USD and Bitcoin both read sensibly. Missing data shows "n/a" instead of a blank cell.- RSI cells turn red at 70 and above and teal at 30 and below; the setup column is highlighted when
isSetup()is true.
One alert for the whole list
Values only change when a new bar opens on the screening timeframe, which is what timeframe.change(tf) detects. At that moment the script builds a list of the symbols that show the setup and sends it with alert(). Create a single alert on the indicator, choose Any alert() function call, and every symbol is covered. The two alert functions are compared in alertcondition() vs alert().
Alerts run on TradingView's servers on the chart's timeframe, so with a daily screening timeframe you can attach the script to an hourly chart and get the alert soon after the daily bar changes.
Customising it
Your own setup. Replace the body of isSetup(). It receives the completed bar's close, RSI and EMA; for anything else, add it to screen() and to the tuple. For example, an "oversold in an uptrend" flag becomes c > e and r < 30, and a momentum flag c > e and r > 60.
More columns. To show ATR as a percentage of price, calculate ta.atr(14) / close * 100 inside screen(), return it with the other values (offset by one bar), add an array and a column. Increase the table's column count in table.new() to match.
More symbols. Add inputs, request lines and array entries, and raise the row count in table.new() and the maxval of the "Symbols to show" input. Mind the limits below.
Other timeframes. The screening timeframe input accepts any timeframe. Weekly screening on a daily chart works the same way.
Limits and caveats
- Request limits. A script can only make a limited number of unique
request.*()calls, and every request loads another symbol's history, which slows the script. The current limit is on the Limitations page of the Pine Script User Manual; a few dozen symbols is the practical ceiling. - Data permissions. Symbols from exchanges that need a data subscription may return delayed data or nothing at all. The "n/a" cells make that visible.
- Different sessions. A forex "daily" bar, a crypto daily bar and a stock's daily session don't close at the same moment. Comparing their daily changes side by side is fine; treating them as simultaneous isn't.
- History. Each symbol needs enough bars on the screening timeframe for the EMA and RSI to warm up. Newly listed symbols show "n/a" or unstable values at first.
- TradingView's own tools. Depending on your plan, TradingView also offers a Pine Screener that runs an indicator over a watchlist. The script approach works on the chart, on any plan, and keeps the logic in code you control.
Sources and further reading
- Pine Script User Manual: Other timeframes and data —
request.security(), tuples, dynamic requests and look-ahead. - Pine Script User Manual: Tables — creating and updating tables efficiently.
- Pine Script User Manual: Limitations — request limits and other per-script limits.
Try it with Edgoro
Describe the screener you want — "scan these ten crypto pairs on the 4-hour chart, show RSI(14), distance from the 200 EMA and 24-hour change, flag RSI below 30 above the EMA, one alert for all" — and Edgoro's Pine Script generator writes the v6 indicator. The free AI Pine Script generator handles smaller requests without an account. The RSI calculation itself is explained on the RSI indicator page.
A screener describes past and current data; it doesn't predict anything. Check any setup on the symbol's own chart before acting on it. Trading involves substantial risk of loss.