An Indicator draws by declaring, not by calling. In kScript (legacy) a
plotLine(...) call ran on every bar and handed the chart a styled
series; an Indicator declares output(name, plot, panel, options) once at
the top of the file and writes one number per bar, and the host draws
that number as a line, a column, a mark, or a tint from the declaration.
That is why a chart can restyle without recompiling, why every host draws
the same thing, and why every drawn series is also a metric an alert can
read. This page maps every kScript plot function to its Indicator form,
and compiles the forms that exist.
The map
| kScript | Indicator | Notes |
|---|---|---|
plot(value, plotType=...) | output(name, line | bar | scatter | candle, panel) | the plot kind is the declaration, not a runtime argument |
plotLine(value, ...) | output(name, line, panel, { color, width, line_style, opacity }) | fill=true is area; smooth and glow have no form |
plotBar(value, ...) | output(name, bar, panel) | one value per bar |
plotHistogram(value, base, colorIndex) | output(name, histogram, panel, { color_by, colors }) | columns grow from zero; a sign ladder colors them |
plotCandle(value) | four outputs, or a box and a segment per bar | no four-output candle binding; see below |
plotShape(value, shape) | output(name, shape, panel, { shape_where }) or render.shape | render.shape picks the mark from nine shapes |
plotBatches(count, fn), plotPie(...) | not in Indicators yet | one thing per bar per renderer |
plotText(text, price) | render.text(name, { y, text, color, size }) | one mark per bar whose slot was written |
plotPriceLabel(price, text) | a text renderer with "style": "price_label" in the sheet | a price tag per bar whose slot was written |
plotLabel(text, position) | a label renderer with "position" in the sheet (the nine corners) | fixed position; the newest bar that wrote the slot wins |
plotTable(data, position) | render.table(name, { rows, cols, cells, position }) | cells are string slots |
plotRange(t1, p1, t2, p2) | box(...) per bar, draw.box for one object, or a box handle | Drawing objects |
plotMiniChartGrid(...) | not in Indicators yet | no viewport-pinned panels |
plotStatRow(value, title, format) | render.stats_row(name, { output, title, format, polarity }) | a strip row over an output |
plotMatrix, plotCurve, plotTiles | not in Indicators yet | Foreign-domain panes |
hline(value) | an output written to the constant every bar | or a segment from: 0, to: 1 |
plotBgColor(color) | render.bgcolor(name, { where, color, color_by, colors }) | a tint per bar where the gate is nonzero |
fillBetween(a, b, color) | a box per bar between the two outputs | fills exists in hand-written sheets; the chart does not draw it yet |
label=[...], desc=[...] | the output name and description | names 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, ornonefor a data-only output (computed, never drawn: the building block for gates, palettes, and shape coordinates).panel:overlay(the price pane) orlower(its own pane). Put small-magnitude series (oscillators, percentages, counts) inlower; a z-score on the price axis hugs the axis floor.- Static style:
color,width,opacity(0..1),line_style("solid","dashed","dotted"),unit(price,%, or a short axis label),description(the legend text). - Per-bar color:
color_by: "<data-only output>"pluscolors(at least two entries). Each bar's value indexes the palette (floored); a missing or out-of-range value falls back to entry 0. - Per-bar width:
width_bypluswidths(1..10 entries, each 0.5..20), the same ladder for line width. - Gated marks:
plot: shapewithshape_where: "<gate output>"draws a mark at the output's value only on bars where the gate is nonzero. displacement_bars(an integer in -500..500) draws the series shifted left or right without changing its values.
An output cannot color, widen, or gate itself: the palette index and the gate must be different outputs. The rest of the vocabulary, including what a hand-written sheet can add, is on the Styling page.
Lines, areas, columns, dots, marks
plot() and plotLine() become a line output; fill=true becomes
area. plotBar() becomes bar and plotHistogram() becomes
histogram: columns grown from zero, colored per bar through a color_by
ladder over a sign output (the colorIndex = value >= 0 ? 0 : 1 idiom).
kScript's base argument moved the baseline; an Indicator's histogram
grows from zero, so a series read around another level is emitted as its
distance from that level. plot(plotType="point") becomes scatter.
plotShape() with na on quiet bars becomes a shape output plus a
shape_where gate: write the price every bar and let the gate decide
which bars draw. hline(70) is an output written to 70 on every bar.
import { area, bar, histogram, input, line, lower, none, ohlcv, output, overlay, param, scatter, shape } from "./sdk/declare";
import { in_close, in_low, in_volume } from "./gen/inputs";
import {
emitRow,
out_cross_mark,
out_crossed,
out_delta,
out_delta_sign,
out_fast,
out_overbought,
out_rsi,
out_slow,
out_spread,
out_volume,
} from "./gen/outputs";
import { p_fast, p_slow } from "./gen/params";
import { Cross, Rsi, Sma } from "./sdk/ta";
param("fast", 9, { min: 1, max: 200 });
param("slow", 21, { min: 2, max: 400 });
input("close", ohlcv.close);
input("low", ohlcv.low);
input("volume", ohlcv.volume);
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();
let fastValue: f64 = NaN;
let slowValue: f64 = NaN;
let rsiValue: f64 = NaN;
let close: f64 = NaN;
let low: f64 = NaN;
let volume: f64 = NaN;
let crossed: i32 = 0;
export function init(): void {
fast = new Sma(i32(p_fast()));
slow = new Sma(i32(p_slow()));
rsi = new Rsi(14);
}
export function state(): i32 {
close = in_close();
low = in_low();
volume = in_volume();
fastValue = fast.update(close);
slowValue = slow.update(close);
rsiValue = rsi.update(close);
crossed = cross.update(fastValue, slowValue);
return isNaN(slowValue) ? 0 : 1;
}
export function finalize(): void {
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);
emitRow();
}
export function reset(): void {
fast.reset();
slow.reset();
rsi.reset();
cross.reset();
fastValue = NaN;
slowValue = NaN;
rsiValue = NaN;
close = NaN;
low = NaN;
volume = NaN;
crossed = 0;
}cross_mark is written on every bar (the bar's low) and drawn only where
crossed is 1; the value on the other bars is still a metric row, which
is what lets an alert watch the gate itself. kScript's momentum_fill
(a two-line fill colored by sign) is this delta histogram, or two none
outputs and a box per bar between them (Drawing primitives).
Candles
candle is a plot kind the sheet accepts, but an output is still one
number per bar: there is no declaration binding four outputs into an OHLC
candle. A derived candle series (Heikin Ashi, a synthetic bar) ports as
four outputs, or as a body box and a wick segment per bar, which draws a
real candle from the four numbers you compute:
import { box, input, none, ohlcv, output, overlay, segment } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_bearish, out_bullish, out_ha_close, out_ha_high, out_ha_low, out_ha_open } from "./gen/outputs";
input("close", ohlcv.close);
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
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;
let open: f64 = NaN;
let high: f64 = NaN;
let low: f64 = NaN;
let close: f64 = NaN;
export function init(): void {}
export function state(): i32 {
const o = in_open();
const h = in_high();
const l = in_low();
const c = in_close();
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.
open = isNaN(prevOpen) ? (o + c) / 2.0 : (prevOpen + prevClose) / 2.0;
high = Math.max(h, Math.max(open, close));
low = Math.min(l, Math.min(open, close));
prevOpen = open;
prevClose = close;
return 1;
}
export function finalize(): void {
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);
emitRow();
}
export function reset(): void {
prevOpen = NaN;
prevClose = NaN;
open = NaN;
high = NaN;
low = NaN;
close = NaN;
}The four none outputs are still metrics (.../ha_close is alertable);
the chart shows only the boxes and wicks.
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 ready bar
in finalize() through generated senders in ./gen/strings: 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:
| Renderer | Draws | kScript counterpart |
|---|---|---|
render.text(name, { y, text, color?, size? }) | one text mark per bar whose slot was written, at (bar, y); with "style": "price_label" in the sheet, drawn as a price tag | plotText, plotPriceLabel |
render.label(name, { x, y, text, color?, size? }) | ONE label at (x, y), x an output in epoch seconds, or at a corner when the sheet gives it a "position"; the newest bar that wrote a nonempty slot wins | plotPriceLabel on the live bar, plotLabel at a corner |
render.table(name, { rows, cols, cells, position? }) | a grid of string slots (rows * cols names, row-major); the newest bar where every cell was written wins | plotTable; a 1x1 table is plotLabel |
render.shape(name, { output, shape, where? }) | a shaped mark per bar at the output's value where the gate is nonzero | plotShape with a named shape |
render.stats_row(name, { output, title?, format?, polarity? }) | a row in the statistics strip under the price pane, one cell per bar | plotStatRow |
render.bgcolor(name, { where, color?, color_by?, colors? }) | a background tint per bar where the gate is nonzero, static or by ladder | plotBgColor, barcolor |
size is an integer pixel count, 6..64 (kScript's "small" / "large"
tiers are 10 and 16). position on a table or a label 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). Declaring a string slot or a renderer switches the
derived sheet to the second runtime contract; a label's position and a
text renderer's style are fields of a hand-written sheet under the third
("abi_version": "wrun-3"), since the declaration options do not take
them yet. The numeric outputs compute exactly as before under either.
A per-bar readout, a live price tag, a dashboard, a strip row, and a regime tint together:
import { input, line, none, ohlcv, output, overlay, param, render, string, time } from "./sdk/declare";
import { in_bar_t, in_close, in_volume } from "./gen/inputs";
import { emitRow, out_average, out_bar_time, out_stretched, out_stretch_pct, out_volume } from "./gen/outputs";
import { p_period, p_stretch } from "./gen/params";
import {
sb_clear,
sb_f64,
sb_text,
str_readout_sb,
str_stretch_label,
str_stretch_text_sb,
str_tag_sb,
} from "./gen/strings";
import { Sma } from "./sdk/ta";
param("period", 20, { min: 1, max: 200 });
param("stretch", 2, { min: 0.1, max: 20, description: "Percent from the average that counts as stretched" });
input("close", ohlcv.close);
input("volume", ohlcv.volume);
input("bar_t", time.bar_open_sec);
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 price tag riding the newest bar.
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" });
let sma = new Sma(20);
let threshold: f64 = 2.0;
let value: f64 = NaN;
let close: f64 = NaN;
let volume: f64 = NaN;
let barTime: f64 = NaN;
export function init(): void {
sma = new Sma(i32(p_period()));
threshold = p_stretch();
}
export function state(): i32 {
close = in_close();
volume = in_volume();
barTime = in_bar_t();
value = sma.update(close);
return isNaN(value) ? 0 : 1;
}
export function finalize(): void {
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();
emitRow();
}
export function reset(): void {
sma.reset();
value = NaN;
close = NaN;
volume = NaN;
barTime = NaN;
}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. plotPriceLabel on every signal bar is the same renderer
with the tag look: a text renderer whose sheet entry carries "style": "price_label" draws each written bar's text as a price tag at y
instead of a bare mark. A label the module moves, re-words, or deletes on
a later bar is a label handle (Drawing objects).
Fixed-position text
plotLabel(text, position="top_right") sat at a viewport anchor and did
not move with price. The Indicator form is a label renderer with a
"position" instead of x and y: it sits at that corner, and the
newest bar that wrote its slot decides the text, so a slot rewritten
every bar is a live readout. A one-cell render.table at the same anchor
is the older form and still works. Both fields are sheet fields today, so
this Indicator is written sheet first: a corner readout of the average,
plus a price tag on the bars where the close is stretched from it.
{
"id": "fn-corner-label",
"name": "Corner label and price tags",
"abi_version": "wrun-3",
"params": [{ "name": "period", "default": 20, "min": 1, "max": 200 }],
"inputSources": { "close": { "source": "ohlcv", "field": "close" } },
"inputs": [{ "index": 0, "name": "close" }],
"outputs": [{ "index": 0, "name": "average", "plot": "line", "panel": "overlay", "color": "#38bdf8" }],
"string_slots": [
{ "index": 0, "name": "status", "max_bytes": 32 },
{ "index": 1, "name": "tag", "max_bytes": 16 }
],
"renderers": [
{ "kind": "label", "name": "status_corner", "text": "status", "position": "top_right", "size": 12 },
{ "kind": "text", "name": "stretch_tag", "y": "average", "text": "tag", "style": "price_label", "color": "#f59e0b", "size": 10 }
]
}import { in_close } from "./gen/inputs";
import { emitRow, out_average } from "./gen/outputs";
import { p_period } from "./gen/params";
import { sb_clear, sb_f64, sb_text, str_status_sb, str_tag_sb } from "./gen/strings";
import { Sma } from "./sdk/ta";
let sma = new Sma(20);
let value: f64 = NaN;
let close: f64 = NaN;
export function init(): void {
sma = new Sma(i32(p_period()));
}
export function state(): i32 {
close = in_close();
value = sma.update(close);
return isNaN(value) ? 0 : 1;
}
export function finalize(): void {
out_average(value);
// The corner label: rewritten on every bar, so the newest bar's text is what shows.
sb_clear();
sb_text("SMA ");
sb_f64(value, 2);
str_status_sb();
// A price tag on stretched bars only: the slot stays unwritten on quiet bars.
const stretch = value == 0.0 ? 0.0 : ((close - value) / value) * 100.0;
if (Math.abs(stretch) >= 2.0) {
sb_clear();
sb_f64(stretch, 1);
sb_text("%");
str_tag_sb();
}
emitRow();
}
export function reset(): void {
sma.reset();
value = NaN;
close = NaN;
}render.label with x and y is the other kind of label, anchored to a
bar and a price, for a tag that rides the live edge; a label the module
owns and moves is a handle.
Ranges and mini charts
plotRange(time1, price1, time2, price2) drew one rectangle from stored
timestamps. Per bar that 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).
Not in Indicators yet. plotMiniChartGrid (viewport-pinned panels of
raw bars), plotBatches and plotPie (a variable number of marks per
bar), and the foreign-domain panes (plotMatrix, plotCurve,
plotTiles) have no renderer: every output is one number per bar on the
chart's time axis. The nearest form for a dashboard is render.table
over string slots; for a per-level picture it is the chart's own
footprint view beside the Indicator.
The name requirement
Every kScript plot needed a unique label; an Indicator's outputs are
unique by construction (a duplicate output("sma", ...) is a named build
error, duplicate output name 'sma'), the name is the legend entry and
the metric id (wrun/@you/name/sma), 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 want | Use | Not |
|---|---|---|
| a value someone could alert on | an output | a box or a label (decorations are never metrics) |
| a mark on some bars only | plot: shape with shape_where, or render.shape with where | a text renderer on every bar |
| a live readout or a price tag | render.label from a string slot | render.text (one mark per bar) |
| a price tag on every signal bar | render.text with the sheet's style: "price_label" | one label per signal |
| a corner readout that does not move with price | render.label with the sheet's position | a table with one cell |
| a dashboard | render.table | many labels |
| a label the module moves or deletes later | a label handle (draw.label(id)) | a renderer, which cannot be moved |
| a per-bar tint | render.bgcolor | recoloring a line |
| a level | an output written to the constant | a drawing |