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

meta, dialog, input.*, output.*, layout, indicators.declare

Definition

meta, events.declare, snapshots.declare, outputs.declare, imports.use, indicators.declare

Study

meta, flows.declare, indicators.declare, dimensions.declare, results.declare

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

onBar(ctx)

Once per bar (and live updates)

Definition

onBar(ctx) / export function onBar

Once per bar

Study

onEvent(ctx) then onFinish(ctx)

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.accum for cross-bar state. Do not use module-level let / var for bar memory.

  • Timeframe arguments must be string literals or values from input.timeframe (via ctx.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.

Next

  1. Shared runtime

  2. First indicator guide

  3. First definition guide

  4. First study guide