Choices and loops
Make decisions with if and match, repeat with for and while, and learn the rules for loop direction, empty loops and the and/or operators.
This page covers the statements that choose and repeat. Decisions come first, then loops, then the operators and and or, whose rule about state surprises people.
if as a statement#
An if with a block under it chooses what to do. else if and else are optional.
indicator "Zone number"
edition = 1
mut zone = 0
if close > open + 1
zone = 2
else if close > open
zone = 1
else
zone = -1
plot(zone, name = "Zone", style = histogram)A statement if has no value. Only the lines in its block run.
if as a value#
if ... then ... else ... on one line is an expression. It has a value, so it needs an else.
indicator "If as a value"
edition = 1
display = price
let direction = if close > open then 1 else -1
plot(close, name = "Close", color = if direction > 0 then color.green else color.red)Writing an if whose value is used, without an else, is E2001.
match#
match picks one of several cases. With a subject, each case is a value to compare with:
indicator "Match on a value"
edition = 1
input mode: text = "Fast", name = "Mode", options = ["Fast", "Normal", "Slow"]
let length = match mode
"Fast" => 8
"Normal" => 21
_ => 55
plot(tech.ema(close, length), name = "Average")Without a subject, each case is a condition and the first true one wins. This example uses conditions only. (With a subject, a case may also add if as a guard: 0 if close > open => 10 needs both the value and the condition.)
indicator "Match with conditions"
edition = 1
let state_of_bar = match
close > open + 1 => 2
close > open => 1
close < open - 1 => -2
close < open => -1
_ => 0
plot(state_of_bar, name = "Bar state", style = histogram)_ is the case that matches anything and must be the last. Without it, bars that match nothing give na, and the checker warns:
indicator "Match without a default"
edition = 1
let size = match int(close) % 2
0 => 1
1 => 2
plot(size)A match on an enum names its members: Mode.fast => 8. Values and types introduces enums.
for#
for i in a to b counts from a up to and including b. until leaves the end out. by sets the step.
indicator "Loop ranges"
edition = 1
mut up = 0
for i in 0 to 3
up += 1
mut until_end = 0
for i in 0 until 3
until_end += 1
mut stepped = 0
for i in 0 to 10 by 5
stepped += 1
plot(up, name = "0 to 3: four passes")
plot(until_end, name = "0 until 3: three passes")
plot(stepped, name = "0 to 10 by 5: three passes")indicator "Counting down"
edition = 1
mut forwards = 0
for i in 3 to 0
forwards += 1
mut backwards = 0
for i in 3 to 0 by -1
backwards += 1
plot(forwards, name = "3 to 0: nothing")
plot(backwards, name = "3 to 0 by -1: four passes")This matters most when the end comes from a variable. for i in 0 to n - 1 over n = 0 counts from 0 to -1: no passes, and nothing breaks. A loop over nothing is not an error.
indicator "Sum of the last n closes"
edition = 1
input n: int = 10, name = "Bars", min = 0, max = 1000
mut total = 0.0
for i in 0 to n - 1
total += close[i]
plot(total, name = "Sum")A step of zero never moves, so the checker refuses a written zero (E2041) and the engine stops a zero or na step that arrives at run time (E-LOOP-STEP).
indicator "A step of zero"
edition = 1
mut count = 0
for i in 0 to 5 by 0
count += 1
plot(count)for ... in a collection#
for x in items visits each item of an array. for (i, x) in items gives the position as well. A map's .keys() is an array of its keys.
indicator "Walk an array"
edition = 1
let levels = [high, low, close]
mut total = 0.0
mut weighted = 0.0
for (i, level_) in levels
total += level_
weighted += level_ * (i + 1)
plot(total, name = "Sum")
plot(weighted, name = "Weighted sum")while, break and continue#
while repeats as long as its condition is true. break leaves the nearest loop, and continue skips to its next pass.
indicator "Bars since a higher high"
edition = 1
mut found = -1
for i in 1 to 20
if close[i] > high
found = i
break
mut odd = 0
for i in 1 to 10
if i % 2 == 0
continue
odd += 1
plot(found, name = "Bars back to a higher close (-1: none)")
plot(odd, name = "Odd numbers up to 10")Every loop is bounded by the engine: one bar may spend 500,000 steps and the whole run 600,000,000. A loop that does not end stops the script with E-LIMIT-OPS, naming the bar and the line. See Limits and budgets.
A loop may not call a function that keeps state between bars (tech.sma, tech.crosses_above, a function of yours that holds a state), because every pass would share one memory. Call it once before the loop, or write the calls out one by one.
indicator "A stateful call in a loop"
edition = 1
mut total = 0.0
for i in 1 to 3
total += tech.sma(close, 5)
plot(total)and, or and not#
and, or and not work on true and false. not binds looser than a comparison, so not a == b means not (a == b).
The right side runs only when it is needed. a and b looks at b only when a is true, a or b only when a is false, a ?? b only when a is na, and if c then x else y runs only the arm it chooses. A guard such as i < n and items.at(i) > 0 never reads outside the array.
One kind of call may not stand there. A call that keeps state, such as a tech.* call, data.series, a state it reads or a function of yours that keeps state, remembers values for the line it is written on, and that memory moves only when the line runs. Written directly in the right side of and, its memory would move only on the bars where the left side was true, which is rarely what the line says. The language refuses it with E3302:
indicator "A call that keeps state in the right side"
edition = 1
let trend = close > tech.sma(close, 50)
let signal = trend and tech.crosses_above(close, tech.sma(close, 10))
plot(if signal then 1 else 0)You say what you mean in one of two ways. Write the call first on a line of its own, and it runs on every bar:
indicator "Run on every bar"
edition = 1
let trend = close > tech.sma(close, 50)
let crossed = tech.crosses_above(close, tech.sma(close, 10))
let signal = trend and crossed
plot(if signal then 1 else 0)Or write the condition as a block if, and the call runs only where the block runs. The warning W3302 is there so that nobody reads it as "every bar" by mistake: on the bars where the block does not run, the memory of the call stays as it was.
indicator "Run only where reached"
edition = 1
let trend = close > tech.sma(close, 50)
let signal = if trend
tech.crosses_above(close, tech.sma(close, 10))
else
false
plot(if signal then 1 else 0)The two lines count differently. In the first one the crossover sees every bar. In the second one it only looks at the bars on which trend was true, so it compares a bar with the last bar it saw, which may be hours older. Use the block when you mean it, for example to avoid a costly call; bind the call first when the call keeps state and should see every bar. The same holds for the arms of an if ... then ... else value and for the right side of ??.
See also: Functions, types and enums, Variables, Common mistakes.