ChartnautDocs

Dependencies

A study measures around its events using what its dependencies already produced: other events from declared definitions, the lines and boxes they drew, and indicator values at any time. This page covers reading them. Declaring them is in Declarations.

Read only what a dependency exposes. If the level, state or time you need is not in its events, payload, drawings, lifecycle or outputs, add it to the definition or indicator first. Rebuilding a volume profile or a gap detector from candles inside a study gives numbers that disagree with what the chart shows.

Events from a definition

flows.declare({ slug: "tpo-zones", as: "tpo", version: 5 });

const later = ctx.flows.tpo.events({
  after: ctx.event.time,
  filter: (e) => e.id === "zone_invalidated" && e.payload.zoneId === ctx.event.payload.zoneId,
});
const invalidation = later.length ? later[0] : null;

events(query?) returns events shaped like ctx.event. In onEvent it reads the definition's recorded events for the whole run, before and after the current event, whether or not your study collected them. In scope it returns only events that had fired by the current event's fire bar, and an open query ends there.

Query field

Meaning

window: [secsBefore, secsAfter]

Seconds around ctx.event.time

after, before

Absolute unix seconds

filter

A function that keeps an event when it returns true

limit

At most this many. With before and no after, the latest ones; otherwise the first ones.

limit is applied before filter, so { after, filter, limit: 1 } can drop the match you wanted. To get the first match, leave out limit, filter, and take [0].

Do the join in onEvent. For a later event that belongs to the current one, query it with after, measure, and collect one finished row. onFinish has no event clock and cannot rebuild the pairs.

ctx.collected is a different source: only the rows this study collected itself. Use events() for another definition's history.

Drawings and lifecycle

Each alias under ctx.flows and ctx.indicators also reads what the script drew:

const levels = ctx.flows.poc.hlines({ window: [3 * 86400, 0] });   // [{ id, time, price, opts }]
const marks = ctx.flows.orb.marks({ before: ctx.event.time, limit: 5 });

Accessor

Returns

hlines(q)

[{ id, time, price, opts }]

marks(q)

[{ id, time, data }]

ranges(q)

[{ time, data }]

plots(q)

[{ id, time, value }]

trendlines(q)

[{ time, data }]

lifecycle(q)

Box instances with createdAt, closedAt, state, bounds and closeReason

layer(id, q)

Rows of a declared output.layer, with createdAt, closedAt and the row's fields

q takes window, after, before and limit, as for events. lifecycle also takes outputId, kind: "range", state ("live", "closed" or "any") and at, which returns what was alive at that second. layer takes state, at, after, before and limit.

Use lifecycle() for when a box formed and when it closed. ranges() is the raw drawing, and treating its fields as lifecycle state is a lint error (study/ranges-not-lifecycle). A study that measures survival or invalidation needs either a later event joined with events({ after, filter }) or lifecycle() (study/lifecycle-source-missing).

For a layer, join on the key field the definition's manifest names, with at: ctx.event.time, instead of scanning every row.

In scope, every accessor here returns what stood at the fire bar: drawings up to it, lifecycle instances as they were then with any later close hidden, and layer rows as they were. A layer row whose fields changed after the fire bar throws. scope has the full list.

Indicator values

indicators.declare({ slug: "atr", as: "atr14", version: 1, settings: { period: 14 } });

export function scope(ctx) {
  if (ctx.event.flow !== "orb") return null;
  return {};
}

export function onEvent(ctx, scope) {
  const atr = ctx.indicators.atr14.atr?.valueAt(ctx.event.time);
  if (!Number.isFinite(atr)) return;
  ctx.collect("atr", atr);
}

Values always sit under the output key the indicator declares, even for a single-output indicator: ctx.indicators.<as>.<outputKey>. A flat read such as ctx.indicators.atr14.valueAt(t) or ctx.indicators.atr14.value is a lint error (study/unsafe-multi-output-indicator-access). Copy output keys from the indicator's own outputs. Some keys, such as individual VWAP bands, exist only when a setting turns them on.

Use optional chaining before the method and check the result with Number.isFinite. A missing value then skips the event instead of stopping the run.

In scope, indicator reads stop at the fire bar, and a missing past value marks the event not ready. To group by an indicator, read it there: an ATR bucket at the event is a scope dimension.

Pick the right time

valueAt(unixSec) returns the value at the last bar at or before that time. Pass the time of the bar your row is about:

const end = ctx.candles.forward({ minutes: 60 });
if (!end) return;
const vwapAtEvent = ctx.indicators.vwap.vwap?.valueAt(ctx.event.time);
const vwapAtEnd = ctx.indicators.vwap.vwap?.valueAt(end.time);

Reading the indicator at the event time while the rest of the row comes from a later bar is the most common study bug. It produces rows that make no sense or get dropped.

Other reads on each output:

Read

Returns

valueAt(t)

Value at or before t

valuesInRange(from, to)

[{ time, value }] between two times, inclusive

historyAt(t, maxBars?)

Values up to t, oldest first

at(n)

n values back from the event. On an indicator that only outputs on some bars, that is n outputs back, which can be far in the past.

last(), lastTime()

The latest value at or before the event, and its time. It carries forward, so it says nothing about whether the indicator fired on the event bar.

history(maxBars?)

Values up to the event, oldest first

A bare .value on an output reads at the event time. Prefer valueAt with an explicit time.

Timeframes

Studies have no timeframe(...) handles. Candles come from the run's timeframe, and each definition's indicator values line up with the timeframe its events were recorded on. To restrict which timeframes a study may run on, set runOn in meta. See Timeframe handles and runOn.

When there are no events

A study only sees events the definition recorded when it was run over history, under the version you pinned. If a run finds none, check that version first. Emit not collected covers the other causes.