ChartnautDocs

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): T

The 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, a Map, a class instance or a timeframe handle in a slot fails with DataCloneError.

  • Use a stable, literal id per slot. A script can hold up to 1,024 slots; past 256 you get a warning.

  • update runs every time you call ctx.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 on ctx.barConfirmed === false only 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 as warmup((w) => w.ema(length)) or warmup((w) => w.day()) for state that resets each day. Without it lint fails with window/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 with state/never-forgets.

  • Inside a timeframe handler, ctx.accum(id, init, update?) writes to that handle's state, and update is optional. See Timeframe handles and runOn.

Need

Use

At most one event per session, zone or key

oncePer on events.declare, never a dedupe map in ctx.accum. See Emit and labels

A scratch object for one run, definitions only

ctx.store, cleared between runs

Values across events in a study

ctx.collect. Studies have no ctx.accum. See Collect and publish

A mode: "once" indicator

A local variable inside onRun. ctx.at(i) has no accum