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): voidExport 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 |
|---|---|
| The current bar. |
| 0-based index within this calculation pass. It restarts on a full recalculation, so derive lasting counters from |
|
|
|
|
| Settings, by input id |
| State that lasts across bars. The only place state may live: never a module-level |
| Tick size and sessions, the chart timeframe, and where the loaded history starts |
| Session boundaries. See Sessions. |
| Values from declared dependencies |
| One answer over a long span. See Summaries and long windows. |
| Volume at each price on this bar, with |
| Emit to declared outputs. See Series types. |
| 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 onlyOr 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
Every
ctx.params.<id>has aninput.*with that id.Every emit uses a declared output id with the matching verb:
plotfor lines,barfor histograms.All state is in
ctx.accum, survives the forming bar re-running, and is covered by awarmupdeclaration.Nothing is emitted inside a
timeframe(...)handler;onBarreads 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.
