Variables: locals, module state, and history
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
| Lifetime | wrun |
|---|---|
| this bar only | a const or let inside onBar() |
| carried across bars | a module-level let, initialized once (at module scope or in onStart()) |
| a value with history | a 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:
| Need | wrun |
|---|---|
| the bar's index | a module-level counter you increment in onBar() |
| the first bar | that counter at 0 (or a first flag onStart() sets and onBar() clears) |
| is this bar closed | no 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 bar | bar.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 exist | your 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.
| Member | Type | What it reads |
|---|---|---|
bar.open() | f64 | the chart candle's open (ohlcv.open) |
bar.high() | f64 | its high (ohlcv.high) |
bar.low() | f64 | its low (ohlcv.low) |
bar.close() | f64 | its close (ohlcv.close) |
bar.volume() | f64 | its volume (ohlcv.volume) |
bar.time() | f64 | its open time in epoch seconds, UTC (time.) |
bar.isLast() | bool | no input: true on the newest bar the run holds, the last row of a full run or the forming bar live |
bar.count() | i32 | no 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);
}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);
}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).