
Monitoring multiple symbols at once is essential for traders who manage watchlists, scan for setups across sectors, or run rotation strategies. Before Pine Script v6, building a multi-symbol dashboard required hardcoding a separate request.security() call for each ticker, resulting in rigid scripts that were painful to modify1. Version 6 changes this with dynamic requests, which let you use series string values as the symbol argument in request.security() calls inside loops2.
This tutorial builds a real-time multi-symbol dashboard from scratch. The finished product is a table that displays the ticker name, current price, percentage change, RSI value, and relative volume for each symbol. You define the tickers in a single text input, and the script handles the rest dynamically.
The dashboard is a table anchored to the top-right corner of the chart. Each row represents one symbol and displays five columns of data:
The table updates on every bar, including realtime ticks. Color coding highlights symbols that are making strong moves or hitting extreme RSI levels, so you can spot opportunities across your entire watchlist without switching charts.
In Pine Script v5, the symbol argument of request.security() required a simple string, meaning the value had to be known at compile time1. You could not pass a variable that changed based on a loop index or runtime condition. This forced developers to write one request.security() call per symbol, leading to repetitive code that was difficult to maintain.
Pine Script v6 enables dynamic requests by default2. This means request.security() now accepts series string values for its symbol and timeframe arguments3. You can store ticker names in an array, loop through them, and call request.security() on each iteration with a different symbol. The Pine runtime handles each call independently.
The critical constraint is the 40-call limit. Pine Script allows a maximum of 40 request.*() calls per script execution3. Since each symbol in the loop generates at least one request.security() call, dashboards should be limited to around 10 to 15 symbols. If you need a single data point per symbol, you can theoretically reach 40 symbols. If you request multiple data points per symbol using tuple returns, each tuple still counts as one call3.
We use input.text_area() to let users enter a comma-separated list of tickers3. This is more flexible than creating individual input.symbol() fields for each ticker because users can add or remove symbols by editing a single text field.
//@version=6
indicator("Multi-Symbol Dashboard", overlay = true)
// --- Inputs ---
string symbolListInput = input.text_area(
"SPY,QQQ,IWM,DIA,AAPL,MSFT,NVDA,AMZN,GOOGL,META",
"Symbols (comma-separated)",
tooltip = "Enter up to 13 ticker symbols separated by commas. Each symbol uses 3 request calls."
)
int rsiLengthInput = input.int(14, "RSI Length", minval = 2)
int rvolLengthInput = input.int(20, "RVOL Average Length", minval = 1)
The default list includes the major US index ETFs and the largest mega-cap stocks. Users can replace these with any valid TradingView ticker symbols, including forex pairs, crypto, or futures contracts. The tooltip reminds users of the practical limit based on request call usage.
We also expose the RSI and relative volume lookback lengths as inputs so users can tune the calculations without modifying the code.
The comma-separated input string needs to be split into an array of individual ticker strings. We use str.split() to break the string by commas and store the resulting substrings in an array3.
// --- Parse Symbols ---
var array symbols = str.split(symbolListInput, ",")
The var keyword ensures the array is created only once on the first bar and persists across all subsequent executions4. Since the input value does not change during the script's lifetime, there is no reason to re-split the string on every bar.
Note that request.security() ignores leading and trailing whitespace in its symbol argument. If a user types "AAPL, MSFT, NVDA" with spaces after the commas, the requests will still work correctly. However, the spaces will appear in the table's symbol column. For a cleaner display, you could use str.replace_all() to strip spaces from each element.
This is where dynamic requests make the magic happen. We loop through the symbols array and call request.security() for each ticker. To minimize request calls, we use a tuple return to fetch the close price, previous close, RSI, current volume, and average volume in a single call per symbol.
// --- Data Storage Arrays ---
var array prices = array.new()
var array changes = array.new()
var array rsiValues = array.new()
var array rvolValues = array.new()
// Clear arrays on each execution to avoid stale data.
prices.clear()
changes.clear()
rsiValues.clear()
rvolValues.clear()
// --- Dynamic Data Requests ---
for [i, symbol] in symbols
[reqClose, reqPrevClose, reqRSI, reqVol, reqAvgVol] = request.security(
symbol, timeframe.period,
[close, close[1], ta.rsi(close, rsiLengthInput), volume, ta.sma(volume, rvolLengthInput)]
)
prices.push(reqClose)
float pctChange = reqPrevClose != 0 ? ((reqClose - reqPrevClose) / reqPrevClose) * 100 : 0.0
changes.push(pctChange)
rsiValues.push(reqRSI)
float rvol = reqAvgVol != 0 ? reqVol / reqAvgVol : 0.0
rvolValues.push(rvol)
Each iteration of the for...in loop calls request.security() with a different symbol from the array2. The tuple return bundles five data points into a single request call3. This is critical for staying within the 40-call limit. With 10 symbols, you use 10 calls. With 13 symbols, you use 13 calls, leaving headroom for other potential requests in the script.
The percentage change calculation divides the difference between the current and previous close by the previous close. The relative volume divides the current bar's volume by its 20-bar simple moving average. Both include a zero-denominator guard to prevent division errors.
We clear the arrays on every execution because the loop rebuilds them with fresh data from the latest bar. Without clearing, the arrays would grow indefinitely as new values are pushed on each execution.
The final step creates and populates a table to display the data. We build the table on the last bar to minimize drawing updates, which is important for performance.
// --- Table Display ---
if barstate.islast
int numSymbols = symbols.size()
var table dashboard = table.new(
position.top_right, 6, numSymbols + 1,
bgcolor = color.new(color.black, 30),
border_color = color.new(color.gray, 60),
border_width = 1,
force_overlay = true
)
// Header row
dashboard.cell(0, 0, "Symbol", text_color = color.white, text_size = size.small)
dashboard.cell(1, 0, "Price", text_color = color.white, text_size = size.small)
dashboard.cell(2, 0, "Change", text_color = color.white, text_size = size.small)
dashboard.cell(3, 0, "RSI", text_color = color.white, text_size = size.small)
dashboard.cell(4, 0, "RVOL", text_color = color.white, text_size = size.small)
dashboard.cell(5, 0, "Signal", text_color = color.white, text_size = size.small)
for i = 0 to numSymbols - 1
int row = i + 1
string sym = symbols.get(i)
float price = prices.get(i)
float change = changes.get(i)
float rsi = rsiValues.get(i)
float rvol = rvolValues.get(i)
// Color coding
color changeColor = change >= 0 ? color.green : color.red
color rsiColor = rsi > 70 ? color.red : rsi < 30 ? color.green : color.gray
color rvolColor = rvol > 2.0 ? color.yellow : color.gray
// Signal logic
string signal = rsi < 30 and rvol > 1.5 ? "WATCH" : rsi > 70 and rvol > 1.5 ? "ALERT" : "-"
color signalColor = signal == "WATCH" ? color.green : signal == "ALERT" ? color.red : color.gray
// Populate cells
dashboard.cell(0, row, sym, text_color = color.white, text_size = size.small)
dashboard.cell(1, row, str.tostring(price, format.mintick), text_color = color.white, text_size = size.small)
dashboard.cell(2, row, str.tostring(change, "#.##") + "%", text_color = changeColor, text_size = size.small)
dashboard.cell(3, row, str.tostring(rsi, "#.#"), text_color = rsiColor, text_size = size.small)
dashboard.cell(4, row, str.tostring(rvol, "#.##") + "x", text_color = rvolColor, text_size = size.small)
dashboard.cell(5, row, signal, text_color = signalColor, text_size = size.small)
The table has six columns and one row per symbol, plus a header row. The force_overlay parameter ensures the table renders on the main chart pane regardless of whether the indicator is assigned to a separate pane3.
Color coding is applied to three columns. The Change column is green for positive moves and red for negative. The RSI column highlights overbought values above 70 in red and oversold values below 30 in green. The RVOL column turns yellow when relative volume exceeds 2x the average, flagging unusual activity.
The Signal column combines RSI and RVOL into a simple screening signal. A "WATCH" signal appears when RSI is oversold and relative volume is elevated, suggesting a potential reversal with participation. An "ALERT" signal appears when RSI is overbought with high volume, suggesting a potential exhaustion point. These are starting points for further investigation, not standalone trade signals.
Here is the full dashboard indicator ready to paste into the Pine Editor.
//@version=6
indicator("Multi-Symbol Dashboard", overlay = true)
// --- Inputs ---
string symbolListInput = input.text_area(
"SPY,QQQ,IWM,DIA,AAPL,MSFT,NVDA,AMZN,GOOGL,META",
"Symbols (comma-separated)"
)
int rsiLengthInput = input.int(14, "RSI Length", minval = 2)
int rvolLengthInput = input.int(20, "RVOL Average Length", minval = 1)
// --- Parse Symbols ---
var array symbols = str.split(symbolListInput, ",")
// --- Data Storage ---
var array prices = array.new()
var array changes = array.new()
var array rsiValues = array.new()
var array rvolValues = array.new()
prices.clear()
changes.clear()
rsiValues.clear()
rvolValues.clear()
// --- Dynamic Data Requests ---
for [i, symbol] in symbols
[reqClose, reqPrevClose, reqRSI, reqVol, reqAvgVol] = request.security(
symbol, timeframe.period,
[close, close[1], ta.rsi(close, rsiLengthInput), volume, ta.sma(volume, rvolLengthInput)]
)
prices.push(reqClose)
float pctChange = reqPrevClose != 0 ? ((reqClose - reqPrevClose) / reqPrevClose) * 100 : 0.0
changes.push(pctChange)
rsiValues.push(reqRSI)
float rvol = reqAvgVol != 0 ? reqVol / reqAvgVol : 0.0
rvolValues.push(rvol)
// --- Table Display ---
if barstate.islast
int numSymbols = symbols.size()
var table dashboard = table.new(
position.top_right, 6, numSymbols + 1,
bgcolor = color.new(color.black, 30),
border_color = color.new(color.gray, 60),
border_width = 1, force_overlay = true
)
dashboard.cell(0, 0, "Symbol", text_color = color.white, text_size = size.small)
dashboard.cell(1, 0, "Price", text_color = color.white, text_size = size.small)
dashboard.cell(2, 0, "Change", text_color = color.white, text_size = size.small)
dashboard.cell(3, 0, "RSI", text_color = color.white, text_size = size.small)
dashboard.cell(4, 0, "RVOL", text_color = color.white, text_size = size.small)
dashboard.cell(5, 0, "Signal", text_color = color.white, text_size = size.small)
for i = 0 to numSymbols - 1
int row = i + 1
string sym = symbols.get(i)
float price = prices.get(i)
float change = changes.get(i)
float rsi = rsiValues.get(i)
float rvol = rvolValues.get(i)
color changeColor = change >= 0 ? color.green : color.red
color rsiColor = rsi > 70 ? color.red : rsi < 30 ? color.green : color.gray
color rvolColor = rvol > 2.0 ? color.yellow : color.gray
string signal = rsi < 30 and rvol > 1.5 ? "WATCH" : rsi > 70 and rvol > 1.5 ? "ALERT" : "-"
color signalColor = signal == "WATCH" ? color.green : signal == "ALERT" ? color.red : color.gray
dashboard.cell(0, row, sym, text_color = color.white, text_size = size.small)
dashboard.cell(1, row, str.tostring(price, format.mintick), text_color = color.white, text_size = size.small)
dashboard.cell(2, row, str.tostring(change, "#.##") + "%", text_color = changeColor, text_size = size.small)
dashboard.cell(3, row, str.tostring(rsi, "#.#"), text_color = rsiColor, text_size = size.small)
dashboard.cell(4, row, str.tostring(rvol, "#.##") + "x", text_color = rvolColor, text_size = size.small)
dashboard.cell(5, row, signal, text_color = signalColor, text_size = size.small)
The dashboard is designed to be extended. Here are ideas for additional columns you can add by expanding the tuple in the request.security() call.
Add close and a 200-period SMA to the tuple, then calculate the percentage distance from the moving average. This tells you which symbols are extended versus those trading near their mean.
// Add to the request tuple:
// ta.sma(close, 200)
// Then calculate:
// float maDistance = ((reqClose - reqSMA200) / reqSMA200) * 100
Include ta.atr(14) in the tuple to display each symbol's current volatility. Normalize it as a percentage of price to compare volatility across symbols with very different price levels.
Create multiple text area inputs, one per sector, and process each group separately. Use different background colors for each sector's rows in the table to visually organize the dashboard by market segment.
Dynamic requests are powerful but come with constraints that you must plan around.
40-call limit: Each script execution can make a maximum of 40 request.*() calls3. Every iteration of the loop that contains request.security() counts as a separate call. If your tuple requests five values, that still counts as one call per symbol because tuples are returned from a single request.security() invocation.
Performance impact: More symbols mean more data to fetch and process. If you notice the script running slowly, reduce the symbol count or simplify the calculations within the tuple expression.
Data availability: Not all symbols are available on all exchanges. If a symbol in the list is invalid or has no data, request.security() returns na for all values3. The table will show "NaN" for that row. Consider adding an na check and displaying "N/A" instead for a cleaner presentation.
Timeframe consistency: The request.security() call uses timeframe.period, which matches the chart's current timeframe. If you switch from a daily chart to a 5-minute chart, the RSI and RVOL calculations will use 5-minute bars. Make sure your lookback lengths are appropriate for the timeframes you plan to use.
A multi-symbol dashboard naturally leads to multi-symbol trading. When the dashboard flags a "WATCH" or "ALERT" signal, the next question is whether you can act on it automatically.
With TradersPost, you can build a Pine Script strategy version of this dashboard that generates entry and exit signals for each symbol, then sends webhook alerts to TradersPost for automated execution. TradersPost supports multi-asset portfolios across stocks, futures, options, and crypto, making it the ideal execution layer for rotation and screening strategies5.
The workflow is straightforward. Convert the indicator to a strategy, add strategy.entry() and strategy.close() calls based on the signal logic, set up TradingView alerts, and connect them to your TradersPost webhook. TradersPost handles the order routing to your broker, position sizing, and execution confirmation5. You focus on the analysis while the automation handles the trades.
1 Pine Script v5 to v6 Migration Guide
2 Pine Script Release Notes
3 Pine Script v6 Language Reference
4 Pine Script User Manual
5 TradersPost - Automated Trading Platform