Variables: locals, module state, and history

View as MarkdownOpen the editor

Variable declaration and bar-state behavior in a wrun indicator: what a local in onBar() is, what a module-level variable is, and how a value with history is kept. An indicator has the ordinary scoping of TypeScript syntax plus two hooks: onStart() once before the first bar, onBar() once per bar.

Declaration roles

Lifetimewrun
this bar onlya const or let inside onBar()
carried across barsa module-level let, initialized once (at module scope or in onStart())
a value with historya History (Stats, history and lists), a TA class, or by hand a module-level "previous" variable or a ring buffer

A local is born when onBar() is called for a bar and gone when it returns. It is right for a scalar you compute and use immediately: this bar's typical price, a ratio, a comparison.

A module-level let lives outside the hooks and keeps its value from one call to the next. A counter incremented on each bar keeps its running total for the whole run; an accumulator like a cumulative delta is one of these; so is any TA object, since its window is state too. The chart replays the forming bar from a snapshot of every module-level variable (Repainting).

A value with history is something an indicator keeps on purpose. For the previous bar's value, store the value in a module-level variable when you see it and read it on the next bar. For [n] over a window, a StaticArray<f64> used as a ring buffer. For a windowed statistic, a TA class holds the window for you. History packages the first two: push() the value once per bar and read ago(n) (Stats, history and lists).

The bar's index, the first bar, the newest bar

There are no bar-state globals in an indicator; each need has a plain form:

Needwrun
the bar's indexa module-level counter you increment in onBar()
the first barthat counter at 0 (or a first flag onStart() sets and onBar() clears)
is this bar closedno flag: on the live chart the newest bar is the forming one; the chart evaluates it again as new data arrives, from a snapshot of the state the closed bars left, and once more as a closed bar when it closes
the newest barbar.isLast(), true on the newest bar the run holds (Execution model); renderers and drawings evaluate the newest ready row on their own
does a later bar existyour counter + 1 below bar.count(), the bars the run holds (Execution model)

bar also carries the chart's own candle, with no input line: reading a field adds that input to the sheet, on the chart's own market and timeframe (Data sources). Every member is this bar's value inside onBar(); in onStart() the fields read NaN.

MemberTypeWhat it reads
bar.open()f64the chart candle's open (ohlcv.open)
bar.high()f64its high (ohlcv.high)
bar.low()f64its low (ohlcv.low)
bar.close()f64its close (ohlcv.close)
bar.volume()f64its volume (ohlcv.volume)
bar.time()f64its open time in epoch seconds, UTC (time.bar_open_sec)
bar.isLast()boolno input: true on the newest bar the run holds, the last row of a full run or the forming bar live
bar.count()i32no input: how many bars the run holds while this bar is evaluated, the forming bar included; every bar of a full run reads the run's bar count

One-time initialization is onStart() itself: it runs once before the first bar and reads the params. Logic that must run on the first bar of data (seeding a level from the first open) is the counter test.

No tuples

A multi-output computation is a class whose fields you read by name after update() (named-streams.md); there are no tuples to destructure.

History example

The close, the one-bar change (a remembered previous close), and a moving average whose class holds the window. Nothing here is indexed; everything is kept.

param("period", 20, { min: 1, max: 200, description: "SMA length" });
output("moving_average", line, overlay, { color: "#dc2626", width: 2, description: "SMA of the close (the class keeps the window)" });
output("change", line, lower, { color: "#2563eb", width: 2, description: "Close minus the previous close (one remembered value)" });

let sma = new Sma(20);
let prevClose: f64 = NaN; // stands in for closeSeries[1]

function onStart(): void {
  sma = new Sma(i32(p_period()));
}

function onBar(): void {
  const close = bar.close(); // a local: this bar's value, gone after the call
  const change = isNaN(prevClose) ? NaN : close - prevClose;
  prevClose = close; // remember it for the next bar
  const average = sma.update(close);
  if (isNaN(change)) return;
  out_change(change);
  out_moving_average(average);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

On the first bar there is no previous close, so change is NaN and the file returns before writing anything; from the second bar on the change draws, and the average joins it once its window is full (writing NaN until then is a gap on that one line, not a blank bar).

Persist example

A bar counter that keeps its running total, plus a bar index and a first-bar seed in the same file.

output("carried_score", line, lower, { color: "#16a34a", width: 2, description: "Bars seen plus this bar's body: a value carried across bars" });
output("bar_index", line, lower, { color: "#94a3b8", description: "0 on the oldest bar, counting up" });
output("first_open", line, lower, { color: "#f59e0b", description: "The open of the first bar of loaded history, held" });

let barsSeen: f64 = 0.0; // module state: survives every bar
let barIndex: i32 = 0; // counts bars from the oldest loaded one
let firstOpen: f64 = NaN; // seeded once, on the first bar

function onBar(): void {
  const open = bar.open();
  const close = bar.close();
  if (barIndex == 0) firstOpen = open; // the first bar of the run
  barsSeen += 1.0;
  const score = barsSeen + (close - open);
  barIndex += 1;
  out_carried_score(score);
  out_bar_index(f64(barIndex - 1));
  out_first_open(firstOpen);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

barsSeen is the carried counter; barIndex is the same idea kept as an i32 and written to an output as f64(barIndex - 1) (the increment happens before the write). The counter says 0 on the oldest loaded bar, not on the first bar the market ever traded: history depth is whatever the chart loaded, and panning back, which runs the indicator again over the longer window, renumbers every bar (Repainting).

Boundaries

Indexing a number is refused. const previous = closeNow[1]; on an f64 fails to compile with Index signature is missing in type 'f64'. There is nothing to index: push() the value into a History and read ago(1) (Stats, history and lists), or promote it to a remembered variable or a ring buffer.

A local does not survive the call. A let declared inside onBar() starts over on every bar. If you meant to carry it, move the declaration to module level.

Nothing in the file puts module state back. The chart does not need it to replay the forming bar: it restores a snapshot of the module taken after the last closed bar, every module-level variable and every TA object included (Repainting).

Allocate once. A module-level new StaticArray<f64>(n) or new Sma(n) is built in onStart() or at module scope, never per bar: the module has no garbage collector, so memory only grows, and the chart refuses a run whose memory grows once the bars start (Collections).