Plotting

View as MarkdownOpen the editor

A wrun indicator draws by declaring, not by calling: it declares output(name, plot, panel, options) once at the top of the file and writes one number per bar, and the chart draws that number as a line, a column, a mark, or a tint from the declaration.

That is why the module never touches a color, and why every drawn output is a value an alert can follow once the indicator is published (Alerts). This page maps everything you might want to draw to its wrun form, and compiles the forms that exist.

The map

You wantwrun formNotes
a plotted seriesoutput(name, line | bar | scatter | candle, panel)the plot kind is the declaration, not a runtime argument
a lineoutput(name, line, panel, { color, width, line_style, opacity, glow, step, smooth, gradient, split })a filled line is area; glow is a soft halo in pixels (Styling); smooth bends it into curves, step draws it as horizontal and vertical segments, gradient shades the stroke by height, split colours it above and below a level or by slope
columnsoutput(name, bar, panel, { width, base, grading, stack })one value per bar; width is a share of the bar slot (1 = columns touch), base the level they grow from, grading fades them by magnitude, stack stacks them on their siblings
a histogramoutput(name, histogram, panel, { color_by, colors, corner })columns grow from zero (or from base); a sign ladder colors them; corner rounds them, in pixels
candlesfour consecutive candle outputs (open, high, low, close), or a box and a segment per barthe chart draws the four as one candle; candle_style on the first output draws them hollow, as OHLC bars or as high-low bars; see below
a mark on some barsoutput(name, shape, panel, { shape, shape_where, location, char, fill, fill_opacity, glow }) or render.shapeeither picks the mark from ten shapes (char draws one character); location sits it above or below the bar or at the pane's edge
several marks or a pie per barnone: one thing per bar per renderera one-summary pie is a panel.pie snapshot; a per-level picture is a plot.levels frame (Cards, frames and panels)
text at a pricerender.text(name, { y, text, color, size, style?, align?, valign?, font_weight?, font_family? })one mark per bar whose slot was written; a style other than plain text draws each as a tag (price_label, pill, callout, badge, box, knockout, emblem)
a price tagrender.label(name, { x, y, text, style: "price_label" }) for one tag at the newest bar, or render.text with the same style for a tag per bara label's style is also plain, pill, callout, badge, box, knockout or emblem
a corner readoutrender.label(name, { position, text, offset? }) pinned to one of the nine anchors, a one-cell render.table(name, { rows: 1, cols: 1, cells, position }), or a render.hud card with one tilefixed position at one of nine anchors; the newest bar that wrote the slot wins
an output on its own scale over the candlespane(name, { place: "price" }) and output(name, plot, overlay, { pane: name })open interest or a cumulative delta drawn over price on a hidden scale of its own (Panes)
several panes below the chartpane(name, { title, height_frac, scale, min, max, format }) and output(name, plot, lower, { pane: name })up to four named panes, stacked in declaration order (Panes)
a tablerender.table(name, { rows, cols, cells, position }) for a grid of words, styled by the same literal (widths, fills, lines, per-cell colours that follow an output; Styled tables); render.hud(name, { position, tiles }) for a card of typed tilescells are string slots; tiles read outputs and slots
a rectanglebox(...) per bar, draw.box for one object, or a box handleDrawing objects
a grid of mini charts pinned to the viewportnonea dashboard is a render.table or a render.hud card; a snapshot panel below the chart is a panel.* frame (Cards, frames and panels)
a statistics-strip rowrender.stats_row(name, { output, title, format, polarity })a strip row over an output
a heatmap, a curve over a category or an index, tilespanel.heatmap, panel.line over a category or index x, panel.tilesa frame snapshot in its own pane below the chart (Cards, frames and panels)
a horizontal leveloutput(name, line, panel, { role: "guide", align?, pill_style?, font_size?, axis_label? }) written to the constant every barone full-width line at the output's last value, out of the legend; align puts its label on the line, axis_label: true on the axis; or a segment from: 0, to: 1
a background tintrender.bgcolor(name, { where, color, color_by, colors, width?, line_style? })a tint per bar where the gate is nonzero; with width a vertical line per gated bar instead of a band
colored candlesrender.barcolor(name, { where, color, color_by, colors })the bar's own candle tinted where the gate is nonzero
a fill between two linesrange("a", "b", { color }) between two drawn outputs for a band with edges, fill("a", "b", { color, opacity }) for the interior only, or a box per barthe chart draws the range as a band with its two edges and the fill as shading with none (Styling)
a name and a descriptionthe output name and descriptionnames are unique per family by construction

Outputs

output(name, plot, panel, options) returns a handle a box or segment can name; a bare statement discards it.

  • plot: line, bar, area, histogram, candle, shape, scatter, or none for a data-only output (computed, never drawn: the building block for gates, palettes, and shape coordinates).
  • panel: overlay (the price pane) or lower (its own pane). Put small-magnitude series (oscillators, percentages, counts) in lower; a z-score on the price axis hugs the axis floor.
  • Static style: color, width, opacity (0..1), line_style ("solid", "dashed", "dotted"), glow (a halo in pixels on a line, an area, columns, candles or marks), z (the paint order inside the pane, an integer -10..10), description (documents the output). A colour is "#rrggbb", "#rrggbbaa" or a theme token such as "theme.up" (Styling).
  • Numbers: format (price, %, si, int, 0, 0.0, 0.00, 0.000, usd, auto), decimals (0..8), signed and unit (printed after the value; unbounded on an output, as it always was) shape the legend, the hover card and the axis tag; decimals and signed need format, and a unit prints only beside one (Styling).
  • Per-bar color: color_by: "<data-only output>" plus colors (at least two entries). Each bar's value indexes the palette (floored); a finite value outside the palette takes entry 0, and a non-finite one draws the static color. color_packed_by: "<output>" reads a packed colour the module computes per bar instead (never beside color_by). A shape output takes either ladder too: each mark in its bar's colour.
  • Per-bar width: width_by plus widths (1..10 entries, each 0.5..20), the same ladder for line width. On bar and histogram outputs width and widths are a share of the bar slot (widths entries from 0.05): 1 means the columns touch, 0.5 half the slot, 2 overlap.
  • Gated marks: plot: shape with shape_where: "<gate output>" draws a mark at the output's value only on bars where the gate is nonzero.
  • Axis tags: axis_label: true tags this output's last value on the price axis (absent, only the first mounted output is tagged); axis_name: true adds the indicator's name to that tag (plus the output's label when several outputs are tagged); price_line: true draws a dotted line across the pane at the last value. The three are refused on a data-only output.
  • pane: "<name>" puts the output in a named pane, declared with pane(...) below; absent, a lower output lands in the default pane.
  • displacement_bars (an integer in -500..500) makes the chart draw the series shifted left or right without changing its values; past the loaded edge it extrapolates at the bar spacing. displacement_bars_by: { param: "<name>", scale?: 1, offset?: 0 } reads the shift from a setting instead: round(scale * value + offset) bars, clamped to -500..500, which wins over displacement_bars while the setting is a finite number.

An output cannot color, widen, or gate itself: the palette index and the gate must be different outputs. The rest of the vocabulary is on the Styling page. The legend shows each output's label, or its name read as words when there is none (bb_upper reads "Bb upper"), and its value in the output's format; legend: false keeps an output out of the legend, and legend({ title: "..." }) adds words after the indicator's name in the legend (Legend).

Panes

pane(name, options) declares a pane of the indicator's own, at most four per indicator, and pane: "<name>" on an output puts the output in it. The name "lower" styles the default pane; any other name is a new pane below the chart, stacked in declaration order, the default pane first unless it is declared later. These time-series panes sit at the bottom of the indicator's stack, next to the time axis; the indicator's panels (panel.line, panel.bars and the rest, on Cards, frames and panels) stack above them. A declared pane with nothing drawn in it is refused, and so is a pane beside a declared overlay, with one exception: beside display({ overlay: "offchart" }) the indicator lives in the default pane, so pane("lower", { format: "0.00" }) formats that pane's axis (format, decimals, signed and unit only; any other pane or word is refused). The options, every one optional:

wrun
pane("rsi", { title: "RSI {{period}}", height_frac: 0.2, min: 0, max: 100 });
pane("oi", { place: "price", scale: "log", format: "si" });
output("rsi", line, lower, { pane: "rsi", color: "#7c3aed" });
output("oi", line, overlay, { pane: "oi", color: "#f59e0b" });
  • title (1..40 characters, {{param}} reads a setting): the pane's legend title. An indicator that homes below the chart (no drawn output on the price pane) keeps its own name on the pane holding its first drawn output, so that pane takes no title.
  • place: "below" (the default) or "price". A pane placed on price draws its outputs over the candles on a hidden scale of their own (open interest or a cumulative delta on price); its outputs declare overlay, a pane placed below takes lower outputs, and the pair must match.
  • height_frac (0.05..0.6): the pane's share of the chart height, refused on a pane placed on price. Absent, a pane below an on-price indicator takes 0.22 and an indicator that homes below keeps the chart's default pane size.
  • scale: "linear" (the default), "log", "percent" or "indexed"; invert: true flips the axis; padding: [top, bottom] (each 0..0.4) sets the autoscale margins; min and max hold either end of the autoscale (RSI: min: 0, max: 100, min below max; a log scale refuses min at or below 0). Dragging the axis by hand still works.
  • format, decimals, signed, unit: the pane's axis ticks, the same four words an output takes (decimals, signed and unit need format). Absent, a lower pane prints K, M and B from 1,000 and its own precision below that.

An inverted pane turns a loss into something that hangs: write a drawdown as a positive percent and flip the axis, so zero sits at the top of the pane and the deepest loss reaches lowest, with the running peak drawn over price above it. The pane keeps a title because the indicator homes on the price pane.

wrun
output("peak", line, overlay, { color: "theme.muted", line_style: "dotted" });
pane("dd", { title: "Drawdown", height_frac: 0.18, invert: true, min: 0, padding: [0.02, 0.1], format: "%", decimals: 1 });
output("drawdown", area, lower, { pane: "dd", color: "#ef4444", fill_color: "#ef4444", fill_opacity: 0.25 });

Ranges, fills, boxes, segments and renderers follow the pane of the output they anchor to, and a card, feed or meter with panel: "lower" sits in the indicator's lower pane too.

Lines, areas, columns, dots, marks

A line is a line output; a filled line is area. Columns are bar, and a histogram is columns grown from zero, colored per bar through a color_by ladder over a sign output (value >= 0 ? 1 : 0); base grows the columns from another level instead (colors[0] at or above it, colors[1] below). Dots are scatter. A mark on some bars is a shape output plus a shape_where gate: write the price every bar and let the gate decide which bars draw. A level such as 70 is a guide: a line output with role: "guide" written to 70 on every bar, drawn as one full-width line at its last finite value, kept out of the legend and inside the pane's autoscale (an alert can still follow it). align ("left", "center", "right") puts the output's label on the line, pill_style ("filled", the default, or "outlined") and font_size (6..64) dress that label, and axis_label: true puts the value and the label on the axis instead (never both). A guide takes no color_by, color_packed_by, width_by or price_line.

The looks a line, an area, a column and a mark take beyond colour and width (the words are opt-in; absent, the output draws as it always has):

PlotWordsWhat they draw
linestep: truehorizontal then vertical segments (trailing stops, funding steps); refused beside smooth
line, areasmooth: truethe line (and an area's edge) bent into curves
linegradient: [...]the stroke shaded by height, 2 to 8 colours listed from the top of the pane to the bottom; never beside color_by, color_packed_by or split
linesplit: "level" | "slope", up_color, down_color, split_level?, split_fill?, split_base?"level": up_color above split_level (default 0), down_color below, and split_fill: true shades down to split_base (default split_level); "slope": up_color while rising, down_color while falling; both colours are required with split
areafill_color, fill_opacity, fill_gradient: [...], gradient_modethe fill's own colour (default the line colour) at fill_opacity (0.4 on a flat fill, 1 on gradient stops), or 2 to 8 stops top to bottom laid over "pane" (the default), "fill" or "line"; fill_color and fill_gradient exclude each other
areafill_color_by + fill_colors, or fill_color_packed_bythe fill coloured per bar by its own ladder (name the line's color_by output to follow the line); fill_opacity and opacity multiply each colour
bar, histogramwidth, base, grading: "linear" | "square", stack: "<name>"width as a share of the bar slot; base the level the columns grow from; grading fades each column by its magnitude within the visible range; bars sharing a stack name stack in declaration order, positives above 0 and negatives below (never beside base, and every member on one pane)
shape, scattershape, location, char, font_family, fill, fill_opacityshape picks the mark (circle, the default, cross, triangle_up, triangle_down, diamond, arrow_up, arrow_down, flag, square, char); location is "absolute" (the default), "above_bar", "below_bar", "top" or "bottom"; shape: "char" with char (one character) draws that character in font_family (ui, mono, serif, rounded); fill: false draws the outline only and fill_opacity fades the interior, neither on a char mark
every drawn plotglow, opacity, za halo in pixels; the opacity, which now fades every colour the output draws (ladders, packed colours, a column's sign pair, a candle's colours, marks, gradient stops); the paint order inside the pane

triangle_down points down (apex at the bottom), triangle_up up.

param("fast", 9, { min: 1, max: 200 });
param("slow", 21, { min: 2, max: 400 });
output("fast", line, overlay, { color: "#2563eb", width: 2, description: "Fast average, a line" });
output("slow", area, overlay, { color: "#94a3b8", opacity: 0.3, description: "Slow average, drawn as an area" });
output("cross_mark", shape, overlay, { color: "#16a34a", shape_where: "crossed", description: "A mark on the low of each bullish-cross bar" });
output("crossed", none, overlay, { description: "1 on the bar the fast average crosses above the slow: the mark's gate" });
output("volume", bar, lower, { color: "#4ecdc4", description: "Volume as bars" });
output("delta", histogram, lower, { color_by: "delta_sign", colors: ["#ef5350", "#26a69a"], description: "Fast minus slow as columns, colored by sign" });
output("delta_sign", none, lower, { description: "0 negative, 1 positive: the histogram's palette index" });
output("spread", scatter, lower, { color: "#f97316", description: "Close minus fast, as dots" });
output("rsi", line, lower, { color: "#7c3aed", width: 2, description: "RSI" });
output("overbought", line, lower, { color: "#ff0000", width: 1, line_style: "dashed", description: "The 70 level, written on every bar" });

let fast = new Sma(9);
let slow = new Sma(21);
let rsi = new Rsi(14);
const cross = new Cross();

function onStart(): void {
  fast = new Sma(i32(p_fast()));
  slow = new Sma(i32(p_slow()));
  rsi = new Rsi(14);
}

function onBar(): void {
  const close = bar.close();
  const low = bar.low();
  const volume = bar.volume();
  const fastValue = fast.update(close);
  const slowValue = slow.update(close);
  const rsiValue = rsi.update(close);
  const crossed = cross.update(fastValue, slowValue);
  if (isNaN(slowValue)) return;
  const delta = fastValue - slowValue;
  out_fast(fastValue);
  out_slow(slowValue);
  out_cross_mark(low);
  out_crossed(crossed == 1 ? 1.0 : 0.0);
  out_volume(volume);
  out_delta(delta);
  out_delta_sign(delta >= 0.0 ? 1.0 : 0.0);
  out_spread(close - fastValue);
  out_rsi(rsiValue);
  out_overbought(70.0);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

cross_mark is written on every bar (the bar's low) and drawn only where crossed is 1. The gate is itself an output, so a declared alert can fire on it (Alerts). A two-line fill colored by sign (a momentum fill) is this delta histogram, or a range() between the two lines with a two-entry colors sign palette (Styling).

The other looks in the table, one coherent look per declaration. An area's fill can fade: list the stops top to bottom and lay them over the fill's own extent, so the colour is strongest at the line and gone at the floor; axis_name puts the indicator's name on the axis tag beside the value.

wrun
// Cumulative delta as an area: amber at the line, nothing at the pane floor, the indicator's name on the axis tag.
output("cvd", area, lower, { color: "#f59e0b", width: 2, smooth: true, fill_gradient: ["#f59e0b", "#f59e0b00"], gradient_mode: "fill", fill_opacity: 0.9, axis_label: true, axis_name: true, format: "si" });

When the line already follows a ladder, the fill can follow the same one: name the ladder output again on fill_color_by and give fill_colors a rung per colour, so the edge and the fill switch together.

wrun
// RSI as an area: oversold, neutral and overbought are the three rungs of one ladder, read by the edge and by the fill.
output("rsi", area, lower, { color_by: "zone", colors: ["theme.up", "theme.muted", "theme.down"], fill_color_by: "zone", fill_colors: ["theme.up", "theme.muted", "theme.down"], fill_opacity: 0.3, smooth: true });
output("zone", none, lower, { description: "0 under 30, 1 between, 2 over 70: the index both ladders read" });

A split line colours itself around a level or by slope, and split_fill shades between the line and split_base, so an oscillator reads as a two-tone area that reaches the floor instead of stopping at the split level.

wrun
// A money flow oscillator on a 0..100 pane: green above 50, red below, the shading hanging from the line down to 0.
output("mfi", line, lower, { width: 2, smooth: true, split: "level", split_level: 50, up_color: "theme.up", down_color: "theme.down", split_fill: true, split_base: 0 });

Columns that share a stack name pile up in declaration order, so two volumes become one column per bar, the second share on top of the first; grading fades the small columns.

wrun
// Buy and sell volume piled into one column per bar.
output("buy_volume", bar, lower, { color: "theme.up", width: 0.8, stack: "volume", grading: "linear" });
output("sell_volume", bar, lower, { color: "theme.down", width: 0.8, stack: "volume", grading: "linear" });

A mark can be softened and lit: fill_opacity thins its interior and glow puts a halo around it (render.shape takes the same two words), which keeps a signal visible without hiding the candle under it.

wrun
// A faded diamond with a halo above each signal bar.
output("signal_mark", shape, overlay, { shape: "diamond", shape_where: "fired", location: "above_bar", color: "#f59e0b", fill_opacity: 0.4, glow: 6 });
output("fired", none, overlay, { description: "1 on the bars that fire the signal: the mark's gate" });

A mark can also be a character: shape: "char" with char draws that one character in font_family, here under each bar that crosses a displaced average whose shift follows a setting through displacement_bars_by (Outputs).

wrun
param.int("shift", 5, { min: -50, max: 50, label: "Shift (bars)" });
// A displaced average, and a character mark under each bar that crosses it.
output("dma", line, overlay, { color: "#38bdf8", width: 2, displacement_bars_by: { param: "shift" } });
output("cross_mark", shape, overlay, { shape: "char", char: "✕", font_family: "mono", shape_where: "crossed", location: "below_bar", color: "theme.text" });
output("crossed", none, overlay, { description: "1 on the bar the close crosses the displaced average: the mark's gate" });

Columns can grow from a level other than zero, and a guide's on-line label can be an outlined pill rather than a filled one: RSI as columns from 50, the sign pair colouring them above and below it, with the 70 level labelled on the left.

wrun
// RSI as columns grown from 50, and the 70 level as a guide with an outlined pill.
output("rsi_bars", histogram, lower, { base: 50, colors: ["theme.up", "theme.down"], width: 0.6, grading: "square" });
output("overbought", line, lower, { role: "guide", align: "left", pill_style: "outlined", font_size: 10, color: "theme.muted", line_style: "dashed" });

Candles

An output is one number per bar, so a candle takes four: declare four consecutive candle outputs in the order open, high, low, close, and the chart draws them as one candle, colored by the first output's colors (bullish, then bearish) or its color_by ladder. A candle output that does not start four consecutive candle outputs is refused when the chart draws it ("candle output '' must start four consecutive candle outputs (open, high, low, close)"). A derived candle series (Heikin Ashi, a synthetic bar) works either way. As four candle outputs:

wrun
output("ha_open", candle, overlay, { colors: ["#4caf50", "#f44336"] });
output("ha_high", candle, overlay);
output("ha_low", candle, overlay);
output("ha_close", candle, overlay);

The group's first output carries the candle look, and the other three refuse these words by name: candle_style is "candle" (the default), "hollow" (up candles outlined), "ohlc" (OHLC bars) or "high_low" (high-low bars); border_colors and wick_colors take one colour for both directions or [up, down] (absent, border and wick follow the body colour); border_width is 0..10 (default 1); opacity fades every candle colour, and glow puts a halo on the candles. The legend lists a candle group as one entry reading its open, high, low and close (Legend).

Hollow candles over the chart's own: the first output carries the look, the borders and the wicks take colours of their own, and opacity lets the real bars show through the group.

wrun
// Heikin Ashi drawn hollow: up candles outlined, down candles filled, one grey wick colour for both.
output("ha_open", candle, overlay, { colors: ["theme.up", "theme.down"], candle_style: "hollow", border_colors: ["theme.up", "theme.down"], wick_colors: ["theme.muted"], border_width: 1.5, opacity: 0.85 });
output("ha_high", candle, overlay);
output("ha_low", candle, overlay);
output("ha_close", candle, overlay);

Or as a body box and a wick segment per bar, which draws the candle from the same four numbers and gives each part its own color:

const haOpen = output("ha_open", none, overlay, { description: "Heikin Ashi open" });
const haHigh = output("ha_high", none, overlay, { description: "Heikin Ashi high" });
const haLow = output("ha_low", none, overlay, { description: "Heikin Ashi low" });
const haClose = output("ha_close", none, overlay, { description: "Heikin Ashi close" });
const bullish = output("bullish", none, overlay, { description: "1 when the smoothed close is above the smoothed open" });
const bearish = output("bearish", none, overlay, { description: "1 otherwise" });
// The body: one box per bar between open and close, one declaration per color.
box("body_up", { top: haClose, bottom: haOpen, when: bullish, color: "#4caf50", opacity: 0.9, borderWidth: 0 });
box("body_down", { top: haOpen, bottom: haClose, when: bearish, color: "#f44336", opacity: 0.9, borderWidth: 0 });
// The wick: a vertical segment from the high to the low on the same bar.
segment("wick", { yFrom: haHigh, yTo: haLow, color: "#9ca3af", width: 1 });

let prevOpen: f64 = NaN;
let prevClose: f64 = NaN;

function onBar(): void {
  const o = bar.open();
  const h = bar.high();
  const l = bar.low();
  const c = bar.close();
  const close = (o + h + l + c) / 4.0;
  // The first bar seeds the open from the raw bar; after that it is the previous smoothed midpoint.
  const open = isNaN(prevOpen) ? (o + c) / 2.0 : (prevOpen + prevClose) / 2.0;
  const high = Math.max(h, Math.max(open, close));
  const low = Math.min(l, Math.min(open, close));
  prevOpen = open;
  prevClose = close;
  out_ha_open(open);
  out_ha_high(high);
  out_ha_low(low);
  out_ha_close(close);
  out_bullish(close >= open ? 1.0 : 0.0);
  out_bearish(close < open ? 1.0 : 0.0);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

The four none outputs draw nothing on their own; the chart shows only the boxes and wicks. A data-only output is not offered as an alert condition, so write a drawn output (or declare an alert over the gate) for anything you want to alert on (Alerts). The recursion the sample writes by hand ships as HeikinAshi in ./sdk/ta-plus: update(open, high, low, close) fills the fields open, high, low, close and works over another market's candles too (Extra indicators).

Text, labels, tables, strips, tints

Text does not travel as an output. A string("name", { max_bytes }) declaration adds a byte-capped slot the module writes once per bar in onBar() through its generated senders (nothing to import): build a line with sb_clear(), sb_text("..."), sb_int(n), sb_f64(x, decimals) and send it with str_<name>_sb(), or send a whole string with str_<name>("..."). Renderers then place the text:

RendererDraws
render.text(name, { y, text, color?, size?, style?, align?, valign?, font_weight?, font_family?, size_by?, color_by? + colors? | color_packed_by?, panel? })one text mark per bar whose slot was written, at (bar, y); plain text is placed by align and valign, sized per bar by size_by, coloured per bar by its ladder; a tag style (price_label, pill, callout, badge, box, knockout, emblem) takes the tag words instead: label_position, background_color (or background_color_by + background_colors), border_color, corner_radius, emblem_shape, emblem_color
render.label(name, { x, y, text, color?, size?, style?, align?, valign?, ...look }), or render.label(name, { position, text, offset?, ...look })ONE label at (x, y), x an output in epoch seconds, or pinned to one of the nine anchors by position and nudged by offset ([x, y] px, each -200..200, pointing inward from the anchored edges: a positive x moves a right-anchored label left, a positive y moves a bottom-anchored label up); the newest bar that wrote a nonempty slot (with finite x and y, when placed by them) wins; the look words are background_color, border_color, corner_radius, padding (0..64, default 4), font_weight, font_family, emblem_shape, emblem_color, size_by
render.table(name, { rows, cols, cells, position?, ...look, styles? })a grid of string slots (rows * cols names, row-major); the newest bar where every cell was written wins; the look keys (width, column widths, fills, lines, font, alignment, header rows) and styles (one entry per styled cell) are on Styled tables
render.shape(name, { output, shape, where?, color?, color_by?, colors?, width?, location?, glow?, char?, font_family?, fill?, fill_opacity?, tooltip? })a shaped mark per bar at the output's value where the gate is nonzero, in color, or in the colors entry a color_by output picks per bar; width (px, any positive number; the engine clamps what it paints) is the mark size, location sits it "absolute", "above_bar", "below_bar", "top" or "bottom", glow adds a halo, shape: "char" with char draws one character in font_family, fill: false draws the outline only, fill_opacity fades the interior, tooltip is a template shown on the mark
render.stats_row(name, { output, title?, format?, polarity?, color?, colors?, color_by?, color_packed_by?, priority?, visible? })a row in the statistics strip under the price pane, one cell per bar; colors is [bull, bear] on a diverging row or the ladder color_by indexes; priority 1..3 (default 2) keeps a row's text longest as bars narrow; visible: false draws no row at all
render.bgcolor(name, { where, color?, color_by?, colors?, width?, line_style? })a background tint per bar where the gate is nonzero, static or by ladder; with width (0.5..10 px) a vertical line per gated bar through the pane instead of a tint, dashed by line_style
render.barcolor(name, { where, color?, color_by?, colors? })the bar's own candle (body and wick) tinted where the gate is nonzero, static or by ladder; untinted bars keep the chart's candle colors

size is an integer pixel count, 6..64 (10 reads as small text, 16 as large). A shape's color_by + colors ladder follows the bgcolor rules: the output's value is floored into colors, a finite index outside the palette takes entry 0, a non-finite value draws no mark (the static color never substitutes), and either half without the other is refused. position on a table is one of nine anchors (top_left, top_center, top_right, middle_left, middle_center, middle_right, bottom_left, bottom_center, bottom_right). format and polarity on a stats row ride to the chart verbatim (si, signedSi, percent, price, raw; magnitude, diverging, none). visible on a stats row (default true) takes true, false or an "@<param>" reference to a param.bool: the setting's default draws, the setting switches the row from the settings dialog, and while it is off the row draws nothing at all, not even an empty strip line. One setting may switch several rows. The strip's own look is one sheet key beside its rows, stats_strip: grading (the population each cell's heat is ranked against: rolling, the default, daily, weekly, visible or whole), label_side (left or right, the edge the label gutter sits on) and theme (candle, the chart's candle colours, or teal_rose, viridis, inferno, blue_red, mono). A param.choice paints a key through style_targets ({ "path": ["stats_strip", "grading"] }) and style_values over that key's words, with its default's word written in the key; the key exists only beside a stats row. A row's color bound to a param.color paints the pick, the default included; mark the target "unset_at_default": true and the chart removes the colour while the setting holds its default, so the strip's theme colours the row until the user picks one. Declaring a string slot or a renderer switches the derived sheet to the second runtime contract; the numeric outputs compute exactly as before. style on a text or label renderer picks its look, the same eight words on both: plain (text alone, placed by align and valign), price_label (the price tag), pill (a rounded chip at the value), callout (a leader line to its bar), badge (a dot in the label's color), box (a rounded box, the handle label's default), knockout (a box in the chart's background colour that hides what is behind the text) and emblem (a small mark before the text, its shape emblem_shape: dot, square, diamond, triangle_up, triangle_down). A tag look takes label_position, background_color, border_color and corner_radius; plain text takes align and valign; the two sets never mix on one renderer. font_weight (normal, medium, bold) and font_family (ui, mono, serif, rounded) apply to both. The label styles, the tooltip template and badges are on Labels, tooltips, badges. A corner readout is a render.label with position (below), a one-cell table or a HUD card (HUD and hover cards).

A per-bar readout, a live label, a dashboard, a strip row, a background tint, and a candle tint together:

param("period", 20, { min: 1, max: 200 });
param("stretch", 2, { min: 0.1, max: 20, description: "Percent from the average that counts as stretched" });
output("average", line, overlay, { color: "#38bdf8", width: 2, description: "Simple average" });
output("bar_time", none, overlay, { description: "Bar open in epoch seconds, the label's x" });
output("stretch_pct", none, overlay, { description: "Close distance from the average, percent" });
output("stretched", none, overlay, { description: "1 while the close is stretched from the average" });
output("volume", none, overlay, { description: "Volume, shown in the strip" });
string("readout", { max_bytes: 32 });
string("tag", { max_bytes: 32 });
string("stretch_label", { max_bytes: 16 });
string("stretch_text", { max_bytes: 16 });
// A text mark on every stretched bar (the slot is left unwritten on quiet bars).
render.text("stretch_mark", { y: "average", text: "readout", color: "#f59e0b", size: 10 });
// One label riding the newest bar, at the average.
render.label("average_tag", { x: "bar_time", y: "average", text: "tag", color: "#38bdf8", size: 11 });
// A fixed-position 1x2 dashboard: the newest bar where both cells were written wins.
render.table("stats", { rows: 1, cols: 2, cells: ["stretch_label", "stretch_text"], position: "top_right" });
// Volume as a statistics-strip row.
render.stats_row("volume_row", { output: "volume", title: "Volume", format: "si", polarity: "magnitude" });
// A background tint on stretched bars.
render.bgcolor("stretch_tint", { where: "stretched", color: "#f59e0b22" });
// The stretched bars' own candles, body and wick, in the same amber.
render.barcolor("stretch_candles", { where: "stretched", color: "#f59e0b" });

let sma = new Sma(20);
let threshold: f64 = 2.0;

function onStart(): void {
  sma = new Sma(i32(p_period()));
  threshold = p_stretch();
}

function onBar(): void {
  const close = bar.close();
  const volume = bar.volume();
  const barTime = bar.time();
  const value = sma.update(close);
  if (isNaN(value)) return;
  const stretch = value == 0.0 ? NaN : ((close - value) / value) * 100.0;
  const stretched = Math.abs(stretch) >= threshold;
  out_average(value);
  out_bar_time(barTime);
  out_stretch_pct(stretch);
  out_stretched(stretched ? 1.0 : 0.0);
  out_volume(volume);
  if (stretched) {
    sb_clear();
    sb_f64(stretch, 1);
    sb_text("%");
    str_readout_sb();
  }
  sb_clear();
  sb_text("SMA ");
  sb_f64(value, 2);
  str_tag_sb();
  str_stretch_label("stretch");
  sb_clear();
  sb_f64(stretch, 2);
  sb_text("%");
  str_stretch_text_sb();
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Leaving readout unwritten on quiet bars is the whole gating story for render.text: a slot not written that bar is absent, and an absent slot draws nothing. A price tag on every signal bar is the same renderer with style: "price_label": each written bar's text sits at y as a price tag. A label the module moves, re-words, or deletes on a later bar is a label handle (Drawing objects).

Mark size

width on a render.shape sets the mark's size in pixels, any positive number; left out, the chart draws its own default size. Two sizes of one mark grade a signal at a glance:

// Volume spikes marked under the bar: a large dot over three times the average volume, a small one over twice.
param.int("length", 20, { min: 2, max: 200, label: "Average length" });
output("low", none, overlay, { description: "The bar low, where the marks sit" });
output("big", none, overlay, { description: "1 when volume is over three times its average" });
output("small", none, overlay, { description: "1 when volume is over twice its average but not three times" });
render.shape("big_spike", { output: "low", shape: "circle", where: "big", color: "#f97316", width: 12 });
render.shape("spike", { output: "low", shape: "circle", where: "small", color: "#f97316", width: 6 });

let average = new Sma(20);

function onStart(): void {
  average = new Sma(i32(p_length()));
}

function onBar(): void {
  const volume = bar.volume();
  const mean = average.update(volume);
  if (isNaN(mean) || mean <= 0.0) return;
  const ratio = volume / mean;
  out_low(bar.low());
  out_big(ratio > 3.0 ? 1.0 : 0.0);
  out_small(ratio > 2.0 && ratio <= 3.0 ? 1.0 : 0.0);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine
  • One renderer per size. width is fixed in the declaration, so a large and a small dot are two render.shape lines over the same output, each with its own gate.
  • Gates that never overlap. small is 1 only between two and three times the average, so a bar never carries both marks.

Fixed-position text

Text that sits at a viewport anchor and does not move with price is a render.label pinned by position over a string slot the module rewrites on every bar, so the newest bar decides the text: render.label("status", { position: "top_right", text: "status_text", offset: [12, 8], style: "box", background_color: "theme.bg" }). With position the label needs no x and y (declaring them beside it is refused: a corner label is nudged by offset, not placed by coordinates, and each number of the offset points inward from the anchored edges), and its style is one of plain, box, knockout, pill, badge or emblem (price_label and callout belong to a label at a price). The other corner readouts are a one-cell render.table at that anchor (render.table("status", { rows: 1, cols: 1, cells: ["status_text"], position: "top_right" })) and a render.hud card with a tile.pill or tile.value at the same anchor (HUD and hover cards). A label the module owns and moves is a handle (Drawing objects).

Legend, HUD and hover

Three surfaces read the indicator's newest values and are drawn by the chart from declarations alone: the legend entry, a HUD card at one of the nine anchors, and the card that opens when the cursor rests on a legend entry, a HUD tile or a line. They share one vocabulary of blocks, and the rest of this section documents them: legend({ title }), label, format and render.legend on Legend; hover(handle, [block.*]) and badges on Hover cards; render.hud(name, { position, title?, columns?, tiles, look?, ... }) on HUD cards, with its surface and type words and the twelve looks a card can wear; every block.* and tile.* constructor, the colour, drawing, headline and height words of a tile, on Blocks and tiles; the three together, with a complete module, on What the chart shows.

Ranges and panels

A rectangle between two times and two prices: per bar it is a box between two outputs over a bar-offset span; for one object placed from the newest bar it is draw.box with four coordinate outputs, two of them epoch seconds from the time source; for a rectangle the module keeps, grows, and deletes it is a box handle (Drawing objects).

A heatmap, a curve over a category or an index, tiles, and a one-summary pie are frame-backed panels: panel.heatmap, panel.line over a category or index x, panel.tiles and panel.pie, each a JSON snapshot the module writes and the chart draws in its own pane below the price chart (Cards, frames and panels).

Viewport-pinned panels of raw bars and a variable number of marks per bar have no renderer: every output is one number per bar on the chart's time axis. For a dashboard use render.table over string slots; for a per-level picture use a plot.levels frame docked on the price axis, or the chart's own footprint view beside the wrun indicator.

The name requirement

A wrun indicator's outputs are unique by construction (a duplicate output("sma", ...) is refused in the editor's Console, duplicate output name 'sma'), the name is the legend entry (read as words), and description is the optional long text. Names follow one grammar across outputs, boxes, segments, renderers, and drawings, and share one namespace: a renderer cannot reuse an output's name.

When to use which

You wantUseNot
a value someone could alert ona drawn output, or a declared alert over a gatea box or a label (decorations are never alert targets)
a mark on some bars onlyplot: shape with shape_where, or render.shape with wherea text renderer on every bar
a live readout at the newest barrender.label from a string slotrender.text (one mark per bar)
text on every signal barrender.text from a slot written on those barsone label per signal
a corner readout that does not move with pricea render.label with a position, a one-cell render.table with a position, or a render.hud tilea label placed by x and y
a dashboardrender.hud with typed tiles (and a look), or render.table for a grid of wordsmany labels
a word or a value in the legendlabel and format on the output, render.legend for a slot's wordsa text renderer in the corner
an answer when the cursor rests on a line or a tiletooltip on the output, or a hover block lista table the user has to read across
a label the module moves or deletes latera label handle (draw.label(id))a renderer, which cannot be moved
a per-bar tint behind the barsrender.bgcolorrecoloring a line
trend or regime colored candlesrender.barcolora background tint fighting the candle for contrast
a levela line output with role: "guide" written to the constanta drawing
an oscillator and a second study in panes of their owntwo pane(...) declarations and pane on their outputsone crowded lower pane