ChartnautDocs

onBar

onBar(ctx) is the function Chartnaut calls once per bar: read price and settings, update state, emit to your outputs. This page covers what is particular to an indicator's onBar, above all the forming bar.

Signature

export function onBar(ctx: ScriptedCtx): void

Export it (export function onBar) or declare it at the top level (function onBar). One per script. Everything else (meta, dialog, input.*, output.*, warmup, layout) runs once, at the top level, before the first bar.

What ctx gives you

Member

What it is

open, high, low, close, volume, volumeBid, volumeAsk, time

The current bar. time is unix seconds, UTC.

barIndex

0-based index within this calculation pass. It restarts on a full recalculation, so derive lasting counters from time or ctx.accum.

barConfirmed

false only on the last bar of a run whose newest bar may still be printing: the live chart's forming bar, or a server run that ends now

isLastBar

true on the newest bar of the live chart, on every tick, and on the last bar of a one-off run such as chartnaut run or the agent's dry run. false in stored runs: studies and canvases

params

Settings, by input id

accum(id, init, update)

State that lasts across bars. The only place state may live: never a module-level let.

instrument, chart, history

Tick size and sessions, the chart timeframe, and where the loaded history starts

session(...), session.isStart(mode)

Session boundaries. See Sessions.

indicators.<as>.<output>

Values from declared dependencies

summary(tf, window)

One answer over a long span. See Summaries and long windows.

volumePriceLevels

Volume at each price on this bar, with dataSource: "volume-candles"

plot, bar, area, baseline, fill, mark, label, note, hline, range, trendline

Emit to declared outputs. See Series types.

log.info, log.warn, log.error

Messages in the builder's Logs tab

Bar fields are on Price and bar data. Higher-timeframe values come from module-level timeframe(...) handles, not from ctx. See Timeframe handles and runOn.

The forming bar runs again on every tick

On the live chart, onBar runs for the forming bar on every tick, not once. Before each run the chart rolls back what the previous tick wrote to ctx.accum, so every run of the forming bar starts from the state the last closed bar left. A buffer that pushes ctx.close holds the forming bar once, and ema = prev + k * (close - prev) steps once per bar.

What the rollback does not cover is a decision. A cross, a break or a confirmed swing on the forming bar can appear on one tick and vanish on the next. Wait for the close:

if (ctx.barConfirmed === false) return; // act on closed bars only

Or draw on every tick and gate only the decision, as the example below does. The examples keep state with a small foldBar helper that commits on a new bar. It predates the rollback and still works.

Drawing on the newest bar

ctx.isLastBar marks the newest bar. Use it for something drawn once at the right edge, through an output that keeps only its latest emit, such as a priceLine:

output.priceLine({ id: "last" });

export function onBar(ctx) {
  if (ctx.isLastBar) ctx.hline("last", ctx.close);
}

There is no lastBarIndex or lastBarTime: results are cached in pieces, and which bar is newest depends on when you run. See Price and bar data.

Example: an EMA with cross markers

meta({ shortName: "EMA Cross", kind: "overlay" });
dialog({ title: "EMA Cross" });
const length = input.number({ id: "length", label: "Length", default: 20, min: 1 });
warmup((w) => w.ema(length));
const emaLine = output.line({ id: "ema", color: "#2196f3" });
output.markers({ id: "crossUp", on: emaLine, color: "#26a69a" });
layout(tab({ id: "main", title: "Main" }, section({ id: "p", title: "Parameters" }, length)));

export function onBar(ctx) {
  const k = 2 / (ctx.params.length + 1);
  const st = ctx.accum("st", () => ({ t: 0, done: null, tip: null }), (s) => {
    if (s.t !== ctx.time) { s.done = s.tip; s.t = ctx.time; }
    const prev = s.done;
    const ema = prev == null ? ctx.close : prev.ema + k * (ctx.close - prev.ema);
    s.tip = { ema, above: ctx.close > ema, wasAbove: prev == null ? null : prev.above };
    return s;
  });
  ctx.plot("ema", st.tip.ema);

  // Act on closed bars only: the forming bar can cross and uncross many times.
  if (ctx.barConfirmed !== false && st.tip.wasAbove === false && st.tip.above) {
    ctx.mark("crossUp", { shape: "arrowUp", position: "belowBar", text: "Up" });
  }
}

The EMA keeps state, so the script declares warmup((w) => w.ema(length)), 30 times the length. Without it lint fails with window/warmup-required. See Warmup and memory.

Checklist

  1. Every ctx.params.<id> has an input.* with that id.

  2. Every emit uses a declared output id with the matching verb: plot for lines, bar for histograms.

  3. All state is in ctx.accum, survives the forming bar re-running, and is covered by a warmup declaration.

  4. Nothing is emitted inside a timeframe(...) handler; onBar reads the handle and emits.

Not a definition

Indicators have no ctx.emit and no events.declare. To mark setups you can run over history and study, write a definition.