Skip to content
ChartDeck ChartScript docs Account Open the terminal

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.

ChartScriptOpen in editor
//@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:

ChartScriptFragmentOpen in editor
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.

ChartScriptError: CS2302
//@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:

ChartScriptOpen in editor
//@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.

VariableTrue when
barstate.isfirstthe run is on the first bar
barstate.islastthe run is on the last bar of the chart (live or not)
barstate.ishistorythe bar is a closed bar of the history
barstate.isrealtimethe bar is the live bar
barstate.isnewthis is the first run of a new live bar
barstate.isconfirmedthe bar is closed: every history bar, and the final run of a live bar as it closes
barstate.islastconfirmedhistorythe 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.
ChartScriptFragmentOpen in editor
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:

  1. na has no type of its own; the context gives it one. let level: float = na is fine. state x = na is not, because nothing says what x will hold (CS2201).
  2. Arithmetic with na gives na: close + na is na. Division by zero also gives na, never infinity.
  3. A comparison with an na operand (<, <=, >, >=, ==, !=) is false. So if (x > 0) simply takes the else path while x is na.
  4. A bool is never na. It is true or false.
  5. To test for na, use na(x). Writing x == na (or x == null) is always false, so the compiler refuses it (CS2203).
  6. nz(x) replaces na with 0 (or with a value you give: nz(x, y)), and fixnan(x) carries the last value that was not na.
  7. && and || stop early, so !na(prev) && close > prev never looks at prev when it is missing.
  8. Every ta.* function follows one rule: a window that contains na gives na, 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.
  9. a ?? b is b when a is na, otherwise a: the same as nz(a, b). null is another way to write na.
ChartScriptError: CS2203
//@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)
ChartScriptError: CS2204
//@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_count up 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 is na on the first 199 bars, so the line starts later than the chart.
  • A stateful function such as ta.sma only advances on the bars where its line actually runs. Calling it inside an if gives 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.

The ChartScript documentation is in English for now; translations will follow.