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