ChartnautDocs

Drawing on the chart

Everything a script draws, whether a line, a marker, a level or a zone, follows one pattern: declare the output at the top of the file, then draw it from onBar with the matching ctx.* call and the same id.

Declare

Script

How you declare

Indicator

output.line, histogram, area, baseline, fill, markers, priceLine, hline, range, trendline, bandTemplate, layer

Definition

outputs.declare({ id, kind }) with kind line, histogram, area, baseline, fill, mark, note, priceLine, hline, range or trendline; output.layer for layers

Study

No declaration. Annotation helpers take their own ids: see the table at the end

The older definition shortcuts output.line / output.mark / output.hline still work. Use outputs.declare in new definitions.

Declare kind to ctx call

Kind

Draw with

line

ctx.plot(id, value, color?). color overrides this bar onward. NaN breaks the line

histogram

ctx.bar(id, value, color?). color overrides one bar, for MACD-style red and green

area / baseline

ctx.area(id, value, color?) / ctx.baseline(id, value). NaN breaks the line

fill

ctx.fill(id, top, bottom, color?). NaN in either bound breaks the ribbon

markers (indicator) / mark (definition)

ctx.mark(id, { price?, text?, shape?, position?, time? })

markers (indicator) / note (definition)

ctx.note(id, { price, text }): text at a price, no glyph

markers (indicator)

ctx.label(id, { price, text }): text tethered to a price on this bar

priceLine

ctx.hline(id, price) with no time1: a chart-wide line. Indicators only

hline

ctx.hline(id, price, { time1 }): a level that starts at time1

range

ctx.range(id, { time1, time2, price1, price2 })

trendline

ctx.trendline(id, { time1, price1, time2, price2 })

layer

handle.upsert(row) / patch / seal. See Layers & rows

A bar where you skip the call is bridged: the line joins the values either side. Emit NaN, null or undefined to break it, as Pine's plot.style_linebr does. A per-bar colour wins over the colour set in the settings dialog, so pick it from input.color settings. See Series types.

A call whose id is not declared fails lint with outputs/unknown-id, and a call that does not match the declared kind with outputs/kind-mismatch. A declared output that is never drawn warns with outputs/declared-unused.

An hline output draws only emissions that carry time1. Without it the level is invisible, and a definition fails lint with outputs/hline-missing-time1. Use time1: ctx.time for "starts at this bar", or the time of the swing, open or breakout the level belongs to. For a chart-wide line on an indicator, declare output.priceLine instead.

Example (definition)

outputs.declare({ id: "close_line", kind: "line", name: "Close", color: "#64b5f6" });
outputs.declare({ id: "signal", kind: "mark", name: "Signal", labelMode: "onHover" });
outputs.declare({ id: "zone", kind: "range", name: "Zone" });

export function onBar(ctx) {
  ctx.plot("close_line", ctx.close);
  if (ctx.close > ctx.open) {
    ctx.mark("signal", { price: ctx.high, text: "Up", position: "atPriceTop" });
  }
  ctx.range("zone", {
    time1: ctx.time,
    time2: ctx.time,
    price1: ctx.low,
    price2: ctx.high,
  });
}

Markers

ctx.mark always takes two arguments, the output id and an object. ctx.mark fills in time (this bar), shape ("circle") and position ("aboveBar") when you leave them out. Shapes are "circle", "square", "arrowUp", "arrowDown" and "none". Positions "aboveBar", "belowBar" and "inBar" anchor to the candle and ignore price. Positions "atPriceTop", "atPriceMiddle" and "atPriceBottom" anchor to an exact price and need price. Pass time to place a marker on an earlier bar, such as a swing you confirmed a few bars later.

Lifecycle: how levels, zones and trendlines end

hline, range and trendline outputs take a mode on the declaration:

Mode

Behaviour

"replace" (default)

One live drawing per id. Each emit moves or grows it

"all"

Each distinct time1 is its own drawing, live for ever. The same time1 refreshes in place

"invalidateOnCustomFunction"

Like "all", and each drawing closes on the first later bar where invalidateWhen is true

"invalidateOnEvent"

Like "all", and every live drawing closes when one of the invalidateOnEvent event ids fires

Under "all", give every emit a fresh time1. A literal time1 collapses every emit onto one drawing.

The two invalidating modes also need expires: how long a drawing stays live if nothing takes it out. It is a duration, "30m", "12h", "5d", "4w", or "max" for no expiry. Time rather than bars, so it means the same on every chart timeframe and on the server. Missing expires fails lint with output/expires-required; expires on a mode that never closes warns with output/expires-without-mode.

output.hline({
  id: "pdh",
  mode: "invalidateOnCustomFunction",
  invalidateWhen: "wickAbove",
  expires: "5d",
  labelMode: "always",
});

On each bar the engine checks events first, then the predicate, then expiry. The bar a drawing is born on never closes it. A drawing that expires closes with closeReason: "expired".

Under "invalidateOnCustomFunction", a level whose time1 is earlier than the bar that emits it, such as a swing high confirmed three bars later, is checked against the bars in between as well. If price already took it out, it is born closed.

expires is available on indicator output.hline, output.range and output.trendline. Definition outputs.declare does not take it yet.

Presets

Output

Presets for invalidateWhen

range

"enter", "exit", "tradeThrough", "closeInside", "closeBeyond", "closeAbove", "closeBelow", "wickAbove", "wickBelow"

hline

"wickAny", "closeOnly", "closeAbove", "closeBelow", "wickAbove", "wickBelow"

trendline

None. A function only

There is no default predicate. "invalidateOnCustomFunction" without invalidateWhen fails lint with outputs/invalidate-needs-policy, and does nothing at runtime.

Custom predicates

A function on the declaration receives { bar, entry, history }. bar is the bar being checked (time, open, high, low, close). entry is the drawing: top / bottom for a range, price for an hline, the endpoints for a trendline, plus id, time1, emittedAt, ageBars and your meta. history is the bars before bar, oldest first; on the chart it holds at most the last 200. Return true to close. Keep it pure: no side effects, no async. Destructure the argument as ({ bar, entry, history }): a predicate that reads ctx.low is reading the wrong shape and fails lint with output/invalidate-predicate-shape, and naming the argument ctx warns with output/invalidate-predicate-ctx-name.

output.range({
  id: "fvg",
  mode: "invalidateOnCustomFunction",
  invalidateWhen: ({ bar, entry }) =>
    entry.meta.dir === "bull" ? bar.low <= entry.bottom : bar.high >= entry.top,
  expires: "10d",
});
// in onBar, once you have found a bullish gap { time, low, high }
ctx.range("fvg", {
  time1: gap.time,
  time2: ctx.time,
  price1: gap.low,
  price2: gap.high,
  meta: { dir: "bull" },
});

Put the rule on the declaration and pass per-drawing numbers through meta. A per-emit invalidateWhen accepts a preset name. A function on an emit fails lint with output/invalidate-per-emit-fn: the engine cannot re-check it after the stretch of history that created it, on the chart or on the server. meta is plain JSON.

Scripts that read these drawings from another script use ctx.indicators.<as>.lifecycle(): see Declaring indicator dependencies.

Labels

Option

Values

labelMode

"always", "onHover", "onLastBar", "none"

labelDedupe

"none", "byBar", "byPrice"

labelPosition on output.hline

"right" (default), "top", "bottom"

Call

Lint

ctx.mark

warns outputs/mark-missing-text when labelMode is "always" or "onHover" and text is missing

ctx.range

warns outputs/range-missing-label when labelMode is "always" and label is missing

ctx.note

fails outputs/note-missing-price without price

ctx.trendline

fails outputs/trendline-missing-coords without all four coordinates

Study annotations

Studies draw with their own helpers, which take times explicitly:

Call

Signature

plot

ctx.plot(id, time, value)

mark

ctx.mark(time, price, opts). Always pass opts, {} if you have none

hline

ctx.hline(price, opts?)

range

ctx.range(id, time1, time2, opts?)

note

ctx.note(time, text)

Empty values

Skip the call when a value is not ready. Never draw NaN or undefined.

if (ema == null) return;
ctx.plot("ema", ema);

Series options, such as a histogram's base and fills between two lines, and the indicator-only bandTemplate are on Series types.