ChartnautDocs

Emit and labels

ctx.emit records that the setup happened on this bar. On the chart it draws the event's paint. When the definition runs over history, each accepted emit becomes a stored event that studies read.

Every emit needs a label: the text on the chart pin and in the events list. An emit without one is dropped.

Signature

ctx.emit(
  eventId: string,
  payload?: Record<string, unknown>,
  opts?: {
    label: string;       // required
    subtitle?: string;
    timeStart?: number;  // unix seconds
    timeEnd?: number;    // unix seconds
    price?: number;
  }
): void

Option

Notes

label

Required. Trimmed; an empty label counts as missing. Stored as _displayLabel.

subtitle

A smaller line under the pin title. Keep it to about two datapoints, such as "52.9% trending · n=68". Stored as _displaySubtitle.

timeStart

Start of the event, in unix seconds. Defaults to ctx.time. The pin and bar tint sit here.

timeEnd

End of the event, in unix seconds. Defaults to ctx.time.

price

The Y position for "highlight_price" paints. Overrides payload.price. Chart only: it is not stored with the event.

Example

// s comes from ctx.session(...), st from ctx.accum(...)
ctx.emit("london_range", {
  sessionKey: s.key,
  high: st.high,
  low: st.low,
}, {
  label: `London range ${(st.high - st.low).toFixed(1)}`,
  subtitle: `H ${st.high.toFixed(2)} · L ${st.low.toFixed(2)}`,
  timeStart: s.start,
  timeEnd: ctx.time,
  price: st.high,
});

A point-in-time event needs neither timeStart nor timeEnd. For an event that spans an interval, such as a session, a gap or a consolidation, set both so the stored event covers the whole interval. Set timeStart to the bar where the pin should sit: the pin goes at timeStart, not at the bar that detected the event.

When an emit is dropped

Cause

What you see

The id was never passed to events.declare

ctx.emit("id"): no matching events.declare; a dry run reports emit_dropped: unknown_event_id

No label, or an empty one

ctx.emit("id"): label is required; a dry run reports missing_label

oncePer already saw these key values

Nothing. It is dedupe working as declared. See oncePer.

The bar is inside the warmup

Nothing stored. The oncePer ledger still advances, so an event suppressed in warmup does not fire again after it.

meta.kind is "indicator"

ctx.emit throws

One run stores at most 100,000 events. The run stops with an error when it passes that. If a definition's events are missing after a run, see Emit not collected.

Label rules

  • Write the label inline in the call: ctx.emit("id", payload, { label: "..." }). Lint events/emit-missing-label reads the call itself, so it warns when the options come from a variable.

  • Write labels a trader can read, such as "OR break ↑", never the raw id "or_break_up". The pin shows the label, not the id.

  • Do not declare _displayLabel or _displaySubtitle in the payload. Lint fails with events/reserved-payload-key.

  • Do not put display text only in the payload. The chart and the events list read the label.

Labels and oncePer

oncePer decides whether the event fires again. The label decides how a fired event is titled. Use both, and never write your own dedupe.

Events that close chart drawings

A range, hline or trendline output declared with mode: "invalidateOnEvent" and invalidateOnEvent: "<event id>" seals its live drawings on the bar that event fires. Detect the break in onBar, emit, and let the output close itself. Do not repeat the same condition in an invalidateWhen predicate. See Drawing on the chart.

Studies

Studies do not call ctx.emit. They read stored events, which carry _displayLabel and, when set, _displaySubtitle. See Study API.