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.…

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

kScriptRoleIndicator
var x = ...this bar onlya const or let inside state() or finalize()
persist n = 0 (or static)carried across barsa module-level let, initialized once, restored by reset()
timeseries s = ..., read with s[1]a value with historya 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:

kScriptIndicator
barIndexa module-level counter you increment in state()
isFirstthat counter at 0 (or a first flag init() sets and state() clears)
isConfirmedno 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
isLastBarnothing 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.

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.

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).