---
title: "Per-bar scripts"
description: "A per-bar script computes each bar from that bar alone: its candle, its inputs and the settings. When the compiler can prove that, the chart computes only the…"
order: 9
section: "core-concepts"
---

<!-- source: docs/indicators/core-concepts/per-bar-scripts.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Per-bar scripts

A per-bar script computes each bar from that bar alone: its candle, its
inputs and the settings. When the compiler can prove that, the chart
computes only the newly loaded bars on a pan-back and keeps every bar it
already drew; when it cannot, every pan-back runs the indicator again over
the whole loaded history. This page shows what the proof accepts, what it
refuses, why rolling math such as a moving average is a different kind of
script rather than a mistake, and where the verdict shows. There is
nothing to declare: the compiler reads the code and decides.

## What a pan-back recomputes

The chart runs an indicator over the bars it has loaded. Panning back
loads earlier bars, and the indicator has to cover them too. Most scripts
cannot reuse what they already drew: a running total, a moving average or
a bar counter starts at the first loaded bar, so bars added in front can
change the values after them, and the chart runs the indicator again from
the new first bar to the newest.

A per-bar script carries nothing from one bar to the next. A bar's value
is the same whether the run started a thousand bars earlier or at that
bar, so the chart runs only the bars that just loaded and keeps the rest:

```text
loaded:            bar 5,000 ............................. newest
pan back loads:    bar 1 ... bar 4,999

whole history:     run bar 1 ................................... newest
per-bar script:    run bar 1 ... bar 4,999    keep bar 5,000 ... newest
```

The forming bar, live updates and a changed setting run as they do for
any indicator ([Execution model](execution-model.md)).

## What the proof accepts

The compiler proves a file per bar when everything `onBar()` reads comes
from the bar at hand or from a value fixed before the first bar, and every
form in the file is one of these:

- **Number and boolean locals with a starting value.**
  `const x = bar.close() * 2.0;`, `let n = 0;` or
  `let up = bar.close() > bar.open();` inside `onBar()` or a helper: made
  when the bar starts, gone when it ends. A local declared without a
  value, a string and an object (`new Sma(14)` included) refuse.
- **Module constants.** A top-level `const` holding a number or a boolean,
  from literals, arithmetic, earlier constants and the kit helpers below
  (`const TUPLE = 3;`, `const SPAN = TUPLE * 2 + 1;`,
  `const STEP = roundTo(0.125, 2);`); `Math` is refused there. A top-level
  table of numbers (`const WEIGHTS = [0.5, 0.3, 0.2];`), read only by index
  and `.length`.
- **Module scalars set once in `onStart()`.** A top-level `let` holding a
  number or a boolean, given a constant starting value and assigned only in
  `onStart()` from settings, constants, `Math`, the kit helpers below and
  arithmetic (`depth = p_depth_pct() / 100.0;`), never from a module `let`;
  `onBar()` reads it and never writes it. `onStart()` holds locals and
  assignments only, so a choice there is a ternary, not an `if`.
- **Scratch made inside `onBar()`.** A local `StaticArray` of numbers
  (`const sums = new StaticArray<f64>(8);`): filled, read and written by
  index, its `.length`, and passed to a helper that takes the same shape.
- **This bar's readers.** `bar.open()`, `bar.high()`, `bar.low()`,
  `bar.close()`, `bar.volume()` and `bar.time()`; a feed's `in_<name>()`;
  a number or boolean setting's `p_<name>()` and `pb_<name>()` (a list
  setting's reader refuses); and a celled input's count,
  `in_<name>_cells()`, with its view read by index through a local `const`
  (`const cells = in_<name>_view();`)
  ([Celled sources](data-sources.md#celled-sources)).
- **Plain functions.** Your own functions that take numbers (or a local
  `StaticArray`) and return a number or a boolean; `isNaN`, `isFinite`,
  `NaN` and `Infinity`; the `Math` functions `abs`, `floor`, `ceil`,
  `round`, `trunc`, `sqrt`, `pow`, `min`, `max`, `log`, `log2`, `log10`,
  `exp`, `sign`, `hypot`, `sin`, `cos`, `tan`, `asin`, `acos`, `atan` and
  `atan2`, and no other `Math` member (`Math.PI`, `Math.random()` and
  `Math.log1p()` refuse); and these kit helpers: the colour helpers
  `rgba`, `rgb`, `isThemeToken`, `alpha`, `mix`, `fromGradient`,
  `lighten`, `darken`, `red`, `green`, `blue`, `opacity`, `transparency`,
  `toPacked` and `fromPacked`; the session helpers `multiHas`,
  `unitToPrice`, `inSession`, `sessionDayOpen`, `sessionTradeDate`,
  `sessionWeekOpen` and `sessionGapBefore`; `delta`, `deltaPct`,
  `roundTo`, `roundToTick` and `epochSec`; and the candle tests
  `candles.doji`, `hammer`, `shootingStar`, `bullishEngulfing`,
  `bearishEngulfing`, `insideBar`, `outsideBar` and `pinBar`. Any other kit
  function, `fromHex` for one, refuses.
- **What the chart draws from the bar.** `out_<name>(value)` written from
  the values above, and what reads only outputs or the bar's cells: lines,
  bands (`range`), boxes and segments, a `render.legend` entry over an
  output, a `render.hud` card over outputs, and a heatmap over the bar's
  own celled block.

## What the proof refuses

Anything that carries a value from one bar into the next, or reads where
the bar stands in the run, refuses the proof. The compile names the first
one it meets, with its line when it has one:

- **A module variable written in `onBar()`.** A flag, a counter, a running
  total or the previous close: `prev = bar.close();` inside `onBar()`
  hands the bar to the next one.
- **An object.** `new Sma(...)`, `new Ema(...)`, `Cvd`, `History`, `List`
  and every other kit class keep earlier bars by design, wherever they are
  made, and a class of your own refuses too.
- **A module-level array.** A `StaticArray` at the top of the file is
  memory every bar writes and the next bar reads (a ring buffer, a
  window): make scratch inside `onBar()` instead. A table that is written,
  filled or passed to a helper refuses the same way.
- **Drawings.** A drawing handle used in code (`draw.label(...)`,
  `draw.line(...)`), a `handles.*` declaration and a `draw.*` declaration
  all live across bars.
- **Text.** A string slot and its senders (`str_<slot>`, the `sb_*`
  builders), a string in code, and a text setting's words
  (`pt_<name>()`).
- **Where the bar stands.** `bar.isLast()` and `bar.count()` answer by how
  many bars the run holds.
- **An input that reaches beyond the bar.** An input with its own
  `interval` or `view`, a `candles` or `intrabar` stream, a tape
  `history`, and a feed beside the chart's own candles that carries its
  last value into a bar with no reading. Carrying is the default for such
  a feed: declare `missing: "nan"` or `"zero"` on it to keep it per bar
  ([The `missing` policy](data-sources.md#alignment-and-the-missing-policy)).
- **Declarations that draw or act across bars.** The renderers
  `render.text`, `render.label`, `render.table`, `render.shape`,
  `render.stats_row`, `render.bgcolor` and `render.barcolor`; frames and
  the levels and panels they feed; a strategy; an alert; and an output
  drawn at another bar (`displacement_bars`).
- **A form outside the proof.** A `switch` (write `if` and `else`), an
  `if` in `onStart()`, an arrow function, an `Array` written by index (use
  a `StaticArray`), a helper that returns an array, a view held in a
  `let`, an `onReset()` with a body, the four-function form, and a second
  file beside the indicator.

## Rolling math is a different kind

Some math needs earlier bars by definition:

- a moving average at a bar averages the bars before it;
- a cumulative delta adds the bar's delta to the total of every bar
  before it;
- a session profile folds every bar since the session opened;
- swings, zones and divergences remember where price has been.

These scripts are right to keep state, in a kit class or in module
variables, and the proof is right to refuse them: on a pan-back the chart
runs them again over the whole loaded history, as it always has. That is
not a mistake to fix, and rolling math bent into another shape to win the
proof only gets harder to read. The verdict covers the whole file, so one
rolling value anywhere in it makes every pan-back recompute the history.
Reach for the per-bar shape when the math is per bar to begin with: a
reading of the bar's own candle, book, profile or trades.

## A per-bar indicator

Bid size against ask size near the close, from each bar's own order book:
the resting size within a depth window around the close, as a percent of
both sides.

```typescript sample=cc-per-bar
param.number("depth_pct", 1.0, { min: 0.1, max: 10, step: 0.1, label: "Depth window in percent", description: "How far from the close a resting order counts, in percent, each side" });
input("close", ohlcv.close); // the primary input: the chart's own candles
input("book", book.cells, { max_cells: 1000, block_size: 10, description: "The resting book at the bar's close, one [price, size, side] level per tuple" }); // side +1 bid, -1 ask
output("imbalance", histogram, lower, { colors: ["#38bdf8", "#f86800"], format: "%", label: "Book imbalance", description: "Bid size minus ask size near the close, as a percent of both" }); // sky above zero, orange below

const TUPLE = 3; // f64s per level: price, size, side
let depth = 0.01; // a module scalar: onStart() sets it from the setting, onBar() only reads it

function onStart(): void {
  depth = p_depth_pct() / 100.0;
}

// Every value comes from this bar: its close, its book and the setting.
function onBar(): void {
  const n = in_book_cells(); // this bar's f64 values: -1 with no book, 0 for an empty one
  const cells = in_book_view(); // the same values in place, read by index
  const close = bar.close();
  let bids = 0.0;
  let asks = 0.0;
  for (let i = 0; i + TUPLE - 1 < n; i += TUPLE) {
    if (Math.abs(cells[i] - close) > close * depth) continue; // outside the window
    if (cells[i + 2] > 0.0) bids += cells[i + 1];
    else asks += cells[i + 1];
  }
  out_imbalance(imbalancePct(bids, asks));
}

// A plain function: numbers in, a number out, nothing kept.
function imbalancePct(bids: f64, asks: f64): f64 {
  const total = bids + asks;
  return total > 0.0 ? (100.0 * (bids - asks)) / total : NaN;
}
```

### Read it line by line

- **`TUPLE` is a module constant.** It is fixed before the first bar and
  never written.
- **`depth` is a module scalar set once.** `onStart()` reads the setting
  into it and `onBar()` only reads it, so every bar sees the same number
  whichever bar the run starts at.
- **`n`, `cells`, `close`, `bids` and `asks` are locals.** Each starts
  with a value, is made when the bar starts and is gone when it ends.
- **The loop reads the book by index into the locals.** `in_book_cells()`
  counts this bar's values (`-1` with no book, `0` for an empty one) and
  `in_book_view()` holds them in place, `[price, size, side]` back to
  back. `i + TUPLE - 1 < n` reads whole levels only and stops at the last
  one this bar holds, so a bar with no book reads nothing. The view is one
  buffer reused bar after bar, and every value past this bar's levels
  reads NaN, in a full run and in a run that starts at this bar alike: an
  earlier bar's levels never leak in.
- **`imbalancePct()` is a plain function.** Two numbers in, one number
  out, nothing kept.
- **`out_imbalance(...)` writes the bar's one value.** A bar with nothing
  inside the window writes NaN and draws nothing.

Nothing survives the bar except the constant and the setting, so Run's
compile proves the file per bar and the sheet carries
`history_independent: true`.

## Before and after: Book Heat's note

[Book Heat](../cookbook/book-heat.md) paints the chart market's order book
behind the candles and plots the same imbalance below them. Its note, one
sentence at the top right on a market that served a book on no bar ("No
order book on this market", on forex and stocks), is the classic shape
that refuses the proof:

```typescript
string("note", { max_bytes: 40 }); // the sentence shown when the market serves no order book
handles.label({ anchor: "top_right", align: "right", color: "#94a3b8", size: 12, style: "plain", safe_area: true });

const note = draw.label(0); // a handle the chart keeps across bars
let bookSeen = false; // the market served a book on some bar

function onBar(): void {
  const n = in_book_cells();
  if (n > 0) bookSeen = true; // a module variable written in onBar()
  if (bar.isLast() && !bookSeen) { // where the bar stands: the last one
    sb_clear();
    sb_text("No order book on this market");
    note.set(16.0, 12.0).text(str_note_sb);
  }
  // ... the imbalance, from this bar's levels
}
```

"No book on any bar" is a fact about the whole history, so the note needs
four things the proof refuses: the string slot and the label handle (kept
across bars), the `bookSeen` flag written in `onBar()` (carried from bar
to bar) and the `bar.isLast()` read (where the bar stands).

The per-bar variant removes them: the slot, the handle declaration,
`const note`, `let bookSeen`, its write and the last-bar block. The
heatmap still paints each bar's own book, the imbalance still reads it
into locals as the sample above does, and the compile proves the file per
bar. A market with no book then shows an empty pane and no sentence.
Which one to ship is a product choice, not a fix: a script with something
to say about the whole history keeps its state, and the chart recomputes
that history on a pan-back.

## Where the verdict shows

- **The sheet.** A proven compile writes `history_independent: true`
  beside `wasm_sha256`; a refused one leaves the key out, never `false`.
  Type `sheet` at the Console prompt after a run to read it
  ([Declarations and the sheet](../reference/declarations.md#the-sheet-run-derives)).
- **The Problems tab.** One info row, on the construct's line when it has
  one, for these refusals: a module variable written in `onBar()`, an
  object or array the proof cannot admit, a drawing handle, a read of
  where the bar stands, text written in code, a helper signature the proof
  cannot admit, an `Array` that grows, a table that is written or passed,
  a view not read into a local `const`, an input with its own `interval`
  or `view`, and a declared handle, string slot or displaced output. The
  row starts "Scrolling back recomputes the whole history:", names the
  construct, and says what a per-bar script does instead. It informs and
  never blocks **Run**, and the badge does not count it. Every other
  refusal (a carried feed, a renderer, a frame, a strategy, an alert, a
  `switch`, an `if` in `onStart()`, an `onReset()` body, the four-function
  form, a second file) shows only in the `sheet` command, as the missing
  key, and in the line a terminal build prints.
- **A build outside the chart.** A build run from a terminal prints the
  verdict as one line of its output: `pan-back: new bars only`, or
  `pan-back: whole history` with the reason in parentheses.

## How to keep a script per bar

- **Keep bar values in locals.** What a bar computes lives inside
  `onBar()`; a value that must reach the next bar makes the file rolling.
- **Set module scalars once, in `onStart()`.** Read the settings there
  into top-level numbers, and only read those in `onBar()`.
- **Make scratch inside `onBar()`.** A local `StaticArray` belongs to the
  bar; one at the top of the file belongs to every bar.
- **Read celled inputs by index, bounded by the count.** `in_<name>_cells()`
  counts this bar's values and the view holds them; read whole tuples into
  locals.
- **Leave the run alone.** No `bar.isLast()`, no `bar.count()`, no handle
  or text kept for a later bar.
- **Declare inputs that stay on the bar.** A feed beside the chart's
  candles takes `missing: "nan"` or `"zero"`; an `interval` or `view` pin
  and a `candles` or `intrabar` stream depend on where the run starts.
- **Let rolling math be rolling.** A moving average, a cumulative delta or
  a session profile needs earlier bars, and the whole-history run on a
  pan-back is the right one for it.

## See also

- [Execution model](execution-model.md) for the bar loop, `onStart()` and
  `onBar()`, and how many bars the run holds.
- [Repainting](repainting.md) for what can still change a closed bar on
  the chart.
- [Data sources](data-sources.md#celled-sources) for celled inputs, their
  count and their view.
- [Declarations and the sheet](../reference/declarations.md#the-sheet-run-derives)
  for `history_independent` and the rest of the derived sheet.
- [Book Heat](../cookbook/book-heat.md) for the whole recipe, heatmap
  included.
