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:
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 ... newestThe 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;orlet up = bar.close() > bar.open();insideonBar()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
constholding 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););Mathis 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-levelletholding a number or a boolean, given a constant starting value and assigned only inonStart()from settings, constants,Math, the kit helpers below and arithmetic (depth = p_depth_pct() / 100.0;), never from a modulelet;onBar()reads it and never writes it.onStart()holds locals and assignments only, so a choice there is a ternary, not anif. - Scratch made inside
onBar(). A localStaticArrayof 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()andbar.time(); a feed'sin_<name>(); a number or boolean setting'sp_<name>()andpb_<name>()(a list setting's reader refuses); and a celled input's count,in_<name>_cells(), with its view read by index through a localconst(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,NaNandInfinity; theMathfunctionsabs,floor,ceil,round,trunc,sqrt,pow,min,max,log,log2,log10,exp,sign,hypot,sin,cos,tan,asin,acos,atanandatan2, and no otherMathmember (Math.PI,Math.random()andMath.log1p()refuse); and these kit helpers: the colour helpersrgba,rgb,isThemeToken,alpha,mix,fromGradient,lighten,darken,red,green,blue,opacity,transparency,toPackedandfromPacked; the session helpersmultiHas,unitToPrice,inSession,sessionDayOpen,sessionTradeDate,sessionWeekOpenandsessionGapBefore;delta,deltaPct,roundTo,roundToTickandepochSec; and the candle testscandles.doji,hammer,shootingStar,bullishEngulfing,bearishEngulfing,insideBar,outsideBarandpinBar. Any other kit function,fromHexfor 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, arender.legendentry over an output, arender.hudcard 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();insideonBar()hands the bar to the next one. - An object.
new Sma(...),new Ema(...),Cvd,History,Listand 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
StaticArrayat the top of the file is memory every bar writes and the next bar reads (a ring buffer, a window): make scratch insideonBar()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(...)), ahandles.*declaration and adraw.*declaration all live across bars. - Text. A string slot and its senders (
str_<slot>, thesb_*builders), a string in code, and a text setting's words (pt_<name>()). - Where the bar stands.
bar.isLast()andbar.count()answer by how many bars the run holds. - An input that reaches beyond the bar. An input with its own
intervalorview, acandlesorintrabarstream, a tapehistory, 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: declaremissing: "nan"or"zero"on it to keep it per bar (Themissingpolicy). - Declarations that draw or act across bars. The renderers
render.text,render.label,render.table,render.shape,render.stats_row,render.bgcolorandrender.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(writeifandelse), anifinonStart(), an arrow function, anArraywritten by index (use aStaticArray), a helper that returns an array, a view held in alet, anonReset()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;
}Read it line by line
TUPLEis a module constant. It is fixed before the first bar and never written.depthis a module scalar set once.onStart()reads the setting into it andonBar()only reads it, so every bar sees the same number whichever bar the run starts at.n,cells,close,bidsandasksare 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 (-1with no book,0for an empty one) andin_book_view()holds them in place,[price, size, side]back to back.i + TUPLE - 1 < nreads 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:
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: truebesidewasm_sha256; a refused one leaves the key out, neverfalse. Typesheetat 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, anArraythat grows, a table that is written or passed, a view not read into a localconst, an input with its ownintervalorview, 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, aswitch, anifinonStart(), anonReset()body, the four-function form, a second file) shows only in thesheetcommand, 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, orpan-back: whole historywith 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 inonBar(). - Make scratch inside
onBar(). A localStaticArraybelongs 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(), nobar.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"; anintervalorviewpin and acandlesorintrabarstream 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
For the bar loop, onStart() and
onBar(), and how many bars the run holds.
Repainting
For what can still change a closed bar on the chart.
Data sources
For celled inputs, their count and their view.
Declarations and the sheet
For history_independent and the rest of the derived sheet.
Book Heat
For the whole recipe, heatmap included.