feature
Script anatomy
Every Chartnaut script is built in two phases: you declare what exists (outputs, events, results, settings), then you write hooks that run over bars or events.
Understanding that split makes the rest of the API click — declarations drive the UI and lint; hooks do the live work. Shared helpers (bars, drawings, deps) live under Shared runtime.
Phase 1 — Declarations (top level)
Top-level calls register metadata the product UI, lint, and collect pipeline read statically:
Tier | Typical declarations |
|---|---|
Indicator |
|
Definition |
|
Study |
|
Declarations drive settings dialogs, result schemas, dependency contracts, and lint. If it is not declared, runtime calls that reference it usually fail lint or drop at runtime.
Phase 2 — Runtime hooks
Tier | Hook | When it runs |
|---|---|---|
Indicator |
| Once per bar (and live updates) |
Definition |
| Once per bar |
Study |
| Once per matching event, then once at end |
Minimal templates
Indicator
meta({ shortName: "EMA", kind: "overlay" });
dialog({ title: "EMA" });
const length = input.number({ id: "length", label: "Length", default: 20, min: 1 });
output.line({ id: "ema", color: "#2196f3" });
layout(
tab({ id: "main", title: "Main" },
section({ id: "p", title: "Parameters" }, length))
);
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);
}Definition (flow)
meta({ name: "Tick Signal", kind: "flow", slug: "tick-signal" });
events.declare({
id: "tick",
intent: "alert",
payload: { reason: { type: "string", description: "Why this fired" } },
paint: "pin",
});
outputs.declare({ id: "close_line", kind: "line", name: "Close" });
export function onBar(ctx) {
ctx.plot("close_line", ctx.close);
if (ctx.close > ctx.open) {
ctx.emit("tick", { reason: "bullish_bar" }, {
label: "Bull bar",
price: ctx.close,
});
}
}Study
meta({ name: "Tick count", kind: "study" });
flows.declare({ slug: "tick-signal", as: "sig" });
results.declare({ id: "rows", kind: "table", title: "Rows" });
export function onEvent(ctx) {
if (ctx.event.flow !== "sig") return;
ctx.collect("rows", { time: ctx.event.time });
}
export function onFinish(ctx) {
ctx.publish("rows", ctx.collected("rows"));
}Rules that apply everywhere
Prefer
ctx.accumfor cross-bar state. Do not use module-levellet/varfor bar memory.Timeframe arguments must be string literals or values from
input.timeframe(viactx.params).Lint before you snapshot. Agents should call their lint tools before proposing edits.
Shared APIs for bars, inputs, drawings, deps, and MTF are documented once under Shared runtime. The Indicator, Definition, and Study sections cover what each type adds on top.
