Dynamic Loop Indicators in v6

Fact checked by
Mike Christensen, CFOA
March 11, 2026
How to create adaptive indicators using Pine Script v6 dynamic for loops with variable boundaries that adjust based on market conditions.

Bottom Line

  • Dynamic for loops in v6 re-evaluate their boundary on each iteration enabling adaptive calculation windows
  • Build indicators that automatically adjust lookback periods based on volatility volume or trend strength
  • Use ATR-based loop boundaries for volatility-adaptive moving averages and support/resistance detection
  • Always include a safety maximum to prevent infinite loops when boundaries change dynamically

Fixed lookback periods are one of the biggest limitations in traditional technical analysis. A 20-period moving average uses exactly 20 bars whether the market is calm or crashing. Pine Script v6 changes this with dynamic for loop boundaries, where the loop's stopping point re-evaluates before every iteration.1 This means you can build indicators that automatically adjust how many bars they look back based on real-time market conditions like volatility, volume, or trend strength.

This tutorial shows you how to build three practical adaptive indicators using dynamic for loops. Each example demonstrates a different approach to letting market data control the calculation window.

What Dynamic Loops Enable

In Pine Script v5, a for loop locked its end boundary before the first iteration.2 If you wrote for i = 0 to length and changed length inside the loop, it had no effect. The loop ran the same number of times regardless. In v6, the for loop re-evaluates its to_num boundary before every iteration, matching the behavior that while loops and for...in loops already had.1

This seemingly small change enables an entirely new category of indicators: those that adapt their calculation window on every bar based on what the market is actually doing. Instead of asking users to pick a fixed period that works "most of the time," you can write indicators where the lookback period grows during volatile conditions and shrinks during quiet ones.

The key requirement is a safety mechanism. Because the boundary can change during execution, a dynamic loop can theoretically run forever if the boundary keeps moving ahead of the counter.4 Every dynamic loop needs a hard maximum that prevents runaway iterations.

Volatility-Adaptive Moving Average

The first example builds a moving average whose lookback period expands when volatility is high and contracts when volatility is low. The idea is simple: when the market is choppy, you want more smoothing to filter noise. When the market is trending cleanly, you want less smoothing so the average stays responsive.

The Concept

Average True Range (ATR) measures volatility.3 When ATR is high relative to its own historical average, the market is volatile. When ATR is low, the market is calm. We use this ratio to scale the lookback period of a simple moving average. The loop runs from bar 0 backward, summing close prices. But instead of a fixed endpoint, the loop's boundary is a volatility-scaled length that re-evaluates dynamically.

The Code

//@version=6
indicator("Volatility-Adaptive MA", overlay = true)

// --- Inputs ---
int   baseLengthInput = input.int(20, "Base MA Length", minval = 5, tooltip = "The starting lookback length before volatility adjustment")
int   maxLengthInput  = input.int(100, "Maximum Length", minval = 20, tooltip = "Safety cap to prevent excessive iteration")
int   atrPeriodInput  = input.int(14, "ATR Period", minval = 1)
float scalingInput    = input.float(2.0, "Volatility Scaling Factor", minval = 0.5, step = 0.5, tooltip = "How aggressively the lookback expands with volatility")

// --- Volatility measurement ---
float currentATR = ta.atr(atrPeriodInput)
float atrAvg     = ta.sma(currentATR, 100)
float volRatio   = atrAvg > 0 ? currentATR / atrAvg : 1.0

// --- Dynamic length calculation ---
int dynamicLength = math.min(math.round(baseLengthInput * math.pow(volRatio, scalingInput)), maxLengthInput)
dynamicLength    := math.max(dynamicLength, 2)

// --- Adaptive MA using dynamic loop ---
float sumClose  = 0.0
int   counted   = 0
for i = 0 to dynamicLength - 1
    sumClose += close[i]
    counted  += 1

float adaptiveMA = counted > 0 ? sumClose / counted : close

// --- Plotting ---
color maColor = adaptiveMA > adaptiveMA[1] ? color.teal : color.red
plot(adaptiveMA, "Adaptive MA", maColor, 3)
plot(ta.sma(close, baseLengthInput), "Fixed MA", color.gray, 1, plot.style_line)

How It Works

The volRatio variable measures how current volatility compares to its recent average. When ATR is double its average, volRatio is 2.0. The dynamic length multiplies the base period by this ratio raised to the scaling factor, then caps it at maxLengthInput. The for loop uses this dynamic length as its boundary, so the moving average automatically uses more bars during volatile periods.

The gray fixed moving average plotted alongside provides a visual comparison. You can see the adaptive line smooth out during choppy conditions while the fixed line continues whipping back and forth.

Volume-Weighted Level Finder

The second example scans backward through bars to find the price level where a target amount of volume has traded. This identifies volume-anchored support and resistance levels that shift based on how much participation occurred at each price.

The Concept

Instead of looking back a fixed number of bars for support, you look back until a cumulative volume threshold is reached. In liquid conditions the threshold fills quickly, so the level is nearby. In thin conditions it takes many more bars, so the level reaches further back in time. This creates support and resistance levels that are anchored to actual market participation rather than arbitrary time windows.

The Code

//@version=6
indicator("Volume-Weighted Level Finder", overlay = true)

// --- Inputs ---
float volumeTargetInput = input.float(1.0, "Volume Target (millions)", minval = 0.1, step = 0.1, tooltip = "Cumulative volume threshold in millions")
int   maxBarsInput      = input.int(200, "Maximum Lookback Bars", minval = 10, tooltip = "Safety cap for the backward scan")
color supportColorInput = input.color(color.teal, "Support Color")
color resistColorInput  = input.color(color.red, "Resistance Color")

// --- Volume target in raw units ---
float volumeTarget = volumeTargetInput * 1000000

// --- Scan backward for volume-weighted high and low ---
float cumVolume     = 0.0
float weightedHigh  = high
float weightedLow   = low
int   barsScanned   = 0

for i = 0 to maxBarsInput - 1
    cumVolume += volume[i]
    if high[i] > weightedHigh
        weightedHigh := high[i]
    if low[i] < weightedLow
        weightedLow := low[i]
    barsScanned += 1
    if cumVolume >= volumeTarget
        break

// --- Plotting ---
plot(weightedHigh, "Volume Resistance", resistColorInput, 2, plot.style_stepline)
plot(weightedLow, "Volume Support", supportColorInput, 2, plot.style_stepline)

How It Works

The loop walks backward bar by bar, accumulating volume into cumVolume. On each iteration it also tracks the highest high and lowest low seen so far. When cumVolume reaches the target threshold, the loop breaks. The result is a support and resistance range that represents the price extremes across a meaningful amount of trading activity.

The maxBarsInput serves as the safety cap. If volume is extremely thin and the target is never reached within 200 bars, the loop stops regardless. This prevents the indicator from scanning the entire chart history on low-volume instruments.

Pattern Scanner With Variable Depth

The third example searches backward for a specific price pattern, adjusting search depth based on recent trend strength. In strong trends the scanner looks further back for a pullback setup. In ranging markets it uses a shorter window to avoid stale signals.

The Concept

The ADX indicator measures trend strength.3 When ADX is high, the market is trending strongly and meaningful pullback levels may be further back in time. When ADX is low, the market is ranging and recent swing points are more relevant. The scanner uses ADX to scale its search depth, then walks backward looking for the most recent swing low in an uptrend or swing high in a downtrend.

The Code

//@version=6
indicator("Adaptive Swing Finder", overlay = true)

// --- Inputs ---
int   baseDepthInput = input.int(20, "Base Search Depth", minval = 5)
int   maxDepthInput  = input.int(100, "Maximum Search Depth", minval = 20)
int   adxPeriodInput = input.int(14, "ADX Period", minval = 1)
int   swingBarsInput = input.int(3, "Swing Confirmation Bars", minval = 1, tooltip = "Bars on each side to confirm a swing point")

// --- Trend strength measurement ---
[diplus, diminus, adxValue] = ta.dmi(adxPeriodInput, adxPeriodInput)
float adxNorm     = math.min(adxValue / 25.0, 2.0)

// --- Dynamic depth based on trend strength ---
int searchDepth = math.min(math.round(baseDepthInput * (1.0 + adxNorm)), maxDepthInput)
searchDepth    := math.max(searchDepth, swingBarsInput * 2 + 1)

// --- Scan for most recent swing low ---
float swingLowLevel  = na
int   swingLowBar    = na
for i = swingBarsInput to searchDepth - 1
    bool isSwingLow = true
    for j = 1 to swingBarsInput
        if low[i] > low[i - j] or low[i] > low[i + j]
            isSwingLow := false
            break
    if isSwingLow
        swingLowLevel := low[i]
        swingLowBar   := i
        break

// --- Scan for most recent swing high ---
float swingHighLevel = na
int   swingHighBar   = na
for i = swingBarsInput to searchDepth - 1
    bool isSwingHigh = true
    for j = 1 to swingBarsInput
        if high[i] < high[i - j] or high[i] < high[i + j]
            isSwingHigh := false
            break
    if isSwingHigh
        swingHighLevel := high[i]
        swingHighBar   := i
        break

// --- Plotting ---
plot(swingLowLevel, "Recent Swing Low", color.teal, 2, plot.style_circles, offset = 0)
plot(swingHighLevel, "Recent Swing High", color.red, 2, plot.style_circles, offset = 0)

How It Works

The adxNorm variable scales the ADX value into a multiplier between 0 and 2. When the ADX reads 50 (strong trend), adxNorm is 2.0, which triples the base search depth. When ADX is 12 (weak trend), adxNorm is about 0.48, keeping the search close to the base depth. The searchDepth variable controls how far back both swing scanners look.

Each scanner uses a nested loop pattern. The outer loop walks backward through candidate bars. The inner loop checks whether the candidate has lower lows (or higher highs) on both sides, confirming it as a swing point. Once a valid swing is found, the outer loop breaks.

Safety Patterns

Dynamic loop boundaries introduce the possibility of runaway loops. If a boundary expression keeps growing faster than the counter increments, the loop could execute indefinitely. Pine Script will eventually stop it with a runtime error, but your indicator will fail.4 Here are three patterns for preventing this.

Hard Maximum Cap

The simplest approach: always apply math.min() to cap the dynamic boundary at a fixed maximum.

int dynamicLen = math.min(calculatedLength, MAX_ALLOWED)
for i = 0 to dynamicLen - 1
    // loop body

Break on Condition

Use a break statement inside the loop to exit early when a target condition is met, regardless of the boundary.

for i = 0 to maxBars - 1
    cumulative += someValue[i]
    if cumulative >= target
        break

Pre-Calculate Outside the Loop

If the boundary should not change during iteration, calculate it once before the loop and assign it to a variable. Use that variable as the to_num argument. This replicates the v5 fixed-boundary behavior in v6.

int fixedBound = array.size(myArray) - 1
for i = 0 to fixedBound
    // boundary is locked because fixedBound is not modified inside the loop

The pre-calculation pattern is especially important when migrating v5 code to v6. If a v5 for loop uses an expression like array.size(id) - 1 as its boundary and the loop body modifies the array, the loop will behave differently in v6 because the boundary now re-evaluates.2 Assigning the expression to a variable outside the loop before it starts restores the original behavior.

Performance Considerations

Dynamic loops that scan many bars on every tick can consume significant processing time. Here are guidelines for keeping your adaptive indicators responsive.

  • Keep maximum lengths reasonable for the timeframe you are using. A 200-bar maximum on a daily chart is different from 200 bars on a 1-minute chart in terms of data coverage
  • Use built-in functions like ta.sma() and ta.atr() for the components that do not need dynamic behavior, and reserve the dynamic loop for the parts that genuinely benefit from adaptive boundaries3
  • Avoid nested dynamic loops where both the inner and outer boundaries are variable. Each additional level of nesting multiplies the iteration count
  • Profile your script using the Pine Profiler to identify bottleneck loops and verify that the dynamic boundary is not producing unexpectedly large iteration counts4

Automation with TradersPost

Adaptive indicators produce signals that account for current market conditions, which makes them well-suited for automated trading. A volatility-adaptive moving average generates fewer whipsaw signals during choppy markets because it automatically increases its smoothing period. A volume-weighted level finder provides dynamic support and resistance that adjusts to actual market participation.

When these indicators generate entry or exit signals, you can connect them to TradersPost through TradingView alert webhooks.5 The alert fires when the adaptive condition triggers, TradersPost receives the webhook and routes the order to your connected broker. Because the indicator itself adapts to market conditions, the automated system inherits that adaptiveness without any additional logic needed on the TradersPost side.

Key Takeaways

Dynamic for loop boundaries in Pine Script v6 let you build indicators that adapt their calculation windows to real-time market conditions.1 Volatility-scaled moving averages, volume-anchored support and resistance levels, and trend-adaptive pattern scanners are all practical applications of this feature. The critical safety pattern is always including a maximum cap on the loop boundary to prevent runaway iterations.2 Combined with TradersPost automation, these adaptive indicators can power trading systems that adjust their behavior without manual intervention.5

References

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

Ready to automate your trading? Try a free 7-day account:
Try it for free ->