Per-bar scripts

View as MarkdownOpen the editor

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).

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).
  • 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).
  • 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.

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;
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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 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:

wrun
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).
  • 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