Remembering values across bars
Anything your script remembers from one bar to the next, such as an EMA, a session open or a "closed above yesterday" flag, lives in ctx.accum. Never in a module-level let or var.
A module-level variable survives between runs, is shared by every copy of the script, and is not reset when the chart recalculates. Values drift, and the chart and the server stop agreeing. Inventing let ema = … at module scope is the most common mistake agents make on indicators and definitions.
Signature
ctx.accum<T>(id: string, init: T | (() => T), update: (prev: T) => T): TThe same on indicators and definitions. id names the slot: pass the same string on every bar to read and write the same value. init seeds the slot on first use and may be a value or a function called once. update receives the previous value, and its return is stored and returned.
Example
The simplest form returns the next value from update. On history this is all you need:
function onBar(ctx) {
const k = 2 / (ctx.params.length + 1);
const ema = ctx.accum("ema", null, (prev) =>
prev == null ? ctx.close : prev + k * (ctx.close - prev)
);
ctx.plot("ema", ema);
}On a live chart, onBar runs again for the forming bar on every tick. Before each re-run the chart rolls back what the previous run wrote to ctx.accum, so the form above steps the EMA once per bar, not once per tick. Scripts written before the rollback keep the last closed value and recompute the current one from it, which still works:
function onBar(ctx) {
const k = 2 / (ctx.params.length + 1);
const st = ctx.accum("ema", () => ({ t: null, base: null, cur: null }), (s) => s);
if (st.t !== ctx.time) { // first call for this bar
st.base = st.cur; // last bar's value becomes the base
st.t = ctx.time;
}
st.cur = st.base == null ? ctx.close : st.base + k * (ctx.close - st.base);
ctx.plot("ema", st.cur);
}The object form keeps several values in one slot: mutate it, with an update that returns it unchanged. For state that only needs closed bars, return early instead:
if (ctx.barConfirmed === false) return; // count each bar once
const st = ctx.accum("st", () => ({ high: -Infinity, count: 0 }), (s) => s);
st.high = Math.max(st.high, ctx.high);
st.count += 1;Rules
Keep slots to plain data: numbers, strings, booleans,
null, plain objects and arrays. A function, aMap, a class instance or a timeframe handle in a slot fails withDataCloneError.Use a stable, literal
idper slot. A script can hold up to 1,024 slots; past 256 you get a warning.updateruns every time you callctx.accum, including on every tick of the forming bar. The chart rolls back the forming bar's writes before each tick, so appends, counts and running values see each bar once. Guard onctx.barConfirmed === falseonly for what must wait for the close.A script that keeps state must declare how far back that state remembers, with a top-level
warmup(...)call after its inputs, such aswarmup((w) => w.ema(length))orwarmup((w) => w.day())for state that resets each day. Without it lint fails withwindow/warmup-required. See Warmup and memory.State that never decays or resets, such as a running total or a latched flag, needs
warmup((w) => w.forever()). A count fails lint withstate/never-forgets.Inside a timeframe handler,
ctx.accum(id, init, update?)writes to that handle'sstate, andupdateis optional. See Timeframe handles and runOn.
Related state
Need | Use |
|---|---|
At most one event per session, zone or key |
|
A scratch object for one run, definitions only |
|
Values across events in a study |
|
A | A local variable inside |
