---
title: "Variables: locals, module state, and history"
description: "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…"
order: 16
section: "core-concepts"
---

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

# 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](../functions/stats-history-lists.md#history-xn-from-pine)), 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](repainting.md)).

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](../functions/stats-history-lists.md#history-xn-from-pine)).

## 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](execution-model.md)); 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](execution-model.md#how-many-bars-the-run-holds)) |

`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](data-sources.md)). 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_open_sec`) |
| `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.

```typescript sample=cc-vars-history
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.

```typescript sample=cc-vars-persist
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](repainting.md)).

## 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](../functions/stats-history-lists.md#history-xn-from-pine)), 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](repainting.md)).

**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](collections.md)).
