Enchartiv

Writing your own indicator

Enchartiv indicators are JavaScript. You are handed the bars of the window you are on, you work out some numbers, and you say what should be drawn. There is no build step, no plugin to install and nothing to compile: you type, press Test, and it is on the chart.

Every picture on this page is the actual output of the code printed beneath it, produced by running that code through the same sandbox your browser uses. Prices shown are the demo instrument's — synthetic data, nobody's live feed.

1. What this is

An indicator is a function body. Not a file, not a class, not a registration call — just the inside of a function, which is handed three things and must return a result:

the shape of every script
(d, lib, input) => {

    your code here

    return { plots: [ ... ] };
}
  • d — the bars of the window you are on.
  • lib — a toolbox of thirty of the usual calculations.
  • input — how you declare the settings your script can be adjusted by.

You do not write the function line, or the closing brace. The editor holds the body, and the app wraps it. Everything else is ordinary JavaScript: const, for, Math, Array and the rest all work as you would expect.

2. Where your code runs

Your script runs in a Web Worker — a separate thread with no access to the page. It cannot see the document, cookies, your session, your account or your trades, and fetch, XMLHttpRequest, WebSocket, EventSource, importScripts, indexedDB and caches are all removed before your code is reached. An indicator cannot talk to the network, and cannot load a library.

That is deliberate, and it is what makes it safe to run a script somebody else wrote. Custom code used to run on the page with the same privileges as the app itself; an indicator copied off a forum could have read your session or shipped your trade history somewhere. Now the only things that cross back out of the worker are plain numbers and strings, field by named field.

Two consequences worth knowing before you start:

  • Only what is on the whitelist comes back. A result object with a field nobody has named in the app is not an error — the field is simply not there on the other side. Where a built-in indicator can do something a script cannot, this manual says so.
  • It is asynchronous. Drawing does not wait for you. A job is posted, and the previous result keeps being drawn until the new one lands. On a fast script you will never notice; on a slow one the indicator trails the candles by a frame or two rather than freezing them.

3. Writing one, and saving it

In the app, click + Ind on a window and choose the My scripts tab, then + New. You get a name box, a short chip label (what the chart strip shows when the full name is too long), and the code.

Four buttons matter:

  • ▶ Test compiles and runs your script against the bars in the active window and tells you what happened: the line a syntax error is on, the error a crash threw, or how many bars it ran on, how long it took and how many real values each plot produced. A plot shorter than the bar count is called out, because a short series is drawn against the wrong bars rather than stopping early.
  • ⤢ Window opens the editor in its own resizable window — put it on a second screen next to the chart it draws on.
  • Save stores it under its name. Saving under a name that already exists edits that script rather than making a second one with the same name.
  • Add to chart puts it on the window you opened the dialog from.

The 📖 Manual button on the left of that row opens this page in a new tab, so it can sit beside the editor while you write.

A saved script is yours across every window and every layout. Once it declares inputs (chapter 8) the dialog opens on its properties rather than its code, the way a built-in does, and ✎ Edit code takes you back to the source.

4. The bars you are given

d holds six parallel arrays, all the same length, oldest bar first:

everything d carries
d.t   // bar open time, seconds since 1970 (UTC)
d.o   // open
d.h   // high
d.l   // low
d.c   // close
d.v   // tick volume - the number of price changes in the bar,
      //   not traded size. FX brokers publish no real volume.

d._tf // this window's timeframe IN SECONDS: 60 = M1, 3600 = H1,
      //   86400 = D1. How a script can tell one from another.

Bar i is d.o[i], d.h[i], d.l[i], d.c[i]. The last bar is still forming: its close is the current price and its high and low can still move. Everything before it is finished.

Three things are worth saying plainly, because each has cost somebody an afternoon:

  • Your output must be as long as d.c. Entry i of a plot is drawn at bar i. A series that starts at zero length and gets pushed to once per bar comes out right; one built by filter does not, and will be drawn shifted.
  • Use NaN for "no value yet". A moving average has nothing to say before it has warmed up, and NaN is how you say so: a NaN breaks the line rather than dragging it to zero. Never use null or 0 for this.
  • The window's timeframe is the window's business. You are given the bars of whatever timeframe the window is on. A script cannot ask for another one; put it on the window whose bars you want.

The smallest indicator there is

Every script is a function body. It is handed the bars and must return a result object with at least one plot. This one plots the closing price back onto the chart it came from - useless, and the shortest thing that works.

What this code draws: The smallest indicator there is
draws on the price chart
return {
  pane: 'chart',
  plots: [{ name: 'Close', data: d.c, color: '#5ab0f0', width: 1.4 }]
};

The same thing in its own pane

Change pane to 'sub' and the result gets a pane of its own under the candles, with its own scale. Anything that is not a price - a rate of change, a count, a ratio - belongs there.

What this code draws: The same thing in its own pane
draws in its own pane
const roc = lib.roc(d.c, 20);
return {
  pane: 'sub',
  plots: [{ name: 'ROC 20', data: roc, style: 'area', color: '#5ab0f0' }],
  hlines: [0]
};

5. What you hand back

One object. Every field is optional except plots, which must be an array even when it is empty:

the whole result object
return {
  pane:   'sub',        // 'sub' = its own pane (default), 'chart' = on the candles
  plots:  [ ... ],      // the series to draw            - chapter 6
  hlines: [ ... ],      // horizontal reference levels   - chapter 7
  labels: [ ... ],      // text pinned to a bar          - chapter 9
  table:  { ... },      // a small grid of values        - chapter 9
  corner: { text: '' }, // one-line shorthand for a table
  boxes:  [ ... ],      // rectangles in chart coords    - chapter 10
  lines:  [ ... ],      // free line segments
  polys:  [ ... ],      // filled polygons
  rightBars: 20,        // keep this many empty bars past the last one
  orders: [ ... ]       // trades to place                - chapter 13
};

pane: 'chart' draws on the price chart, in price coordinates — use it for anything that is a price: moving averages, bands, stops, levels. pane: 'sub' gives you a pane of your own under the candles with its own scale — use it for anything that is not: oscillators, counts, ratios, spreads.

The pane comes from the result itself, so switching it in code takes effect the next time the script runs — there is nothing else to change.

6. Plots: the six shapes

A plot is an object. name and data are the only required fields:

every field a plot understands
{
  name:   'RSI 14',     // shown in the pane's name strip
  data:   [ ... ],      // one number (or NaN) per bar
  style:  'line',       // line | area | hist | histud | candle | dots
  color:  '#5ab0f0',
  colors: [ ... ],      // OPTIONAL per-bar colour, entry i colours the
                        //   segment ENDING at bar i; color is the fallback
  width:  1.4,
  dash:   [4, 3],       // [on, off] in pixels
  label:  true,         // chip this plot's newest value on the scale
  pos:    'rgba(...)',  // area/hist: the fill above zero
  neg:    'rgba(...)',  // area/hist: the fill below zero
  o: [], h: [], l: []   // candle only - data is then the close
}

style: 'line' - the default

A plot with no style is a line. width sets its thickness, dash makes it dashed ([on, off] in pixels). NaN breaks the line rather than dragging it to zero, which is why every lib helper returns NaN during its warm-up instead of a number it has not earned yet.

What this code draws: style: 'line' - the default
draws on the price chart
const fast = lib.ema(d.c, 20);
const slow = lib.ema(d.c, 50);
return {
  pane: 'chart',
  plots: [
    { name: 'EMA 20', data: fast, color: '#e8a33d', width: 1.4 },
    { name: 'EMA 50', data: slow, color: '#5ab0f0', width: 1.4, dash: [4, 3] }
  ]
};

style: 'area' - filled to zero

An area plot fills between the line and zero, in one colour above and another below. pos and neg name those two fills; the line itself still uses color. The fill splits exactly on the zero crossing, so the shape never bow-ties.

What this code draws: style: 'area' - filled to zero
draws in its own pane
const m = lib.macd(d.c, 12, 26, 9);
return {
  pane: 'sub',
  plots: [{
    name: 'MACD',
    data: m.macd,
    style: 'area',
    color: '#d7dde5',
    pos: 'rgba(47,184,164,.30)',
    neg: 'rgba(240,84,79,.30)'
  }],
  hlines: [0]
};

style: 'hist' - bars from zero

A histogram draws one bar per value, from zero. Its colour comes from the SIGN of the value: pos for zero and above, neg for below.

What this code draws: style: 'hist' - bars from zero
draws in its own pane
const m = lib.macd(d.c, 12, 26, 9);
return {
  pane: 'sub',
  plots: [
    { name: 'Hist', data: m.hist, style: 'hist',
      pos: 'rgba(47,184,164,.75)', neg: 'rgba(240,84,79,.75)' },
    { name: 'MACD',   data: m.macd,   color: '#5ab0f0', width: 1.2 },
    { name: 'Signal', data: m.signal, color: '#e8a33d', width: 1.2 }
  ],
  hlines: [0]
};

style: 'histud' - bars coloured by the CANDLE

The same bars, but the colour comes from the price bar underneath - teal where that candle closed up, red where it closed down - and the bars grow from the bottom of the pane rather than from zero. This is what Tick Volume and Candle Range are drawn with. The colours are fixed: histud takes no colour of its own, because the whole point of it is that it agrees with the candles.

What this code draws: style: 'histud' - bars coloured by the CANDLE
draws in its own pane
const n = d.c.length;
const body = new Array(n);
for (let i = 0; i < n; i++) body[i] = Math.abs(d.c[i] - d.o[i]);

return {
  pane: 'sub',
  plots: [{ name: 'Body', data: body, style: 'histud' }]
};

style: 'candle' - draw bars, not lines

A candle plot carries four series: o, h and l alongside data, which is the close. Use it where the SHAPE carries the meaning and a single line would throw it away. This one is price with its own 50-bar average taken out, so the pane shows how far each bar ran from fair value.

What this code draws: style: 'candle' - draw bars, not lines
draws in its own pane
const base = lib.ema(d.c, 50);
const n = d.c.length;
const O = new Array(n), H = new Array(n), L = new Array(n), C = new Array(n);
const col = new Array(n);
for (let i = 0; i < n; i++) {
  const b = base[i];
  O[i] = d.o[i] - b; H[i] = d.h[i] - b;
  L[i] = d.l[i] - b; C[i] = d.c[i] - b;
  col[i] = d.c[i] >= d.o[i] ? '#2fb8a4' : '#f0544f';
}
return {
  pane: 'sub',
  plots: [{ name: 'Detrended', style: 'candle',
            o: O, h: H, l: L, data: C, colors: col }],
  hlines: [0]
};

style: 'dots' - one marker per bar

Dots are for a series that is a sequence of points rather than a path - Parabolic SAR is the classic. They are sized off the candle width, so they neither vanish zoomed out nor balloon zoomed in. Dots are a CHART-pane style: in a sub pane the same plot comes out as a line.

What this code draws: style: 'dots' - one marker per bar
draws on the price chart
const ps = lib.psar(d.h, d.l, 0.02, 0.2);
const col = ps.side.map(s => s > 0 ? '#2fb8a4' : '#f0544f');
return {
  pane: 'chart',
  plots: [{ name: 'PSAR', data: ps.sar, style: 'dots', colors: col }]
};

One line, many colours

colors is an array as long as data: entry i colours the segment that ENDS at bar i, and color is the fallback wherever it is null. Before this existed the only way to colour a line by state was to draw it twice with each half NaN-ed out.

What this code draws: One line, many colours
draws on the price chart
const len = 34;
const ma = lib.ema(d.c, len);
const col = ma.map((v, i) => {
  if (!isFinite(v) || i === 0 || !isFinite(ma[i - 1])) return null;
  return v >= ma[i - 1] ? '#2fb8a4' : '#f0544f';
});
return {
  pane: 'chart',
  plots: [{ name: 'EMA ' + len, data: ma, colors: col,
            color: '#7f8b99', width: 2 }]
};

label: tag the current value on the scale

label: true on a plot prints its NEWEST value as a chip on the pane's own scale, in the plot's colour. Newest means strictly the last bar: if that value is NaN nothing is chipped, rather than a stale number being presented as today's.

What this code draws: label: tag the current value on the scale
draws in its own pane
const r = lib.rsi(d.c, 14);
return {
  pane: 'sub',
  plots: [{ name: 'RSI 14', data: r, color: '#c678dd', width: 1.4,
            label: true }],
  hlines: [30, 50, 70]
};

Two footnotes to the list. A plot with no style is a line — that is the sixth, not a seventh. And dots only draws as dots on the price chart: in a pane of its own the same plot comes out as a line.

7. Levels

A level is a horizontal line across the pane. They are the cheapest thing in the language and the most useful: a zero line turns a signed series into something you can read at a glance.

Reference levels

hlines takes plain numbers, or objects for control: {v, color, dash, width, label}. label: true prints the value on the scale, label: 'text' prints your own words. Levels are counted in the pane's auto-scale, which is the one lever you have over it - a custom script cannot set a fixed range, so pinning 0 and 100 with two hlines is how an oscillator keeps a steady scale.

What this code draws: Reference levels
draws in its own pane
const r = lib.rsi(d.c, 14);
return {
  pane: 'sub',
  plots: [{ name: 'RSI 14', data: r, color: '#c678dd', width: 1.4 }],
  hlines: [
    0,                                                  // pins the scale
    { v: 30, color: '#2fb8a4', dash: [3, 4] },
    { v: 50, color: '#39434f' },
    { v: 70, color: '#f0544f', dash: [3, 4], label: 'hot' },
    100                                                 // pins the scale
  ]
};

Levels are counted in the pane's automatic scale, and for a custom script that is the only handle on it there is. A built-in indicator can declare a fixed range — RSI's 0 to 100 — but that setting is not one of the fields that crosses out of the sandbox. Two levels at the ends of the range you want do the same job. (Whoever is looking at the chart can always drag the pane's own axis, and that beats everything.)

8. Inputs, and the properties dialog

Declaring an input is what turns a piece of code into a real indicator: the properties dialog grows a control for it, and it can be retuned from the chart chip without opening the editor at all.

the seven kinds
input.int('Length', 20, { min: 1, max: 500 })      // whole numbers
input.number('Mult', 2, { min: 0.1, step: 0.1 })   // any number
input.bool('Show band', true)
input.color('Line', '#5ab0f0')
input.text('Note', '')
input.select('Mode', 'fast', ['fast', 'slow'])
input.source('Source', 'close')  // open|high|low|close|hl2|hlc3|ohlc4

Each call returns the value to use — the saved one if there is one, the default otherwise. So you use the return value and never read the saved settings yourself.

Three rules that are not obvious:

  • The label makes the key. "EMA length (fast)" becomes ema_length_fast. Rename a label and the old saved value no longer matches it — the input falls back to its default.
  • Declare the same label twice and it is offered once. Two boxes writing to one key is not a form anyone can use.
  • Every value is clamped before you see it. Saved settings live in a layout the user owns, and a length of -1 or "banana" reaching your loop would be your crash and their confusion. Below the minimum clamps to the minimum; nonsense falls back to the default; a colour that is not a colour, or an option that is not on your list, does the same.

Every kind of input, in one script

Declaring an input is what turns your code into a normal indicator: the properties dialog grows a control for it, and it can be retuned from the chip without opening the editor. Each call returns the saved value, or the default when nothing is saved - so you use the return value and never read params yourself.

What this code draws: Every kind of input, in one script
draws in its own pane
const len   = input.int('Length', 20, { min: 1, max: 500 });
const mult  = input.number('Band width', 2, { min: 0.1, step: 0.1 });
const band  = input.bool('Show band', true);
const line  = input.color('Line', '#5ab0f0');
const src   = input.source('Source', 'close');       // open/high/low/close/hl2/hlc3/ohlc4
const mode  = input.select('Basis', 'ema', ['ema', 'sma']);
const note  = input.text('Note', '');

const s = src === 'open' ? d.o : src === 'high' ? d.h
        : src === 'low'  ? d.l : src === 'hl2'  ? d.h.map((v, i) => (v + d.l[i]) / 2)
        : src === 'hlc3' ? d.h.map((v, i) => (v + d.l[i] + d.c[i]) / 3)
        : d.c;

const basis = mode === 'sma' ? lib.sma(s, len) : lib.ema(s, len);
const a = lib.atr(d.h, d.l, d.c, len);

const plots = [{ name: 'Basis', data: basis, color: line, width: 1.4 }];
if (band) {
  plots.push({ name: 'Up', data: basis.map((v, i) => v + mult * a[i]),
               color: line, width: 1, dash: [3, 4] });
  plots.push({ name: 'Dn', data: basis.map((v, i) => v - mult * a[i]),
               color: line, width: 1, dash: [3, 4] });
}
return { pane: 'sub', plots: plots,
         corner: note ? { text: note, color: '#7f8b99' } : null };

Inputs are discovered by running your script, not by reading it — it runs every time anyway, so a declaration is just a function call that reports itself. That is also why a script that has not run yet has no properties to show, and—the reason the mechanism is a function call at all: a script whose source is never shown can still declare what it can be tuned by.

A script is also told which timeframe it is running on, so it can tune itself — a trigger that fits M5 is wrong on the hourly. d.tf is the seconds, d.tfName the label; compare the name for an exact match, or the seconds for a range.

Know which timeframe you are on

The script is told its timeframe: d.tf in SECONDS (60 M1, 300 M5, 900 M15, 1800 M30, 3600 H1, 14400 H4, 86400 D1, 604800 W1, 2592000 MN) and d.tfName as the label ('M5', 'H1', 'D1', 'W1', 'MN'...). Branch on either to tune a value per timeframe - trigger levels that make sense on M5 are wrong on the hourly. Compare the name for an exact match, or the seconds for a range.

draws in its own pane
let trigger;
if      (d.tfName === 'M5')  trigger = 0.00143;
else if (d.tfName === 'M15') trigger = 0.00235;
else if (d.tf >= 3600)       trigger = 0.005;   // H1 and higher
else                         trigger = 0.001;
return {
  pane: 'sub',
  plots: [{ name: 'trigger (' + d.tfName + ')',
           data: d.c.map(() => trigger), color: '#e8a33d' }]
};

9. Labels, tables and readouts

A label is text pinned to a bar and a value, so it travels with the chart:

a label
labels: [{
  i: 240,               // bar index
  value: 1.0954,        // where on the pane's scale
  text: 'up',
  style: 'up',          // up | down | center | circle | none
  size: 'small',        // tiny | small | normal | large
  color: '#2fb8a4',     // the badge
  textColor: '#08130f',
  pill: true            // rounded ends
}]

Labels on bars

A label is pinned to a bar index and a value: {i, value, text}. style picks the shape - 'up' sits under the bar with a pointer up, 'down' above it, 'center' on it, 'circle' is a dot and 'none' is bare text. There is no cap on how many you draw, so thin them out yourself if the chart is to stay readable.

What this code draws: Labels on bars
draws on the price chart
const fast = lib.ema(d.c, 12), slow = lib.ema(d.c, 34);
const labels = [];
for (let i = 1; i < d.c.length; i++) {
  const now = fast[i] - slow[i], was = fast[i - 1] - slow[i - 1];
  if (!isFinite(now) || !isFinite(was)) continue;
  if (was <= 0 && now > 0)
    labels.push({ i: i, value: d.l[i], text: 'up',
                  style: 'up', color: '#2fb8a4', textColor: '#08130f' });
  if (was >= 0 && now < 0)
    labels.push({ i: i, value: d.h[i], text: 'dn',
                  style: 'down', color: '#f0544f', textColor: '#1a0908' });
}
return {
  pane: 'chart',
  plots: [
    { name: 'EMA 12', data: fast, color: '#e8a33d' },
    { name: 'EMA 34', data: slow, color: '#5ab0f0' }
  ],
  labels: labels
};

A table of numbers

table draws a small grid anywhere in the pane. rows is an array of arrays; a cell is a string, or {text, color, bold} when it needs to carry a state. position takes top/middle/bottom crossed with left/center/right. corner: {text} is the one-line shorthand for the same machinery.

What this code draws: A table of numbers
draws in its own pane
const n = d.c.length - 1;
const r = lib.rsi(d.c, 14);
const a = lib.atr(d.h, d.l, d.c, 14);
const adx = lib.adx(d.h, d.l, d.c, 14);
const f = (v, k) => isFinite(v) ? v.toFixed(k) : '-';
const state = adx.adx[n] > 25 ? { text: 'TRENDING', color: '#2fb8a4', bold: true }
                              : { text: 'chop', color: '#e8a33d' };
return {
  pane: 'sub',
  plots: [{ name: 'ADX 14', data: adx.adx, color: '#c678dd', width: 1.4 }],
  hlines: [{ v: 25, color: '#39434f', dash: [3, 4] }],
  table: {
    position: 'top_right',
    fontSize: 11,
    rows: [
      ['RSI',   { text: f(r[n], 1),      color: r[n] > 70 ? '#f0544f' : '#c7d0da' }],
      ['ATR',   f(a[n], 5)],
      ['ADX',   f(adx.adx[n], 1)],
      ['State', state]
    ]
  }
};

The pane prints its own name in the top-left corner, so a table put there shares the space with it. top_right is the habit for that reason.

There is no cap on the number of labels — dropping labels a script meant to draw would be a lie about the chart. Thinning them out so the chart stays readable is your job, not the renderer's.

10. Free drawing

Boxes, lines and polygons are drawn in chart coordinates: x is a bar index and y is a value on the pane's scale. They move with the chart rather than sitting on the glass, and x may run past the last bar into the margin rightBars asks for.

the three primitives
boxes: [{ x1: 10, y1: 1.09, x2: 40, y2: 1.10,
          bg: 'rgba(91,142,214,.15)', border: '#5b8ed6', bw: 1,
          text: '31p', tcolor: '#8b97a6', tsize: 'tiny',
          halign: 'center', valign: 'top' }],

lines: [{ x1: 10, y1: 1.09, x2: 60, y2: 1.11,
          color: '#e8a33d', width: 1, dash: [4, 4] }],

polys: [{ pts: [[10, 1.09], [40, 1.10], [40, 1.08]],
          fill: 'rgba(47,184,164,.2)', line: '#2fb8a4',
          width: 1, closed: true }],

rightBars: 26   // 0-400 empty bars kept past the last candle

Boxes in chart coordinates

boxes, lines and polys are drawn in CHART coordinates: x is a bar index and y is a price, so they move with the chart instead of sitting on the glass. This one marks each day's range and labels its height. Boxes and lines are capped at 2500 each per result.

What this code draws: Boxes in chart coordinates
draws on the price chart
const boxes = [];
let start = 0, hi = -Infinity, lo = Infinity;
const dayOf = i => Math.floor(d.t[i] / 86400);

const flush = (end) => {
  if (end <= start || !isFinite(hi)) return;
  const up = d.c[end] >= d.o[start];
  boxes.push({
    x1: start, y1: lo, x2: end, y2: hi,
    bg: up ? 'rgba(47,184,164,.07)' : 'rgba(240,84,79,.07)',
    border: up ? 'rgba(47,184,164,.45)' : 'rgba(240,84,79,.45)',
    text: ((hi - lo) * 10000).toFixed(0) + 'p',
    tcolor: '#8b97a6', tsize: 'tiny', valign: 'top'
  });
};

for (let i = 0; i < d.c.length; i++) {
  if (i && dayOf(i) !== dayOf(i - 1)) {
    flush(i - 1);
    start = i; hi = -Infinity; lo = Infinity;
  }
  if (d.h[i] > hi) hi = d.h[i];
  if (d.l[i] < lo) lo = d.l[i];
}
flush(d.c.length - 1);

return { pane: 'chart', plots: [], boxes: boxes };

A volume profile in the right margin

rightBars asks the chart to keep that many empty bars past the last one, and x may run into them - which is what makes a profile, a forecast cone or a projection possible at all. This is the shape of script the drawing primitives were added for: no time series could express it.

What this code draws: A volume profile in the right margin
draws on the price chart
const bins  = input.int('Bins', 24, { min: 4, max: 120 });
const width = input.int('Width in bars', 26, { min: 4, max: 200 });
const look  = input.int('Lookback', 240, { min: 20, max: 2000 });

const n = d.c.length, from = Math.max(0, n - look);
let hi = -Infinity, lo = Infinity;
for (let i = from; i < n; i++) { if (d.h[i] > hi) hi = d.h[i]; if (d.l[i] < lo) lo = d.l[i]; }
const step = (hi - lo) / bins;
if (!isFinite(step) || step <= 0) return { pane: 'chart', plots: [] };

const vol = new Array(bins).fill(0);
for (let i = from; i < n; i++) {
  const k = Math.min(bins - 1, Math.max(0, Math.floor((d.c[i] - lo) / step)));
  vol[k] += isFinite(d.v[i]) ? d.v[i] : 1;
}
let peak = 0, at = 0;
for (let k = 0; k < bins; k++) if (vol[k] > peak) { peak = vol[k]; at = k; }

const boxes = [];
for (let k = 0; k < bins; k++) {
  const w = peak ? (vol[k] / peak) * width : 0;
  if (w < 0.5) continue;
  boxes.push({ x1: n - 1, y1: lo + k * step, x2: n - 1 + w, y2: lo + (k + 1) * step,
               bg: k === at ? 'rgba(232,163,61,.45)' : 'rgba(91,142,214,.28)' });
}
return {
  pane: 'chart',
  plots: [],
  rightBars: width + 2,
  boxes: boxes,
  lines: [{ x1: from, y1: lo + (at + 0.5) * step, x2: n - 1 + width,
            y2: lo + (at + 0.5) * step, color: '#e8a33d', dash: [4, 4] }]
};

The caps are per result: 2500 boxes, 2500 lines, 200 polygons of up to 4400 points each, and rightBars is held between 0 and 400. They are there so a runaway script cannot flood the renderer — if you are near any of them, the thing you are drawing probably wants to be a plot.

11. The lib toolbox

Thirty calculations, all written the textbook way — Wilder's originals where he defined them — so a number here matches MT5 or TradingView on the same bars. They take plain arrays, so you can feed them anything, including each other's output.

{"ok":false,"error":"Something went wrong. Please try again."}