Skip to main content

The bar model

Read this page before writing a script that keeps any state.

Closed bars and forming bars​

The host calls onBar():

  • once for every closed bar. isNewBar() is true.
  • any number of times for the bar that is still forming, on every price update. isNewBar() is false.

In a backtest or on history every bar is closed, so onBar() runs exactly once per bar. Live, the last bar is re-run on each tick until it closes.

Series and indexing​

A Series is a value per bar. s[0] is the current bar, s[1] the bar before, and so on. Every ta.* function returns a series, and any series can be the input of another indicator:

const rsi = ta.rsi(src.close, 14);
const smoothed = ta.ema(rsi, 5); // an indicator over an indicator

Create your own series at top level with series() and write the current bar with s[0] = value. Only index 0 is writable.

On a forming bar, [0] changes with every tick. [1] and older never change. Signals computed from [1] do not repaint.

Which state rolls back​

After each forming-bar run the host rolls its own state back, so the next tick starts from the last closed bar. Your module's own variables are not rolled back:

StateBehaves likeWhy
src.*, every ta.* result, every series()Pine varHost-owned. Committed on a closed bar, rolled back after a forming-bar run
Module-level let / const, class fields, arrays, mapsPine varipLives in the script's own memory, which is never rolled back

So count += 1 in a module-level variable counts ticks, not bars, on a live chart. A backtest does not show the problem, because it runs each bar once. Three correct patterns:

// 1. A number: keep it in a series. It rolls back for you.
const count = series();
export function onBar(): void {
const prev = na(count[1]) ? 0 : count[1];
count[0] = src.close[0] > src.open[0] ? prev + 1 : prev;
}
// 2. Any other state: only change it on a closed bar.
let lastSignalBar: i64 = -1;
export function onBar(): void {
if (isNewBar() && src.close[0] > src.open[0]) lastSignalBar = barIndex();
}
// 3. Simplest: ignore forming bars entirely.
export function onBar(): void {
if (!isNewBar()) return;
// everything below runs once per bar
}

The stateful helpers Cross, Rising, Falling, BarsSince and ValueWhen already follow pattern 2, so they are safe to call on every tick.

Indicators stream​

Each ta.* call site keeps its own state, and each tick costs one incremental step. Keep a call site's parameters constant: a length that changes every bar creates a new indicator each time, and the host stops the script after 256.

na inputs are skipped by recursive indicators (ema, rsi, ...) and make windowed ones (sma, highest, ...) na while the na is inside the window.

Bar state and context​

FunctionMeaning
isNewBar(), barstate.isConfirmed()This run is a closed bar
barstate.isRealtime()Live data, not history
barstate.isLast()The newest bar (every live tick, or the final bar of a backtest)
barstate.isFirst()The first bar this script instance saw
barIndex(), barTime(), timeClose()Position and times of the current bar
timeframe.inSeconds(), timeframe.period()The chart timeframe
syminfo.ticker(), syminfo.mintick(), syminfo.type()The symbol