ChartnautDocs

Events and snapshots

Every event a definition emits needs an events.declare first. The declaration names the event, describes its payload, and sets how it paints on the chart. The payload schema is what studies read and filter on.

snapshots.declare is separate: it records intermediate state for the builder's Snapshots tab and never creates an event.

events.declare

events.declare({
  id: string;
  intent: string;
  payload: Record<string,
    | { type: "string" | "number" | "boolean" | "object" | "array"; description: string }
    | { type: { enum: string[] }; description: string }
    | { enum: string[] }
  >;
  paint?: "none" | "pin" | "highlight_event_bar" | "highlight_price"
        | "highlight_event_bar_and_price";
  oncePer?: { keys: string[] };
}): void

Field

Notes

id

What ctx.emit names. Unique within the definition.

intent

Free-form tag stored on every event, such as "alert", "signal" or "diag".

payload

Field name to type and a plain-English description. Studies and the Agent read the descriptions.

paint

How the event shows on the chart. Defaults to "pin".

oncePer

Declarative dedupe over payload fields.

Call it at the top level. Helper files in a multi-file package can declare events too. ctx.emit with an id that was never declared is dropped: the chart logs no matching events.declare and a dry run reports emit_dropped: unknown_event_id.

Example

events.declare({
  id: "london_session_classified",
  intent: "signal",
  payload: {
    sessionKey: { type: "string", description: "London session key" },
    regime: { type: { enum: ["trend", "range"] }, description: "Regime at classification" },
    atr: { type: "number", description: "ATR at classification" },
  },
  oncePer: { keys: ["sessionKey"] },
  paint: "pin",
});

Payload

Keep keys stable and values normalised, so a study reads them directly without reverse-engineering. Put facts known when the event fires in the payload (direction, break side, the session it belongs to). Outcomes that only become known later belong in the study that measures them.

Two keys are reserved. _displayLabel and _displaySubtitle are written by the runtime from the emit's label and subtitle, and declaring either fails with events/reserved-payload-key. See Emit and labels.

paint

Value

On the chart

"pin"

A pin at timeStart showing the emit's label. The default.

"highlight_event_bar"

The pin plus a narrow tint on the timeStart candle.

"highlight_price"

The pin placed at a price: the emit's price option, else payload.price.

"highlight_event_bar_and_price"

The pin at the price plus the candle tint.

"none"

Nothing drawn. The event is still stored.

Paint only affects the chart. Running the definition over history stores the event whatever its paint.

For "highlight_price" and "highlight_event_bar_and_price", declare price: { type: "number", description: "..." } in the payload. Lint events/paint-needs-price warns when it is missing. Lint events/hidden-paint-explicit warns on "none": use it only when you want data-only events.

To draw a zone over time and price, use an outputs.declare({ kind: "range" }) output with ctx.range instead of event paint. See Drawing on the chart.

The old names "marker" (read as "pin"), "highlight" and "both" (both read as "highlight_event_bar") still load. Do not use them in new scripts.

oncePer

oncePer: { keys: [...] } keeps at most one event per unique combination of those payload values, over the whole run. Once an (event id, key values) pair has fired, later emits with the same values are dropped. Chartnaut enforces it identically on the chart and when the definition runs over history, including across the chunks a long run is split into.

Pick the keys that identify the event:

You want

keys

One per day

["dayKey"]

One per session

["sessionKey"]

One per symbol per day

["symbol", "dayKey"]

Every occurrence

Omit oncePer

Every key must be a field you pass in the payload. A missing field counts as one fixed value, so an event keyed on a field you never send fires once in the entire run and never again.

Use oncePer rather than a dedupe set kept in ctx.accum. A hand-rolled ledger drifts from the declaration and lint cannot check it.

oncePer.windowBars is deprecated and ignored.

snapshots.declare

snapshots.declare({
  id: string;
  schema: Record<string, "string" | "number" | "boolean" | "object" | "array" | { enum: string[] }>;
  retain?: number;
}): void

Record one from onBar with ctx.snapshot(id, payload). Snapshots appear in the builder's Snapshots tab only. Running the definition over history does not store them. retain is advisory and not enforced.

snapshots.declare({
  id: "pre_london",
  schema: { high: "number", low: "number" },
});

export function onBar(ctx) {
  ctx.snapshot("pre_london", { high: ctx.high, low: ctx.low });
}

Lint

Rule

Severity

Trigger

events/missing-id

error

events.declare without id

events/missing-intent

error

events.declare without intent

events/duplicate-id

error

The same event id declared twice

events/bad-payload-type

error

A payload field with an unknown type

events/payload-missing-description

error

A { type } payload field without description

events/reserved-payload-key

error

_displayLabel or _displaySubtitle in the payload

events/unknown-paint

warn

A paint value outside the list above

events/hidden-paint-explicit

warn

paint: "none"

events/paint-needs-price

warn

A price paint without a numeric payload.price

events/emit-missing-label

warn

ctx.emit without label

snapshots/duplicate-id

error

The same snapshot id declared twice

snapshot/schema-mismatch

error

A snapshot schema field with an unknown type

All rules are listed on Lint and error codes.