Marks, spans and rays

View as MarkdownOpen the editor

Three declarations draw many small things on each bar, in one batch: plot.marks (bubbles, dots, rings or squares, each at a price with a quantity), plot.spans (boxes, each a price range over a run of bars) and plot.rays (segments, each from one price to another over a run of bars).

A section holds up to 64 records a bar. Each record is written on its own bar and rides that bar's outputs, so the chart draws and hovers a whole section at once: there are no ids to keep, nothing to delete and no live-object cap to stay under. A span or a ray may also run between two times your script knows, starting before the bar that writes it (Timed spans and rays). Anything drawn on most bars belongs here. An object that must move, restyle or vanish on a later bar is a handle instead, and so are a few objects across a chart (a session's box, a broken zone, a swing-to-swing line): a section spends its slots on every bar, record or not, and for tens of objects a chart handles measured cheaper to paint.

wrun
// One mark a bar on the close, sized by the bar's volume: an up bar takes the second colour.
plot.marks({ name: "prints", slots: 1, size: { law: "sqrt", unit: 1000, scale: 5 }, colors: ["theme.down", "theme.up"], tooltip: "{{qty}} at {{price}}" });

function onBar(): void {
  const side = bar.close() >= bar.open() ? 1 : 0;
  out_prints_mark(0, bar.close(), bar.volume(), side, 1, 0);
}

How a section works

plot.marks(options), plot.spans(options) and plot.rays(options) are top-level declarations: an object literal the build reads and removes before the file compiles, like every plot declaration. name and slots are required; slots (1..64, a number written out) is how many records the section holds on one bar. The build generates a record writer for each section, and out_<name>_clear(), which empties every slot:

KindRecord writerA slot holds
marksout_<name>_mark(slot, price, qty, side, fills, offsetMs)the price, the quantity and the integer fields
spansout_<name>_span(slot, top, bottom, lengthBars, style, word, extend)the top, the bottom, the length and the integer fields
raysout_<name>_ray(slot, y1, y2, lengthBars, style, word, extend)the two prices, the length and the integer fields
timed spansout_<name>_span_at(slot, top, bottom, startTs, endTs, style, word, extend)the top, the bottom, the start and end times and the integer fields
timed raysout_<name>_ray_at(slot, y1, y2, startTs, endTs, style, word, extend)the two prices, the start and end times and the integer fields

A spans or rays section is timed when your file calls its _at writer and runs a length of bars when it calls the other one; a section takes one of the two, and calling both is refused by name.

  • Slots. slot runs from 0 to slots - 1; any other slot stops the run with the section's name ("marks 'prints': a slot outside 0..0 was written").
  • Empty slots. A slot not written on a bar is empty and draws nothing. Every bar starts empty, so a bar that writes no record draws none, and a forming bar the chart evaluates again on a live update shows what the last evaluation wrote: four records, then one, leaves one. A file with the four functions (init, state, finalize, reset) calls out_<name>_clear() in finalize() before its writes, since every output there is written on every row.
  • The integer fields. side (0..3) picks a mark's colour, fills (0..65535) is a count the hover readout prints, and offsetMs (0 to 1,073,741,824 ms, about 12.4 days) places a mark inside its bar under x: 1. style (0..7) picks a span's or a ray's colour, word (0..30) picks one of its words (a negative word, or one past the list, draws none), and a nonzero extend runs it to the chart's right edge. lengthBars counts the chart's bars after the record's own (0 is that bar alone; below 0 reads 0). A value past its range is clamped to it.
  • Outputs. Each section adds slots x 3 (marks), slots x 4 (spans and rays) or slots x 5 (timed spans and rays) hidden outputs, named <name>_<slot>_<field>, after every output you declare. They count toward the 256 outputs a file may declare; you never write them one by one.
  • Per bar. A section carries nothing from one bar to the next, so a per-bar script stays per bar with one: on a pan-back the chart computes only the bars it has not drawn.

Marks

plot.marks(options) draws up to 64 marks a bar, each at a price with a quantity. The size follows the quantity, the colour follows the side, and a caption can print the quantity on the larger marks.

wrun
// Up to 8 rings a bar, each placed at its own time inside the bar, the largest captioned.
plot.marks({ name: "trades", slots: 8, size: { law: "sqrt", unit: 10, scale: 5 }, opacity_pct: 30, look: 2, text: { mode: 1, field: "qty", format: "compact", min_px: 22 }, x: 1, colors: ["#F05351", "#27A69A"], tooltip: "{{side}} {{qty}} at {{price}}, {{fills}} fills", panel: "overlay" });
WordValueWhat it does
slots1..64, requiredthe marks one bar holds
size{ law, unit, scale }how big a mark draws (below)
opacity_pct10..80 (default 30)the fill's opacity, in percent
look0..3 (default 0)0 a bubble (a fill with a solid edge), 1 a soft dot (a denser fill, no edge), 2 a ring (a solid edge over a faint fill), 3 a square (a denser filled square)
text{ mode, field, format, min_px }a caption: mode 0 off, 1 on the largest mark of each bar (default), 2 on every mark; field "qty"; format "compact" (default), "price" (the chart's price digits) or "int"; min_px (0..64, default 22) the smallest diameter that takes one
x0 or 1 (default 0)0 the bar's centre, 1 the mark's own time: the bar's open plus offsetMs
colors2..4 colours (default ["theme.down", "theme.up"])side picks one: 0 the first, 1 the second
tooltipa template of at most 200 characters, or null (default null, no readout)the hover readout over a mark: {{side}}, {{qty}}, {{price}}, {{fills}}
panel"overlay"marks draw on the price pane

Size. unit (above 0, default 1) is the quantity a base-size mark holds and scale (1..10, default 5) sets the base: with k = 0.5 + 0.1 x scale, a mark's diameter is 11 x k x sqrt(qty / unit) px, rounded and kept between 11 x k and the smaller of 64 and 50 x k. law: "linear" drops the square root, so the diameter follows the quantity instead of the area.

Order. Larger marks draw first, so a small mark is never hidden under a large one, and each translucent mark composes over the marks under it; captions draw last, over every mark.

Spans

plot.spans(options) draws up to 64 boxes a bar. A box covers a price range from its own bar across lengthBars more of the chart's bars, or on to the right edge with extend.

wrun
// The range of a bar whose body fills most of it, held for 10 bars: an up bar takes the first colour and word.
plot.spans({ name: "zones", slots: 1, colors: ["theme.up", "theme.down"], opacity: 0.15, border: { width: 1, opacity: 0.8 }, words: ["Up bar", "Down bar"], tooltip: "{{word}} {{bottom}} to {{top}} over {{length}} bars", panel: "overlay" });

function onBar(): void {
  if (Math.abs(bar.close() - bar.open()) < 0.7 * (bar.high() - bar.low())) return;
  const pick = bar.close() >= bar.open() ? 0 : 1;
  out_zones_span(0, bar.high(), bar.low(), 10, pick, pick, 0);
}
WordValueWhat it does
slots1..64, requiredthe boxes one bar holds
colors1..8 colours (default ["theme.accent"])style picks one
opacity0..1 (default 0.2)the fill's opacity
border{ width, opacity } (default width 1, opacity 0.8), or null for nonethe edge: width 0..4 px, opacity 0..1
wordsup to 16, each 1..32 charactersword picks one, drawn with the box
tooltipa template of at most 200 characters, or null (default null, no readout)the hover readout over a box: {{top}}, {{bottom}}, {{length}}, {{word}}
panel"overlay"spans draw on the price pane

Rays

plot.rays(options) draws up to 64 segments a bar, each from its own bar at y1 to lengthBars bars later at y2, or on to the right edge with extend. A ray with a length of 0 and extend set is a level from its bar to the edge.

wrun
// A long upper wick's high, carried 20 bars; a long lower wick's low, run to the right edge.
plot.rays({ name: "wicks", slots: 2, colors: ["theme.muted", "#f59e0b"], width: 1.5, line_style: "dashed", opacity: 0.9, words: ["Wick high", "Wick low"], tooltip: "{{word}} at {{y1}}", panel: "overlay" });

function onBar(): void {
  const range = bar.high() - bar.low();
  if (range <= 0.0) return;
  if (bar.high() - Math.max(bar.open(), bar.close()) > 0.5 * range) out_wicks_ray(0, bar.high(), bar.high(), 20, 0, 0, 0);
  if (Math.min(bar.open(), bar.close()) - bar.low() > 0.5 * range) out_wicks_ray(1, bar.low(), bar.low(), 0, 1, 1, 1);
}
WordValueWhat it does
slots1..64, requiredthe segments one bar holds
colors1..8 colours (default ["theme.accent"])style picks one
width0.5..8 px (default 1)the line's width
line_style"solid" (default), "dashed" or "dotted"the line's pattern
opacity0..1 (default 1)the line's opacity
wordsup to 16, each 1..32 charactersword picks one, drawn with the segment
tooltipa template of at most 200 characters, or null (default null, no readout)the hover readout over a segment: {{y1}}, {{y2}}, {{length}}, {{word}}
panel"overlay"rays draw on the price pane

Timed spans and rays

out_<name>_span_at(slot, top, bottom, startTs, endTs, style, word, extend) and out_<name>_ray_at(slot, y1, y2, startTs, endTs, style, word, extend) write a record that runs between two times your script knows: a box from the bar at startTs to the bar at endTs, or a segment from (startTs, y1) to (endTs, y2). The start may lie before the bar that writes the record, so a box over the bars a condition held, written on the bar it ended, is one record on one bar. The rule above still decides: timed records that land on most bars belong here; a session's box, a broken zone or a line between two swings, a few a chart, stay handles.

wrun
// Each run of 10 bars boxed from its first bar's open to its last bar's close, written on the run's last bar.
chart.interval_sec();
plot.spans({ name: "range", slots: 1, colors: ["theme.accent"], opacity: 0.1, tooltip: "{{bottom}} to {{top}}, {{start}} to {{end}}" });

let intervalSec = 0.0;
let firstTs = 0.0;
let hi = 0.0;
let lo = 0.0;
let count = 0;

function onStart(): void {
  intervalSec = p_chart_interval_sec();
}

function onBar(): void {
  if (count == 0) {
    firstTs = bar.time();
    hi = bar.high();
    lo = bar.low();
  }
  hi = Math.max(hi, bar.high());
  lo = Math.min(lo, bar.low());
  count += 1;
  if (count == 10) {
    out_range_span_at(0, hi, lo, firstTs, bar.time() + intervalSec, 0, -1, 0);
    count = 0;
  }
}
  • Times, not bar counts. startTs and endTs are epoch seconds, the unit bar.time() reads, and the chart places both ends at their exact times, the way it places a handle's corners. A count of bars your script keeps is not the chart's count: the chart leaves out the bars it has no data for, a session market's grid skips its closed hours, and volume or range candles have no fixed width, so a box drawn back by a count lands on the wrong bar where a time lands exact. Take both ends from times your script stored (a bar's open is bar.time(), its close that plus chart.interval_sec()), and never convert them to milliseconds.
  • The end. An end not after the start is written as the start: the record covers its start alone. A nonzero extend continues the record past its end to the chart's right edge, a box at its prices and a ray along its slope, so a ray needs a real second point even when it is extended.
  • The integer fields are those of the run-of-bars writers: style picks a colour, word one of words, and a value past its range is clamped to it.
  • The hover readout takes {{start}} and {{end}} in place of {{length}}.
  • A section's layout follows the writer your file calls: a timed section holds slots x 5 outputs, <name>_<slot>_top (or _y1), _bottom (or _y2), _start, _end and _meta, and the sheet says "stride": 5.

A chart built before timed spans and rays reads the run-of-bars layout only and refuses an indicator with a timed section by name ("stride must be 4"), so an indicator that uses one needs the current chart.

Settings

Every look key except slots, panel and text.field takes "@<param>", so a setting drives it (Setting kinds): a colour takes a param.color; a number key a param.number or param.int whose min..max lies inside the key's range; size.law, text.format and line_style a param.choice over their words; and look, text.mode and x a param.choice with at most as many options as the key has, the picked option's position being the value ("Ring" below sets look to 2), or a param.int inside the key's range.

wrun
param.number("min_size", 10, { min: 0.0001, max: 1000000000 });
param.choice("look", ["Bubble", "Soft dot", "Ring", "Square"], "Ring");
param.int("bubble_size", 5, { min: 1, max: 10 });
param.int("opacity", 30, { min: 10, max: 80 });
param.choice("show_size", ["Off", "Big bubbles", "All"], "Big bubbles");
param.choice("position", ["Candle center", "Trade time"], "Trade time");
param.color("buy_color", "#27A69A");
param.color("sell_color", "#F05351");
plot.marks({ name: "trades", slots: 8, size: { unit: "@min_size", scale: "@bubble_size" }, look: "@look", opacity_pct: "@opacity", text: { mode: "@show_size" }, x: "@position", colors: ["@sell_color", "@buy_color"] });

A setting writes one value to every key it drives, so one param.choice cannot set both a position key (look) and a word key (line_style): that is refused by name; declare two settings.

What the chart receives

Run writes each section into the sheet with the two numbers the chart reads it by: first_output, the index of the section's first output, and stride, the fields one slot holds (3 for marks, 4 for spans and rays, 5 for timed spans and rays). Slot i's fields are the outputs whose indexes start at first_output + stride x i. A sheet always carries every look key: Run writes the default for each key a declaration leaves out (a nested key of size, text or border too), and the chart draws what the sheet says. The first example on this page derives:

json
{
  "outputs": [
    { "index": 0, "name": "prints_0_price", "plot": "" },
    { "index": 1, "name": "prints_0_qty", "plot": "" },
    { "index": 2, "name": "prints_0_meta", "plot": "" }
  ],
  "marks": [
    { "name": "prints", "slots": 1, "first_output": 0, "stride": 3, "size": { "law": "sqrt", "unit": 1000, "scale": 5 }, "opacity_pct": 30, "look": 0, "text": { "mode": 1, "field": "qty", "format": "compact", "min_px": 22 }, "x": 0, "colors": ["theme.down", "theme.up"], "tooltip": "{{qty}} at {{price}}", "panel": "overlay" }
  ]
}

A slot's last field packs its integer fields into one number: a mark's side + 4 x fills + 262144 x offsetMs, a span's or a ray's (timed or not) style + 8 x word + 256 x extend. A timed slot's start and end fields hold the two times in epoch seconds. An empty slot is NaN in every field.

What refuses

Every misuse is refused by name when Run derives the sheet:

  • a fifth section of one kind ("marks must declare at most 4 entries"), or slots outside 1..64 ("plot.marks 'm' slots must be an integer from 1 to 64 (the records it holds a bar)");
  • sections that take the file past 256 outputs ("plot.marks 'm2' derives 66 outputs (22 slots x 3 fields), which takes the sheet to 259 outputs; a row carries at most 256, so declare fewer slots or fewer outputs");
  • an output you declare under a derived name ("plot.marks 'm' derives output 'm_0_price', which is already declared; rename one"), and a section named like an output, a canvas or any other named part: names share one namespace;
  • colors outside 2..4 on marks or 1..8 on spans and rays, a colour that is not "#rrggbb", "#rrggbbaa" or a theme token, more than 16 words, and a tooltip over 200 characters or naming a field its kind does not carry ("tooltip names {{word}}, which a marks record does not carry");
  • a setting whose kind or range does not fit its key ("plot.marks 'm' look references "@look", which is not a param.choice of at most 4 options or a param.int within 0..3");
  • both writers of one section in one file ("plot.spans 'z' is written by both out_z_span() (line 5, a run of bars) and out_z_span_at() (line 6, two timestamps); a section holds one record layout, so call one of the two writers"), and a timed section's tooltip naming {{length}}.

Sections draw when the indicator runs in your browser and when it runs on OpenMarket's servers as a Protected indicator: the server keeps the section and sends its rows to your chart, which draws them. A timed section needs a chart that draws stride-5 records (it advertises wrun-batched-v2); an older chart keeps the indicator off its hosted lane by name. The caps are on Limits.