Series types
Every series an indicator draws is declared with an output.* call at the top of the script and fed from onBar with the matching ctx.* verb. This page lists every kind, its verb and its options.
Output kinds
Declare | Emit in | Draws |
|---|---|---|
|
| A line; |
|
| Bars from |
|
| A line filled down to zero; |
|
| A line coloured |
|
| A ribbon between two values; |
|
| Arrows, circles, squares and text at bars or prices |
|
| A full-width horizontal line; last write per bar wins |
|
| A horizontal segment from |
|
| A rectangle, with a lifecycle |
|
| A diagonal segment, with a lifecycle |
|
| One upper line, lower line and fill per row of an |
| the returned handle: | Rows the script paints itself. See Layers. |
The verb must match the kind. ctx.plot("hist", v) on an output.histogram fails lint with outputs/kind-mismatch, and a histogram fed with ctx.plot draws nothing. An id you emit but never declared fails with outputs/unknown-id; one you declare but never emit is a warning (outputs/declared-unused).
Output ids are unique across all kinds: a line and a histogram cannot share an id. The ids are also the keys other scripts read when they declare this indicator as a dependency, so renaming one breaks its readers.
Options every series takes
Option | Effect |
|---|---|
| Draw in a subpane from |
| Bind to a price scale from |
| A fixed colour, or one bound to an |
| Show or hide the series from an |
output.line also takes lineWidth / lineWidthFrom and lineStyle / lineStyleFrom. Line, histogram, area and baseline take labelFrom (an input.boolean for the price-scale label) and titleWhenLabeled. output.area takes lineWidth / lineWidthFrom. output.priceLine takes a static price, title, lineWidth and lineStyle. output.bandTemplate takes pane but not scale.
Bind styling to inputs with the *From options rather than reading colours in onBar. Read a colour in onBar only when it changes per bar: see Per-bar colour.
Lines, histogram and fill
meta({ shortName: "MACD", kind: "pane" });
dialog({ title: "MACD" });
const fast = input.number({ id: "fast", label: "Fast", default: 12, min: 1 });
const slow = input.number({ id: "slow", label: "Slow", default: 26, min: 1 });
const sig = input.number({ id: "signal", label: "Signal", default: 9, min: 1 });
warmup((w) => w.sum(w.ema(slow), w.ema(sig)));
const macdLine = output.line({ id: "macd", color: "#2196f3" });
output.line({ id: "signal", color: "#ff9800" });
output.histogram({ id: "hist" });
output.fill({ id: "spread", color: "rgba(33,150,243,0.12)" });
output.markers({ id: "crosses", on: macdLine });
layout(tab({ id: "main", title: "Main" }, section({ id: "p", title: "Parameters" }, fast, slow, sig)));
export function onBar(ctx) {
const st = ctx.accum("st", () => ({ t: 0, done: null, tip: null }), (s) => {
if (s.t !== ctx.time) { s.done = s.tip; s.t = ctx.time; }
const p = s.done;
const ema = (prev, n, x) => (prev == null ? x : prev + (2 / (n + 1)) * (x - prev));
const f = ema(p && p.f, ctx.params.fast, ctx.close);
const sl = ema(p && p.sl, ctx.params.slow, ctx.close);
const macd = f - sl;
const signal = ema(p && p.signal, ctx.params.signal, macd);
s.tip = { f, sl, macd, signal, wasAbove: p ? p.macd > p.signal : null };
return s;
});
const { macd, signal, wasAbove } = st.tip;
const hist = macd - signal;
ctx.plot("macd", macd);
ctx.plot("signal", signal);
ctx.bar("hist", hist, hist >= 0 ? "#26a69a" : "#ef5350");
ctx.fill("spread", Math.max(macd, signal), Math.min(macd, signal));
if (ctx.barConfirmed !== false && wasAbove === false && macd > signal) {
ctx.mark("crosses", { shape: "arrowUp", position: "belowBar", text: "Cross" });
}
}ctx.fill takes both bounds in one call. When either bound is missing on a bar, pass NaN for it and the ribbon breaks there.
Breaking a line
A line, area, baseline or fill breaks at a bar where you emit NaN, null or undefined: the line stops at the bar before and starts again at the next value. For a fill, either bound being NaN, null or undefined breaks it. This is Pine's plot(na) with plot.style_linebr.
A bar where you do not call the verb at all is bridged: the line joins the values either side. So a line drawn only on some bars stays joined, and a gap needs an explicit NaN:
ctx.plot("orHigh", inSession ? st.high : NaN); // breaks between sessionsPer-bar colour
ctx.plot, ctx.area and ctx.fill take an optional last argument, a colour for this bar. A line takes it from that point forward; an area colours its line and fill with it; a fill colours the ribbon. A colour that is missing or not valid falls back to the series colour. ctx.baseline has no per-bar colour.
A per-bar colour wins over the colour the trader sets in the settings dialog. Expose the colours you pick from as input.color settings and read them from ctx.params, so the trader can still change them:
const up = input.color({ id: "up", label: "Up", default: "#26a69a" });
const down = input.color({ id: "down", label: "Down", default: "#ef5350" });
output.line({ id: "trend" });
export function onBar(ctx) {
// st.isUp and st.level come from your Supertrend state
ctx.plot("trend", st.level, st.isUp ? ctx.params.up : ctx.params.down);
}Markers
One output.markers declaration backs ctx.mark, ctx.label and ctx.note; there is no separate label output. Without on, markers attach to the candles; on: lineHandle rides a declared output.line.
A marker's position decides whether it takes a price:
| Anchored to |
|
|---|---|---|
| The candle | Not allowed |
| An exact price | Required |
ctx.mark("signals", { shape: "arrowDown", position: "atPriceTop", price: ctx.high, text: "SH" });shape is "circle" (default), "square", "arrowUp", "arrowDown" or "none". time defaults to ctx.time; pass an earlier bar's time to mark a swing after it is confirmed. Markers also take color, size (0 to 4), id, labelMode, labelColor, labelPosition and labelDedupe. ctx.label(id, { price, text, color?, background?, size? }) puts text at a price on the current bar, and ctx.note(id, { price, text }) is a text-only marker.
Several markers per output per bar are allowed. A re-run of the forming bar replaces that bar's markers instead of adding more.
priceLine or hline
Both are driven by ctx.hline; the declaration decides which one you get.
output.priceLine({ id: "prevClose", color: "#9e9e9e", title: "Prev close" });
output.hline({ id: "dayOpen", color: "#ffb300", name: "Day open" });
// in onBar:
ctx.hline("prevClose", prevClose); // full-width, moves as you re-emit
ctx.hline("dayOpen", ctx.open, { time1: day.start }); // segment from the day's startpriceLine: no
time1. One chart-wide line per id; stop emitting it and it disappears.hline:
time1is required on every call. Without it the level draws nothing, and lint refuses the call (outputs/hline-missing-time1).
output.hline, output.range and output.trendline share a lifecycle: mode ("replace", "all", "invalidateOnCustomFunction", "invalidateOnEvent"), invalidateWhen, expires, labels and zIndex. The modes, presets and the required expires are on Drawing on the chart.
bandTemplate
output.bandTemplate turns each row of an input.array into series. Emit each row's values with ctx.plot, using the template's patterns with {id} replaced by the row's id:
output.bandTemplate({
from: bands, // an input.array handle
upperIdPattern: "band_up_{id}",
lowerIdPattern: "band_dn_{id}",
fillIdPattern: "band_fill_{id}", // fills between the two, no emit needed
itemStyle: { colorField: "color", fillEnabledField: "fillEnabled", fillColorField: "fillColor" },
titleTemplate: "Band {deviation}",
});
// in onBar, for each enabled row:
ctx.plot("band_up_" + row["id"], upper);
ctx.plot("band_dn_" + row["id"], lower);itemIdField and itemEnabledField default to "id" and "enabled". VWAP is a full example.
