
The for loop is one of the most fundamental building blocks in Pine Script. You use it to iterate over bars, scan arrays, and run repetitive calculations. In March 2025 TradingView changed how for loops evaluate their stopping condition1, and this change has significant implications for how you write indicators and strategies in Pine Script v6.
This guide explains what changed, why it matters, how to take advantage of dynamic boundaries in your own scripts, and what to watch out for when migrating existing code.
The March 2025 Pine Script update changed the boundary-checking behavior of the for loop structure1. The core change is simple to state but has far-reaching consequences for how loops behave.
Before (v5 behavior): A for loop evaluated its end boundary (to_num) exactly once, before the first iteration. If code inside the loop modified the variable or expression used as to_num, those modifications had no effect on how many times the loop ran2. The boundary was locked in at the start.
After (v6 behavior): A for loop evaluates its to_num boundary dynamically, before every iteration1. If code inside the loop changes the value of the variable or expression used as to_num, the loop adjusts its iteration count accordingly. The boundary can grow or shrink while the loop runs.
This brings the for loop into alignment with how while loops and for...in loops already worked2. Those loop types always had dynamic stopping conditions. The for loop was the only one that locked its boundary at the start, and now that inconsistency is resolved.
In Pine Script v5 and earlier, the for loop evaluated the to_num expression once and stored the result internally2. Here is a basic example showing how this worked.
//@version=5
indicator("v5 Fixed Boundary Demo")
int boundary = 10
int count = 0
for i = 0 to boundary
boundary := boundary + 1 // This had no effect on the loop
count := count + 1
plot(count) // Always plotted 11 (iterations 0 through 10)
Even though the code incremented boundary on every iteration, the loop still ran exactly 11 times because v5 captured the value 10 before the first iteration and never checked it again2. The variable boundary would end up at 21, but the loop never saw those changes.
This behavior was predictable and safe. You could never accidentally create an infinite loop with a for statement because the iteration count was fixed. However, it also meant you could not use for loops for tasks where the number of iterations depended on what the loop discovered during execution.
In Pine Script v6, the same code behaves very differently. The loop evaluates the to_num boundary before each iteration1, so changes to the boundary variable inside the loop directly affect how many times the loop runs.
//@version=6
indicator("v6 Dynamic Boundary Demo")
int boundary = 10
int count = 0
for i = 0 to boundary
boundary := boundary + 1 // Now this DOES affect the loop
count := count + 1
// WARNING: This loop runs indefinitely and causes a runtime error!
This version of the code creates an infinite loop because every iteration increases the boundary by one, which means the counter can never reach the stopping point. The loop runs until Pine Script raises a runtime error for exceeding the maximum execution time3.
The power of dynamic boundaries comes from using them intentionally, not accidentally. When you understand the behavior, you can write loops that adapt their iteration count based on meaningful conditions discovered during execution.
Dynamic for loop boundaries unlock several practical patterns that were previously impossible or required workarounds with while loops.
Previously, you would need a while loop for any of these tasks. While loops are more flexible but also more prone to infinite loop bugs because they have no built-in counter4. Dynamic for loops give you the adaptability of a while loop with the structure and counter variable of a for loop.
One of the most practical applications of dynamic for loops is building an indicator with an adaptive lookback period. Instead of using a fixed number of bars, the loop expands its range until it finds enough qualifying bars to produce a meaningful result.
//@version=6
indicator("Adaptive Lookback Demo", overlay = false)
//@variable The minimum number of bars where close differs from current close by at least one standard deviation.
int minBarsInput = input.int(10, "Min Qualifying Bars", minval = 1)
//@variable The maximum lookback range to prevent excessive computation.
int maxLookbackInput = input.int(500, "Max Lookback", minval = 10)
//@variable The 20-bar standard deviation of close prices.
float stdev = ta.stdev(close, 20)
//@variable Tracks how many qualifying bars have been found.
int qualifyingBars = 0
//@variable The dynamic boundary that expands as the loop searches for enough qualifying bars.
int searchRange = math.min(minBarsInput, maxLookbackInput)
for i = 1 to searchRange
// Check if the past bar's close differs from current close by at least one standard deviation.
if math.abs(close[i] - close) >= stdev
qualifyingBars += 1
// If we have not found enough qualifying bars yet, expand the search range.
if qualifyingBars < minBarsInput and searchRange < maxLookbackInput
searchRange += 1
plot(qualifyingBars, "Qualifying Bars", color.blue, 2)
plot(searchRange, "Lookback Used", color.gray, 1)
This indicator searches backward through price history looking for bars where the close price differs from the current close by at least one standard deviation. If it has not found enough qualifying bars within the current search range, it expands the range by one. The loop self-adjusts its iteration count based on what it finds.
The maxLookbackInput variable acts as a safety cap to prevent the loop from searching too far back and exceeding Pine Script execution limits. This is a critical best practice when working with dynamic boundaries.
Another powerful pattern is a search loop that narrows its own boundary when it finds what it is looking for. Instead of always running the maximum number of iterations, the loop can cut itself short by reducing the to_num value.
//@version=6
indicator("Self-Terminating Search", overlay = true)
//@variable The initial search range for finding pivot levels.
int rangeInput = input.int(100, "Search Range", minval = 10)
//@variable The number of pivot highs to find before stopping.
int targetCount = input.int(3, "Pivots to Find", minval = 1)
//@variable Tracks how many pivot highs have been found.
int foundCount = 0
//@variable The dynamic loop boundary that shrinks when enough pivots are found.
int searchLimit = rangeInput
//@variable Array to store the prices of found pivot highs.
var array pivotPrices = array.new()
pivotPrices.clear()
for i = 5 to searchLimit
// Check if the bar at offset i is a pivot high with 5 bars on each side.
float pivotVal = ta.pivothigh(high, 5, 5)
if not na(high[i]) and high[i] == ta.highest(high, 11)[i - 5]
foundCount += 1
pivotPrices.push(high[i])
// Once we have found enough pivots, shrink the boundary to stop the loop.
if foundCount >= targetCount
searchLimit := i
// Draw horizontal lines at the found pivot levels.
if barstate.islast
for [idx, price] in pivotPrices
line.new(bar_index - rangeInput, price, bar_index, price,
color = color.new(color.orange, 30), width = 2)
In this example the loop starts with a wide search range but terminates early by setting searchLimit to the current index once enough pivot highs have been found. In v5 this would have been impossible because changing searchLimit inside the loop had no effect on when the loop stopped.
The March 2025 update also introduced a new setter function for box drawings1. The box.set_xloc() function sets the left and right coordinates of a box and updates its xloc property in a single call3. Previously, you had to use separate set_left() and set_right() calls to reposition a box, and changing the xloc mode required recreating the box entirely.
//@version=6
indicator("box.set_xloc() Demo", overlay = true)
var box myBox = box.new(na, na, na, na, bgcolor = color.new(color.blue, 80))
if barstate.islast
// Set both x-coordinates and the xloc mode in one call.
box.set_xloc(myBox, bar_index - 20, bar_index, xloc.bar_index)
box.set_top(myBox, ta.highest(high, 20))
box.set_bottom(myBox, ta.lowest(low, 20))
The function signature is box.set_xloc(id, left, right, xloc) where id is the box object, left and right are the x-coordinate values, and xloc specifies whether those values represent bar indices (xloc.bar_index) or UNIX timestamps (xloc.bar_time)3. This mirrors the existing set_xloc() functions already available for lines and labels3.
If you are converting scripts from v5 to v6, the dynamic for loop behavior is one of the changes most likely to introduce subtle bugs2. A loop that worked perfectly in v5 can behave completely differently in v6 if its to_num argument depends on values modified inside the loop body.
Look for these patterns in your v5 code that may break when converted to v6.
If a for loop requires the v5 behavior where the boundary is evaluated only once, assign the expression to a variable before the loop starts and use that variable as the to_num argument2.
//@version=6
indicator("Migration Fix Demo")
var array<float> data = array.new<float>()
if data.size() == 20
data.shift()
data.push(close)
// WRONG: data.size() changes if modified inside the loop
// for i = 0 to data.size() - 1
// CORRECT: Capture the boundary before the loop starts
int loopEnd = data.size() - 1
for i = 0 to loopEnd
float val = data.get(i)
// Process val without affecting the loop boundary
By assigning data.size() - 1 to loopEnd before the loop starts, you ensure that modifications to the array inside the loop do not accidentally expand or contract the iteration range. The variable loopEnd is set once and remains unchanged throughout the loop execution.
After converting a script from v5 to v6, verify the behavior of every for loop by adding temporary log statements that record the iteration count. Compare the results against the v5 behavior to ensure nothing has changed unintentionally.
//@version=6
indicator("Loop Debug", overlay = true)
int iterations = 0
int boundary = 50
for i = 0 to boundary
iterations += 1
// Log the actual iteration count on the last bar.
if barstate.islast
log.info("Loop ran {0} times with boundary {1}", iterations, boundary)
The March 2025 update to Pine Script for loops is a fundamental change to how the language works1. By evaluating the to_num boundary dynamically before every iteration, Pine Script v6 for loops become far more powerful and flexible. You can now build adaptive indicators that adjust their lookback periods, search patterns that terminate early, and iterative algorithms that converge on results without needing to switch to while loops.
The tradeoff is that existing v5 code may behave differently after migration if it modifies variables used in the to_num expression2. Always capture boundary values in a separate variable before the loop when you need the old fixed-boundary behavior.
If you build adaptive strategies using dynamic for loops and want to automate them, TradersPost connects your TradingView alerts directly to supported brokers5. Once your adaptive indicator generates a signal, TradersPost handles the order execution so you can focus on refining your loop logic and entry conditions rather than manually placing trades.
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