The main loop
Conceptually the runtime does this:
for i = 0 … barCount - 1:
makeBarState(i)
runBody() // every top-level statement in order
collectOutput(i)
i = 0is the oldest bar in the loaded history,i = barCount - 1is the current (possibly forming) bar.- Every top-level statement runs on every iteration. A plain
=assignment is therefore re-computed every bar. varinitializers run only on the first bar they are reached;vartargets then survive across iterations.- Visual output (
plot(),plotshape(), drawings) are collected into per-bar arrays that the host renders after the loop finishes.
Series access during the loop
When the body is at bar i, close returns the close of bar i. close[1] returns the close of bar i − 1, close[2] of bar i − 2, and so on. The runtime maintains a per-series ring buffer that grows automatically up to the deepest [n] the script ever requests.
rsi = ta.rsi(close, 14)
up = rsi > rsi[1] // true when RSI rose from the previous bar
On bar 0 the lookback is na (there is no previous bar) and bar-state booleans are false. All [n] accesses with n >= i + 1 evaluate to na.
Historical pass vs live pass
Two phases execute the same body:
- Historical pass — bars 0 through the last confirmed bar are iterated once;
barstate.isconfirmedistrueon each. - Live pass — the forming bar re-runs on every tick with
barstate.isconfirmed = false, until it closes and joins the historical pass.
During the live pass the runtime creates a snapshot of all state at the last confirmed bar. Each tick restores the snapshot, runs the body once, and collects the forming bar's output. This is what makes a strategy's "process_orders_on_close = false" market order queue up on the signal bar and fill at the next bar's open — the queue sits in the strategy state carried across the snapshot/restore cycle.
varip variables (see Variable declarations) opt out of snapshot rollback and keep updating as ticks arrive.
Repainting
A script "repaints" when values displayed on confirmed bars change after the fact. In ChartScript this can happen for two reasons:
- A built-in that reads from
[1]or further back inside a function body — the historical pass computes the function with the historical values, but a live-pass re-run uses the forming bar's forming values. Confiningrequest.security(..., lookahead=barmerge.lookahead_off)or using..[1]eliminates the forward peek. - Explicit lookahead (
request.security(..., lookahead=barmerge.lookahead_on)) — accepted, but documented as "repaints by design"; use it only when the forward peek is intentional.
See Repainting for the full catalogue.
Side effects and ordering
Statements inside a function body execute top-to-bottom just like at script scope. Function calls are evaluated when they appear — plot(), strategy.entry(), label.new(), and log.info() all fire at the point of the call, so order inside the body determines rendering z-order and strategy order sequence:
if ta.crossover(close, ta.sma(close, 20))
strategy.entry("Long", strategy.long)
label.new(bar_index, high, "Long")
if ta.crossunder(close, ta.sma(close, 20))
strategy.close("Long")
label.new(bar_index, low, "Exit", color=color.red)
Series-mode functions
Many library functions carry state across bars (ta.sma, ta.rsi, ta.pivothigh, …). They must be called from the top-level body on every bar — otherwise their internal buffer skips bars and goes out of sync. Calling such a function conditionally will trigger a compile warning:
if close > open
rsi = ta.rsi(close, 14) // ⚠ warning: series fn inside conditional
The idiomatic fix is to compute the series unconditionally and gate the consumption:
rsi = ta.rsi(close, 14)
if close > open
plot(rsi, "RSI on green bars")
(There is no actual gating of plot behind a condition — the typical pattern is plot(cond ? rsi : na).)
Order of evaluation
Within a single statement, operands evaluate left-to-right; function arguments evaluate left-to-right before the call. Short-circuit and/or apply as expected.
See also
- Script structure
- Bar states — barstate.*, timenow, barmerge
- Repainting
- Variable declarations —
varandvarip