Loops and Functions
Counted loops, the accumulator pattern, and user functions declared with fn.
A counted loop is written for i = 0 to len - 1, and both bounds are inclusive, so that form runs len times with i taking every value from 0 to len - 1. The counter is an int the body may read and must not assign. The body is either indented under the header or wrapped in braces — both are accepted, but one block should not mix them. Loops run inside a single bar evaluation, which is the cost to keep in mind: a loop of length len on every one of n bars is len times n iterations, so a 200-period loop over five thousand bars is a million passes.
The accumulator pattern is what almost every loop in the indicator library is doing. Declare the accumulator before the loop with = and a float literal so its type is unambiguous, update it with := inside, and read it after. The weighted moving average is the clean example: weighted = 0.0, then weighted := weighted + (len - i) * src[i] across the window, divided by len * (len + 1) / 2. Because the accumulator is a plain local and not a var, it starts again from zero on the next bar, which is precisely the behaviour a windowed calculation needs.
One pass can carry several accumulators, and the linear regression channel does exactly that: sumX, sumY, sumXY and sumXX are all built in a single loop over the window, and the slope and intercept fall out of them afterwards. A second loop over the same window then accumulates the squared residuals for the channel width. Loops are also how you reach for a position rather than a value — Aroon starts from sinceHigh = 0, walks its window, and inside an if that compares high[i] with high[sinceHigh] writes sinceHigh := i, which is an argmax over history that no single built-in expresses.
Functions are declared with fn, take typed parameters, and use a braced body with return: fn midpoint(int n) { return (ta.highest(high, n) + ta.lowest(low, n)) / 2 }. Parameter types are required and are the ordinary ones — series, int, float, bool, string, color. A function may read anything declared above it in the script, which is why a moving-average helper can switch on an input.enum it never receives as an argument: fn ma(series s, int n) { return type == "SMA" ? ta.sma(s, n) : type == "WMA" ? ta.wma(s, n) : ta.ema(s, n) }. What it may not do is declare an input or emit a plot, so the settings panel and the output list stay statically knowable.
Two rules of thumb. Write a function when the same expression appears more than twice — the Ichimoku midpoint of extremes, the four rate-of-change terms inside the KST, an RSI helper applied once to price and once to a streak of closes — and give it the name the formula uses, because that is what makes the code readable against the calculation beside it. And prefer a built-in to a loop whenever one exists: ta.wma is the same arithmetic as the accumulator above and is evaluated incrementally rather than rebuilt each bar. Reserve while for the case where the number of iterations genuinely is not known in advance, such as growing a value area outward from the point of control until it covers a share of total volume, and make sure the condition provably goes false.