---
title: "Variables: locals, module state, and history"
description: "Variable declaration and bar-state behavior in an Indicator: what a local in state() is, what a module-level variable is, and how a value with history is kept.…"
order: 13
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 an Indicator: what a local in
`state()` is, what a module-level variable is, and how a value with history
is kept. kScript (legacy) had three keywords for three lifetimes, `var`,
`persist`, and `timeseries`; an Indicator has ordinary TypeScript scoping
plus the four functions, and `reset()` to put everything back.

## Declaration roles

| kScript | Role | Indicator |
| --- | --- | --- |
| `var x = ...` | this bar only | a `const` or `let` inside `state()` or `finalize()` |
| `persist n = 0` (or `static`) | carried across bars | a module-level `let`, initialized once, restored by `reset()` |
| `timeseries s = ...`, read with `s[1]` | a value with history | a module-level "previous" variable, a TA class, or a ring buffer |

A **local** is born when `state()` 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 four functions 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. Every
module-level variable is `reset()`'s responsibility.

A **value with history** is what `timeseries` gave you for free and what
an Indicator makes you keep on purpose. For `[1]`, 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 class from `./sdk/ta` holds the window for you.

## Bar-state globals

kScript exposed `barIndex`, `isFirst`, `isConfirmed`, and `isLastBar` as
globals. None of them exists in an Indicator; each has a plain replacement
or no need:

| kScript | Indicator |
| --- | --- |
| `barIndex` | a module-level counter you increment in `state()` |
| `isFirst` | that counter at `0` (or a `first` flag `init()` sets and `state()` clears) |
| `isConfirmed` | no flag: on the chart the newest bar is the forming one and replays on every tick through `reset()`; on your machine `om metric series` returns it as its newest row, and closed-bar consumers cut it before the module sees it |
| `isLastBar` | nothing to write: renderers and drawings evaluate the newest ready row on their own, and a metric is read at its newest value |

The one-time initialization `isFirst` served is `init()` 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.

## Tuple destructuring

A multi-output builtin bound with `[macdLine, signalLine, histLine] =
macd(...)` becomes a class whose fields you read by name after `update()`
(`named-streams.md`); there are no tuples.

## History example

The kScript "timeseries source moves" example, ported: 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
import { input, line, lower, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_change, out_moving_average } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";

param("period", 20, { min: 1, max: 200, description: "SMA length" });
input("close", ohlcv.close);
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]
let change: f64 = NaN;
let average: f64 = NaN;

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

export function state(): i32 {
  const close = in_close(); // a local: this bar's value, gone after the call
  change = isNaN(prevClose) ? NaN : close - prevClose;
  prevClose = close; // remember it for the next bar
  average = sma.update(close);
  return isNaN(change) ? 0 : 1;
}

export function finalize(): void {
  out_change(change);
  out_moving_average(average);
  emitRow();
}

export function reset(): void {
  sma.reset();
  prevClose = NaN;
  change = NaN;
  average = NaN;
}
```

On the first bar there is no previous close, so `change` is `NaN` and the
row abstains; 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 an abstained row).

## Persist example

The kScript "persist counter" example, ported: a bar counter that keeps its
running total, plus the `barIndex` and `isFirst` replacements in the same
file.

```typescript
import { input, line, lower, ohlcv, output } from "./sdk/declare";
import { in_close, in_open } from "./gen/inputs";
import { emitRow, out_bar_index, out_carried_score, out_first_open } from "./gen/outputs";

input("close", ohlcv.close);
input("open", ohlcv.open);
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; // persist barsSeen = 0
let barIndex: i32 = 0; // the barIndex global, kept by hand
let firstOpen: f64 = NaN; // seeded once, on the first bar
let score: f64 = NaN;

export function init(): void {}

export function state(): i32 {
  const open = in_open();
  const close = in_close();
  if (barIndex == 0) firstOpen = open; // isFirst
  barsSeen += 1.0;
  score = barsSeen + (close - open);
  barIndex += 1;
  return 1;
}

export function finalize(): void {
  out_carried_score(score);
  out_bar_index(f64(barIndex - 1));
  out_first_open(firstOpen);
  emitRow();
}

export function reset(): void {
  barsSeen = 0.0;
  barIndex = 0;
  firstOpen = NaN;
  score = NaN;
}
```

`barsSeen` is the persisted counter; `barIndex` is the same idea kept as
an `i32` and written to an output as `f64(barIndex - 1)` (the increment
happens before `finalize()` runs). The counter says `0` on the oldest
loaded bar, not on the first bar the market ever traded: history depth is
whatever the host loaded.

## 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: promote the value to a remembered variable or a
ring buffer.

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

**`reset()` must restore every module-level variable.** The host snapshots
the module after the last closed bar and replays the forming bar into it on
every tick; a variable `reset()` forgets is a stale value the replay reads,
and the forming bar draws wrong (`repainting.md`). Reassign every
module-level `let` and call `.reset()` on every TA object; a class you wrote
gets a `reset()` method of its own.

**Allocate once.** A module-level `new StaticArray<f64>(n)` or `new Sma(n)`
is built in `init()` or at module scope. Building one per bar is the one
pattern that makes a long history slow: the module has no garbage
collector, so memory only grows (`collections.md`).
