Execution model
How a script runs: once per bar from the oldest bar to the newest, again on every live update of the last bar, with history, rollback and the na rules.
A ChartScript script is not a program that runs once from top to bottom and stops. It is a recipe for one bar. ChartDeck runs it on the first bar of the chart, then on the second, and so on to the newest bar, then keeps running it as the live bar updates. Once you understand this loop, most of the language makes sense: why close is "the close of the bar being worked on", why state exists, and why some values look different on history than on the live bar.
#One run per bar
When you add a script to a chart, ChartDeck loads the bars the script runs on (the newest 5,000 of the chart's market and timeframe unless it asks for another number, see bars per run) and runs your code once for each of them, oldest first. On every run the built-in variables describe the current bar: open, high, low, close, volume, time and bar_index (0 on the first bar of the run, then 1, 2 …).
Each run produces one value per output. plot(close) on bar 57 stores the close of bar 57; when all bars have run, the chart draws the stored values as a line.
//@chartscript 1
indicator("Bar counter")
// a const or let variable starts again on every bar
const range = high - low
// a state variable keeps its value from one bar to the next
state upBars: int = 0
if (close > open) {
upBars += 1
}
plot(range, "Range of this bar", color.muted)
plot(upBars, "Up bars so far", color.up, 2)range is recalculated from scratch on every bar. upBars is declared with state, so its starting value 0 is only used on the first bar; after that it carries over, and the script adds 1 on each up bar.
#History: x[n]
Every value your script computes is remembered bar by bar, so you can look back. x[n] is the value of x on the bar n bars ago:
const change = close - close[1] // this close minus the previous close
const twoBack = ta.ema(close, 9)[2] // the EMA as it was two bars ago
const prevRange = (high - low)[1] // works on any expression
plot(change)Write [n] directly after the value, on the same line: a line that starts with [ always starts a new statement, so close on one line and [1] on the next is not a history read.
On the first bars there is nothing to look back at, so close[1] is na on bar 0 (see the na rules). The offset must be a whole number of 0 or more. A negative constant is refused when you compile, and a negative or na offset computed while the script runs stops it with CS5201.
//@chartscript 1
indicator("Look ahead?")
const next = close[-1] // there is no future bar to read
plot(next)How far back you can look is limited by a buffer. For a constant offset such as close[20] the buffer is sized for you. A computed offset (close[n] with n changing from bar to bar) uses max_bars_back, 500 bars by default and up to 10,000. You can raise it in the declaration, indicator("My script", { max_bars_back: 2000 }), or for one series with max_bars_back. Reaching past the buffer stops the script with CS5202, which tells you the value to set.
#The live bar
After the history has run, the newest bar is usually still forming. Each time it changes (a new trade moves its close, high or low), ChartDeck runs your script again on that bar. When the bar closes, it runs one last time with the final values, and the next bar starts.
To keep these repeated runs from piling up, ChartDeck rolls back before each live update: every state variable, and everything a state variable holds (array elements, fields of objects, map entries), goes back to the value it had when the previous bar closed. Then the script runs again with the new prices. Drawings created on a live update are rolled back in the same way.
This is why the bar counter above counts each live bar once, not once per price update. If it did not roll back, a busy live bar could add hundreds of up bars.
#When you want to count updates: live state
Sometimes you do want state that survives the live updates of one bar, for example to count how many updates arrived. Declare it with live state:
//@chartscript 1
indicator("Updates per bar")
live state updates: int = 0
live state lastBar: int = -1
if (bar_index != lastBar) {
updates = 0
lastBar = bar_index
}
updates += 1
plot(updates, "Updates this bar", color.accent, { style: plot.style_columns })Both variables are live state: if lastBar were a plain state, it would be rolled back before every update and the count would start again each time. On history every bar runs once, so the plot is 1. On the live bar it climbs with every update. Keep in mind what that means: a live state value depends on when you were watching, so the same chart reloaded later shows different numbers on bars that were live. Use it for display, not for signals you want to trust on history. See Variables for the details.
#Bar states
The barstate variables tell your code which kind of run it is in. They are series bool: each is true or false on each run.
| Variable | True when |
|---|---|
barstate.isfirst | the run is on the first bar |
barstate.islast | the run is on the last bar of the chart (live or not) |
barstate.ishistory | the bar is a closed bar of the history |
barstate.isrealtime | the bar is the live bar |
barstate.isnew | this is the first run of a new live bar |
barstate.isconfirmed | the bar is closed: every history bar, and the final run of a live bar as it closes |
barstate.islastconfirmedhistory | the bar is the last closed bar of the history |
Two common uses:
- Draw something once, on the newest bar only:
if (barstate.islast) { … }. Labels and tables that summarize the chart are usually created this way, which keeps the script fast. - Act only on closed bars:
if (barstate.isconfirmed) { … }. A condition checked this way never flickers while the live bar is still moving. The Repainting page explains why that matters.
const crossUp = ta.crossover(close, ta.sma(close, 20))
const confirmedCross = crossUp && barstate.isconfirmed
plotshape(confirmedCross, "Confirmed cross", shape.triangleup, location.belowbar, color.up)#The na rules
na means "no value": the close of a bar before the first one, an average that does not have enough bars yet, a map key that is not there. ChartScript treats it consistently:
nahas no type of its own; the context gives it one.let level: float = nais fine.state x = nais not, because nothing says whatxwill hold (CS2201).- Arithmetic with
nagivesna:close + naisna. Division by zero also givesna, never infinity. - A comparison with an
naoperand (<,<=,>,>=,==,!=) isfalse. Soif (x > 0)simply takes theelsepath whilexisna. - A
boolis neverna. It istrueorfalse. - To test for
na, usena(x). Writingx == na(orx == null) is always false, so the compiler refuses it (CS2203). nz(x)replacesnawith 0 (or with a value you give:nz(x, y)), andfixnan(x)carries the last value that was notna.&&and||stop early, so!na(prev) && close > prevnever looks atprevwhen it is missing.- Every
ta.*function follows one rule: a window that containsnagivesna, and a recursive average such as an EMA starts again once it has enough values in a row. You never have to handle this yourself. a ?? bisbwhenaisna, otherwisea: the same asnz(a, b).nullis another way to writena.
//@chartscript 1
indicator("Bad na test")
const avg = ta.sma(close, 50)
const ready = avg != na // always false: use !na(avg)
plot(ready ? avg : close)//@chartscript 1
indicator("Number as a condition")
const diff = close - open
if (diff) { // a number is not a condition
log.info("moved")
}
plot(diff)The fix for the second one is to say what you mean: if (!na(diff) && diff != 0).
#Good to know
- Your script runs on its own bars (5,000 by default,
calc_bars_countup to your plan's bars per run), not on the whole history of the market and not only on the bars the chart shows. Bar 0 is the first bar of the run, so a running total starts there. Chart bars older than the run show no script values. - Indicators that use
ta.*need a few bars to warm up. A 200-bar average isnaon the first 199 bars, so the line starts later than the chart. - A stateful function such as
ta.smaonly advances on the bars where its line actually runs. Calling it inside anifgives a different result from calling it on every bar. The compiler warns about this (CS2503); Functions shows the usual fix. - Values that depend on the moment you watch (
timenow,live state) can differ between history and live bars.