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[] };
}): voidField | Notes |
|---|---|
| What |
| Free-form tag stored on every event, such as |
| Field name to type and a plain-English |
| How the event shows on the chart. Defaults to |
| 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 |
|---|---|
| A pin at |
| The pin plus a narrow tint on the |
| The pin placed at a price: the emit's |
| The pin at the price plus the candle tint. |
| 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 |
|
One per session |
|
One per symbol per day |
|
Every occurrence | Omit |
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;
}): voidRecord 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 |
|---|---|---|
| error |
|
| error |
|
| error | The same event id declared twice |
| error | A payload field with an unknown type |
| error | A |
| error |
|
| warn | A |
| warn |
|
| warn | A price paint without a numeric |
| warn |
|
| error | The same snapshot id declared twice |
| error | A snapshot schema field with an unknown type |
All rules are listed on Lint and error codes.
