
Pine Script v6 introduced dynamic requests, one of the most significant upgrades to the language in years1. For the first time, request.security() and all other request.*() functions can accept series string arguments, execute inside loops and conditional blocks, and work within exported library functions2. This unlocks multi-symbol dashboards, watchlist scanners, and dynamic sector rotation strategies that were simply impossible in v5.
If you have ever tried to build a Pine Script indicator that loops through a list of symbols and pulls data for each one, you know the frustration. In v5, the symbol and timeframe arguments required simple string values, meaning they had to be known at compile time and could never change during execution2. Every symbol needed its own hardcoded request.security() call. That limitation is now gone.
In this guide, we will walk through exactly how dynamic requests work, demonstrate practical code patterns for loops, conditionals, and libraries, and build two real-world examples: a multi-symbol dashboard and a dynamic sector rotation strategy. We will also cover the 40-call execution limit and performance best practices.
In Pine Script v5, all request.*() function calls required simple string arguments for the symbol and timeframe parameters2. A simple string must be known on the first script execution and cannot change afterward. This meant every request.security() call was permanently bound to one specific symbol and timeframe combination, and it had to execute in the global scope.
Pine Script v6 changes the default behavior so that all request.*() calls are dynamic2. Dynamic requests can:
series string arguments for symbol, timeframe, and other context-defining parameters3request.*() call evaluates another4Pine v6 scripts enable dynamic requests by default. If you need backward-compatible behavior for any reason, you can disable it by adding dynamic_requests = false to your indicator() or strategy() declaration3.
The core technical change is the type qualifier upgrade for request parameters. Here is the difference at a glance:
request.security("AAPL", "1D", close)2request.security(myDynamicSymbol, timeframe.period, close)3This means a single request.security() instance in your code can fetch data from different symbols or timeframes on different bars, which is exactly what makes loops and conditionals possible.
The most common use case for dynamic requests is iterating over an array of symbols and pulling data for each one. In v5, you would need a separate request.security() call for every symbol, hard-coded in the global scope2. In v6, a single call inside a for...in loop handles everything4.
Here is a working example that requests volume data for ten forex pairs and calculates aggregate statistics:
//@version=6
indicator("Multi-Symbol Loop Demo", format = format.volume)
//@variable An array of symbol strings to request data for.
var array<string> symbols = array.from(
"EURUSD", "USDJPY", "GBPUSD", "AUDUSD", "USDCAD",
"USDCHF", "NZDUSD", "EURJPY", "GBPJPY", "EURGBP"
)
//@variable An array to hold retrieved volume for each symbol.
array<float> requestedData = array.new<float>()
// Loop through symbols and request volume for each one.
for symbol in symbols
float data = request.security("OANDA:" + symbol, timeframe.period, volume)
requestedData.push(data)
// Calculate aggregate statistics from the collected data.
float avgVolume = requestedData.avg()
float maxVolume = requestedData.max()
float minVolume = requestedData.min()
// Plot the results.
plot(avgVolume, "Average volume", color.gray, 3)
plot(maxVolume, "Highest volume", color.teal, 3)
plot(minVolume, "Lowest volume", color.maroon, 3)
This pattern is clean and scalable. You define your symbol list once, loop through it, and collect the results. The request.security() call inside the loop executes once per symbol per bar, and each iteration pulls data from a different data feed.
There is one important constraint to understand: the expression argument of a request.*() call inside a loop cannot depend on the loop variable or any mutable variable that changes within the loop scope4.
For example, this code causes a compilation error because the expression volume * (10 - i) depends on the loop index i:
// This causes a compilation error.
for [i, symbol] in symbols
float data = request.security("OANDA:" + symbol, timeframe.period, volume * (10 - i))
requestedData.push(data)
The symbol and timeframe parameters can depend on loop variables, but the expression parameter cannot. If you need to weight or transform the returned data, do so after the request returns.
Dynamic requests also work inside if and switch blocks4. This means a request.*() call can activate zero or one time per execution, depending on a condition. In v5, every request call ran on every bar regardless of any surrounding logic.
Here is an example that switches the data source based on a user-selected asset class:
//@version=6
indicator("Conditional Request Demo", overlay = true)
//@variable User input to select the asset class.
string assetClass = input.string("Equities", "Asset Class",
options = ["Equities", "Crypto", "Forex"])
//@variable The requested close price from a benchmark symbol.
float benchmarkClose = switch assetClass
"Equities" => request.security("AMEX:SPY", "1D", close)
"Crypto" => request.security("BINANCE:BTCUSDT", "1D", close)
"Forex" => request.security("OANDA:EURUSD", "1D", close)
plot(benchmarkClose, "Benchmark", color.orange, 2)
Each branch of the switch contains a different request.security() call. Only the branch matching the user's input executes on any given bar. This keeps your code organized and avoids unnecessary data requests.
You can also use a single request.security() call with a series string symbol that changes based on conditions:
//@version=6
indicator("Series Symbol Demo")
//@variable Switches between two symbols every 20 bars.
string dynamicSymbol = bar_index % 40 < 20 ? "NASDAQ:AAPL" : "NASDAQ:MSFT"
float reqClose = request.security(dynamicSymbol, timeframe.period, close)
plot(reqClose, "Dynamic close", color.purple, 3)
// Label each symbol change for clarity.
if dynamicSymbol != dynamicSymbol[1]
label.new(bar_index, reqClose, dynamicSymbol, textcolor = color.white)
This pattern is useful when you want to rotate through symbols programmatically based on bar conditions, time of day, or indicator signals.
Libraries with dynamic requests enabled can export functions containing request.*() calls4. This is a powerful feature for building reusable data-fetching modules that other scripts can import.
Here is a library that exports a function to fetch confirmed higher-timeframe OHLC prices:
//@version=6
library("DynamicRequests")
//@function Requests confirmed HTF OHLC data for a specified ticker and timeframe.
//@param tickerID The ticker identifier to request data for.
//@param timeframe The timeframe of the requested data.
//@returns A tuple of the last confirmed open, high, low, and close.
export htfPrices(string tickerID, string timeframe) =>
if timeframe.in_seconds() >= timeframe.in_seconds(timeframe)
runtime.error("The timeframe argument must be higher than the chart timeframe.")
request.security(tickerID, timeframe, [open[1], high[1], low[1], close[1]],
lookahead = barmerge.lookahead_on)
Any script that imports this library can call htfPrices("NASDAQ:AAPL", "1D") to get daily OHLC data without writing its own request.security() logic. The [1] offset combined with barmerge.lookahead_on is the recommended method to avoid repainting in higher-timeframe requests.
Key constraint: The expression argument inside an exported library function cannot depend on the function's parameters4. The symbol and timeframe parameters can use the function arguments, but the evaluated expression must be independent of them.
Let us build a practical multi-symbol dashboard that displays the current price and daily change for a list of stocks. This combines dynamic requests in a loop with table drawing:
//@version=6
indicator("Multi-Symbol Dashboard", overlay = true)
//@variable A user-defined list of symbols.
string symbolListInput = input.text_area(
"NASDAQ:AAPL,NASDAQ:MSFT,NASDAQ:GOOGL,NASDAQ:NVDA,NASDAQ:AMZN",
"Symbols (comma-separated)")
//@variable An array created by splitting the input string.
var array<string> symbols = str.split(symbolListInput, ",")
//@variable A dashboard table displayed in the top-right corner.
var table dashboard = table.new(position.top_right, 3, symbols.size() + 1, chart.fg_color)
// Initialize header row on the first bar.
if barstate.isfirst
dashboard.cell(0, 0, "Symbol", text_color = chart.bg_color, text_size = size.small)
dashboard.cell(1, 0, "Price", text_color = chart.bg_color, text_size = size.small)
dashboard.cell(2, 0, "Change %", text_color = chart.bg_color, text_size = size.small)
// Update data on the last bar only to minimize resource usage.
if barstate.islast
for [i, symbol] in symbols
float symClose = request.security(symbol, timeframe.period, close)
float symPrevClose = request.security(symbol, "1D", close[1],
lookahead = barmerge.lookahead_on)
float changePercent = (symClose - symPrevClose) / symPrevClose * 100
color changeColor = changePercent >= 0 ? color.green : color.red
dashboard.cell(0, i + 1, symbol,
text_color = chart.bg_color, text_size = size.small)
dashboard.cell(1, i + 1, str.tostring(symClose, format.mintick),
text_color = chart.bg_color, text_size = size.small)
dashboard.cell(2, i + 1, str.tostring(changePercent, "#.##") + "%",
text_color = changeColor, text_size = size.small)
This dashboard loops through each symbol, requests the current close and previous daily close, calculates the percentage change, and color-codes it green or red. The table updates only on the last bar to minimize drawing operations.
Dynamic requests are especially powerful for sector rotation strategies, where you compare relative strength across multiple instruments and allocate to the strongest performer. Here is a strategy skeleton that ranks symbols by momentum:
//@version=6
strategy("Dynamic Sector Rotation", overlay = false)
//@variable An array of sector ETF symbols to compare.
var array<string> sectors = array.from(
"AMEX:XLK", "AMEX:XLF", "AMEX:XLE", "AMEX:XLV",
"AMEX:XLY", "AMEX:XLP", "AMEX:XLI", "AMEX:XLU"
)
int lookback = input.int(20, "Momentum Lookback", minval = 5)
//@variable Tracks the highest momentum value found.
var float bestMomentum = na
//@variable Stores the symbol with the highest momentum.
var string bestSymbol = na
float currentBest = -999.0
string currentBestSym = ""
// Loop through sectors and calculate momentum for each.
for sector in sectors
float sectorClose = request.security(sector, "1D", close)
float sectorPast = request.security(sector, "1D", close[lookback],
lookahead = barmerge.lookahead_on)
float momentum = (sectorClose - sectorPast) / sectorPast * 100
if momentum > currentBest
currentBest := momentum
currentBestSym := sector
bestMomentum := currentBest
bestSymbol := currentBestSym
// Plot the best momentum and display the leading sector.
plot(bestMomentum, "Best Momentum %", color.teal, 2)
if barstate.islast
label.new(bar_index, bestMomentum,
"Leader: " + bestSymbol + "\n" + str.tostring(bestMomentum, "#.##") + "%",
color = color.teal, textcolor = color.white)
This strategy requests daily close data for eight sector ETFs on every bar, calculates the percentage momentum over a user-defined lookback period, and identifies the strongest sector. In a full implementation, you would add strategy.entry() calls to rotate into the leading sector and exit lagging positions.
Dynamic requests are powerful, but they come with important constraints that every Pine Script developer should understand.
A script can execute a maximum of 40 unique request.*() calls per bar3. If a loop iterates 50 times and each iteration calls request.security() with a different symbol, the script will raise a runtime error on the 41st iteration. Identical calls (same symbol, timeframe, and expression) are counted only once, so redundant requests do not count toward the limit4.
Here is what triggers the error:
// This raises a runtime error at i == 41.
for i = 1 to 50
reqSum += request.security(syminfo.tickerid, str.tostring(i), close)
Each iteration requests a different timeframe string, producing 50 unique calls and exceeding the 40-call limit.
All datasets that a dynamic request accesses on realtime bars must have been previously requested on historical bars4. You cannot request a new symbol or timeframe for the first time on a realtime bar. If you try, the script raises a runtime error. This means your loop or conditional logic must cover all possible data feeds during the historical execution phase.
The expression argument of any request.*() call inside a loop cannot depend on the loop variable or mutable variables modified within the loop4. The symbol and timeframe parameters can use loop-dependent values, but the expression must remain loop-invariant.
All request.*() calls combined cannot return more than 127 tuple elements3. If you need more values, use user-defined types (UDTs) to bundle fields into a single object, which counts as one tuple element.
Working with dynamic requests efficiently requires some planning. Here are best practices to keep your scripts fast and within the limits.
if barstate.islast to avoid redrawing on every historical bar.calc_bars_count to limit how far back the request fetches3. This reduces memory usage.[open, high, low, close] as a single tuple instead of making four separate calls for the same symbol.request.security() evaluating another) add complexity and can produce unexpected results4. Use them deliberately.ignore_invalid_symbol = true to prevent runtime errors from typos or delisted tickers3.Dynamic requests open up multi-symbol analysis inside TradingView, but executing trades across multiple symbols still requires automation infrastructure. This is where TradersPost bridges the gap between your Pine Script signals and live broker execution.
Here is how the workflow fits together:
The combination of Pine Script v6 dynamic requests for analysis and TradersPost for execution gives you a complete automated pipeline. Your script handles the intelligence, scanning and ranking symbols in real time, while TradersPost handles the execution logistics across any number of brokers and accounts.
Dynamic requests are the most impactful feature in Pine Script v6 for traders who work with multiple symbols. The ability to use series string arguments, place request.security() inside loops and conditionals, and export request-containing functions from libraries removes years of workarounds and code duplication.
The key points to remember are:
request.security() call inside a loop can fetch data for an entire watchlistrequest.*() calls per bar executionWhether you are building multi-symbol dashboards, sector rotation strategies, or cross-market correlation scanners, dynamic requests give you the tools to do it cleanly and efficiently within Pine Script.
1 Pine Script Release Notes
2 Pine Script v5 to v6 Migration Guide
3 Pine Script v6 Language Reference
4 Pine Script User Manual
5 TradersPost - Automated Trading Platform