Pine Script conditionals and loops, past the basics
This page assumes you already know a plain if and a plain for, covered in Pine Script basics. It covers the control-flow Pine Script adds on top of those: else if chains, if as an expression, the switch structure, while, for...in, loop control with break and continue, and the ternary operator, ending with turning one of these into a live Autoview command.
else if chains and if as an expression
A plain if can chain as many else if clauses as you need, evaluated in order, with the first true one winning:
if close > ta.sma(close, 50) * 1.05
label.new(bar_index, high, "Strong")
else if close > ta.sma(close, 50)
label.new(bar_index, high, "Above avg")
else
label.new(bar_index, low, "Below avg")if can also be assigned directly to a variable, making it an expression rather than just a side effect. Every branch's block has to resolve to the same type:
zone = if close > ta.sma(close, 50) * 1.05
"strong"
else if close > ta.sma(close, 50)
"above"
else
"below"zone holds whichever string the matching branch produced. If nothing matches and there's no else, the whole structure returns na (or false, if the result type is bool) instead.
switch: one clause, no fallthrough
switch picks exactly one clause to run, based on matching an expression against a list of cases. Unlike a C-style switch, there's no fallthrough between cases; once one clause matches, the rest are skipped entirely.
rsi = ta.rsi(close, 14)
zone = switch
rsi > 70 => "overbought"
rsi < 30 => "oversold"
=> "neutral"Each expression => block line is a case; the final bare => block with no expression before it is the default, the clause that runs when nothing else matched. switch can also key off a single expression instead of a series of boolean cases, matching it against a list of exact values, which reads more clearly than a long else if chain once you have more than two or three branches.
while: loop until a condition stops holding
for needs a fixed range up front. while instead repeats for as long as its condition stays true, re-checking that condition before every iteration, including the first:
sum = 0.0
count = 0
while count < 14 and not na(close[count])
sum := sum + close[count]
count := count + 1
avg14 = count > 0 ? sum / count : naThis accumulates up to 14 bars of close, stopping early if it runs out of history (na(close[count)] catches that). for can't express "stop early for a reason unrelated to the counter" nearly this cleanly; that's the case while is for.
Version note: lazy evaluation. On Pine Script v6, and and or evaluate their two sides lazily (short-circuit): once count < 14 is false, and stops there and never touches close[count]. That's what keeps this while condition safe. Pine Script v5 evaluated both sides regardless, so the same expression relied more on na() catching an already-computed out-of-range reference rather than on the left side gating the right. Write it with v6's short-circuit in mind: put the cheaper, bounding condition first.
for...in: iterate a collection directly
When you already have an array, matrix, or map, for...in iterates its items directly, no manual index counting required:
levels = array.from(100.0, 105.0, 110.0)
touched = 0
for level in levels
if high >= level and low <= level
touched := touched + 1Add an index alongside the item with for [index, item in collection_id], the form maps require since a map's keys aren't sequential integers.
Version note: dynamic loop bounds. A numeric for i = 0 to n loop's upper bound, n, now re-evaluates before every iteration on Pine Script v6, rather than being locked in once at the loop's start the way v5 evaluated it. If something inside the loop body changes the variable you used as the bound, a v6 loop can run more or fewer times than it would have on v5. for...in iterating a fixed array like levels above isn't affected the same way since the collection itself, not a separate numeric bound, controls the iteration count -- but a plain counted for whose bound is a variable is exactly the case this changed.
break and continue
Both work inside for, while, and for...in. continue skips whatever's left in the current iteration and jumps straight to re-checking the loop's condition; break exits the loop immediately, with no further iterations at all:
firstBigMove = na
for i = 0 to 19
change = math.abs(close[i] - close[i + 1])
if na(change)
continue
if change > ta.sma(close, 20) * 0.02
firstBigMove := i
breakcontinue skips a bar where change can't be computed yet (not enough history); break stops the search the moment the first qualifying bar is found, rather than checking all 20 regardless.
The ternary operator: if, inline
condition ? value_if_true : value_if_false is if compressed into a single expression, useful when you just need to pick between two values rather than run a block of statements:
barColor = close > open ? color.green : color.red
plot(close, color=barColor)Chaining ternaries can stand in for a small switch, but past two or three links it reads worse, not better; reach for switch once you're past a single condition.
When the condition is na
A common question is what if or a ternary does when its condition is na. On Pine Script v6 the answer is short: a bool can't be na. TradingView's v6 migration guide says a bool must be either true or false, with no third state, and that expressions which returned a boolean na in v5 now return false. Its own example: using [] on the very first bar to read a past value of a bool variable returned na in v5, because no earlier bar exists, and returns false in v6.
So in practice, a condition that would have been na is false: the if block doesn't run, and a ternary picks its value_if_false side. Two related v6 rules from the same guide:
- Numbers aren't conditions any more. v6 no longer casts an
intorfloattoboolautomatically. Wrap it withbool()where a condition is expected. - You can't test a bool with na().
na(),nz(), andfixnan()don't acceptboolarguments in v6. Test the underlying number instead, before you compare it.
That last point is the one worth building a habit around. If a value might be missing, check it with na() as part of the condition rather than relying on the comparison to come out false:
prevHigh = high[1]
if not na(prevHigh) and close > prevHigh
label.new(bar_index, high, "breakout")Written this way, the intent is visible in the code, and the same pattern the while example above uses (not na(close[count)]) keeps working. On v5 the rules were different: the migration guide notes that a boolean na was evaluated as false when cast to bool, but was not equal to false when compared with ==. If you're maintaining a v5 script, that difference is the one to watch.
See the computed value before you trade on it
Before wiring a switch or while result to an order, look at it. A plotted label costs nothing and confirms the logic is computing what you think before any alert or order is involved:
//@version=6
indicator("Zone Signal", overlay=true)
rsi = ta.rsi(close, 14)
zone = switch
rsi > 70 => "overbought"
rsi < 30 => "oversold"
=> "neutral"
if barstate.islast
label.new(bar_index, high, zone, color=color.blue, textcolor=color.white)Run this alone first. The label on the last bar should read overbought, oversold, or neutral and match what rsi is actually doing. A non-trading alert() call, one whose message is just zone itself with no Autoview command syntax in it, does the same check through TradingView's own Alerts panel or log instead of the chart:
if zone != zone[1]
alert("zone changed to " + zone, alert.freq_once_per_bar)Neither of those touches Autoview. Only once the label and the plain-text alert agree with what zone should be do you move to the version that fires a real command.
Turning any of these into a live command
A switch or while result is computed at runtime, so it can change bar to bar, the same category of value the functions guide covers. alertcondition() can't carry it, its message has to be a constant string fixed at compile time. alert() plus str.tostring() can:
//@version=6
indicator("Zone Signal", overlay=true)
rsi = ta.rsi(close, 14)
zone = switch
rsi > 70 => "overbought"
rsi < 30 => "oversold"
=> "neutral"
justEnteredOversold = zone == "oversold" and zone[1] != "oversold"
if justEnteredOversold
alert("s=BTCUSDT b=buy q=1 t=market // zone=" + zone, alert.freq_once_per_bar)zone is computed fresh every bar by the switch; justEnteredOversold is an if-friendly boolean built by comparing zone against its own value one bar back. When it turns true, alert() fires with zone's current text folded into the message via str.tostring()-style concatenation (a string value like zone concatenates with + directly, no str.tostring() call needed since it's already text). See label & comment your alerts for what the trailing // comment does once Autoview receives it.
Test before you trust it
Add d=1 to the message before you rely on any of this. Autoview's dry-run gate is exchange-agnostic: every integration's trade path calls a shared withDebugReporter check, and when cmd.d is set, it logs what the command would have done and returns before any live order is sent. Let justEnteredOversold trigger a few times, check the log for the zone and side you expected, then remove d=1 once it matches.
Repaint and once-per-bar check. justEnteredOversold compares zone to its own value one bar back, and a script re-runs on every realtime tick, not just at bar close, so an intrabar zone flip can trigger and un-trigger before the bar is done. Gate the if on barstate.isconfirmed if you want exactly one fire per bar close instead of one per intrabar transition: if justEnteredOversold and barstate.isconfirmed. Test it the same way as the value check above -- watch the dry-run log across a live session and confirm each entry is one bar apart, not several in the same minute.
A note on what Autoview is. Autoview is an execution tool, not a trading or investment advisor. It places the orders your script and alerts tell it to; it doesn't generate signals or make any claim about results. Trading carries risk of loss, and you're responsible for the strategy you automate. See our disclosures.