ChartSynq

The indicator API, written for an AI

This is the same API as the manual, with the explaining taken out and the completeness put in. It is meant to be handed to an assistant, whole, before you ask for an indicator.

How to use it

  1. Copy the reference below — or give your assistant the plain-text link, which is the whole page and nothing else:
    https://chartsynq.com/manual-ai.php?raw=1
  2. Ask for what you want in your own words. Say what should be measured, which pane it belongs on, and what you want to be able to adjust.
  3. Paste the answer into ChartSynq: + IndMy scripts+ New, then ▶ Test.

Read what Test says before you trade on it. A model can write something that compiles, runs, and is confidently wrong about the maths. Test proves the script runs and that its output lines up with the bars; it cannot tell you the formula is the one you meant. Nobody has checked that but you.

Open the plain text The manual for humans

The reference

49,600 characters
# ChartSynq custom indicators - complete API reference

AUDIENCE: a language model writing a chart indicator on behalf of a ChartSynq
user. This document is the whole contract. If something is not in here, it does
not exist - do not carry a feature over from Pine Script, MQL5, or any other
charting language on the assumption that it is probably supported.

HOW TO USE THIS: read section 10 (RULES) before writing anything. Produce ONE
JavaScript function body. The user pastes it into ChartSynq: + Ind -> My
scripts -> + New -> paste -> Save -> Add to chart.


## 1. EXECUTION MODEL

A script is a FUNCTION BODY, not a file and not a module. The app evaluates it
as, in effect:

    new Function('d', 'ta', 'input', <your code>)

so:
  - Do NOT write a function signature, a wrapper, an export, or a closing brace
    for one.
  - `return` at top level is REQUIRED and is how the result leaves.
  - `const`, `let`, arrow functions, template literals, destructuring, Math,
    Array, Number, JSON, Map, Set, Float64Array all work.

It runs in a Web Worker, once per redraw, with these globals DELETED:
`fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, `importScripts`,
`indexedDB`, `caches`. There is no `document`, no `window`, no `localStorage`,
no `require`, no `import`. A script cannot fetch data, load a library, read the
page, or persist anything between runs (except through the streaming form's
state, section 6).

The result is copied out of the worker FIELD BY NAMED FIELD. Anything not named
in sections 5 and 6 is silently dropped - it is not an error and there is no
warning. Only numbers, strings, booleans and arrays of those survive. Functions,
class instances, Dates and Maps do not.

Every value in a data array is normalised to `number` or `NaN` on the way out.
`null` becomes NaN deliberately (`isFinite(null)` is `true`, so a null used to
reach the renderer and blank the window).


## 2. INPUT: `d` - the bars

Six parallel arrays, equal length, oldest bar first, plus one scalar:

    d.t   number[]  bar OPEN time, seconds since the Unix epoch, UTC
    d.o   number[]  open
    d.h   number[]  high
    d.l   number[]  low
    d.c   number[]  close
    d.v   number[]  TICK volume (count of price changes), not traded size
    d.tf     number  the window's timeframe IN SECONDS - branch on it to tune a
                     value per timeframe (e.g. if (d.tf >= 3600) for H1 and up)
                     60=M1 300=M5 900=M15 1800=M30 3600=H1
                     14400=H4 86400=D1 604800=W1 2592000=MN
    d.tfName string  the same timeframe as its label: 'M5','H1','D1','W1','MN'...
                     (if (d.tfName === 'M15')). d._tf is the old alias, still set.

`d.c.length` is the bar count. The LAST bar is still forming: its close is the
current price, and its high/low can still move.

NOT PROVIDED, and there is no way to get it: the symbol name, the instrument's
point size or digits, the account currency, spread, positions, orders, any
other timeframe, any other symbol, the wall clock, or anything from the server.
If a script needs the point size (for a pip or mpip conversion) it must take it
as an input - see section 3.


## 3. INPUT: `input` - declared settings

Declaring an input makes the app draw a control for it in the indicator's
properties dialog, so the user can retune the script without editing it.

    input.int(label, default, {min, max, step})       -> number, rounded
    input.number(label, default, {min, max, step})    -> number
    input.bool(label, default)                        -> boolean
    input.color(label, default)                       -> string '#rrggbb'
    input.text(label, default)                        -> string (<=200 chars)
    input.select(label, default, [option, ...])       -> string from the list
    input.source(label, default)                      -> string; the list is
        ['open','high','low','close','hl2','hlc3','ohlc4'] - the app validates
        against it, but MAPPING that name to a series is the script's job.

Each call RETURNS THE VALUE TO USE: the saved value if the user has set one,
otherwise the default. Never try to read saved settings any other way; there is
no `params` object in scope.

Rules:
  - The key is derived from the label: lowercased, runs of non-alphanumerics
    become `_`, trimmed, cut to 40 chars. 'EMA length (fast)' -> `ema_length_fast`.
    Changing a label orphans the saved value (it falls back to the default).
  - Declaring the same label twice registers it ONCE; both calls return the
    same value.
  - Every value is clamped/validated before the script sees it: below `min`
    clamps to `min`, above `max` clamps to `max`, a non-number falls back to
    the default, an unlisted select/source option falls back to the default, a
    string that is not `#rrggbb`-shaped falls back to the default. A script
    never needs to defend against a hostile setting.
  - At most 40 inputs; `select` at most 40 options.
  - Declare inputs UNCONDITIONALLY at the top. A declaration inside an `if`
    that did not run this time is an input that disappears from the dialog.


## 4. INPUT: `ta` - the calculation toolbox

All take plain arrays and return plain arrays (or an object of them). Values
are NaN until the calculation has warmed up. Wilder's definitions where Wilder
defined them, so numbers match MT5 / TradingView on the same bars.

### Averages and smoothing

    lib.ema(src, len)                                    -> array
        Exponential moving average. Seeded with the first len values' mean, NaN before that.
    lib.sma(src, len)                                    -> array
        Simple moving average.
    lib.wma(src, len)                                    -> array
        Linearly weighted moving average - the newest bar counts len times, the oldest once.
    lib.dema(src, len)                                   -> array
        Double EMA: 2*ema - ema(ema). Turns faster than an EMA of the same length.
    lib.rma(src, len)                                    -> array
        Wilder's smoothing - an EMA with k = 1/len. ADX and RSI are DEFINED on this; ema(2n-1) is only an approximation of it.

### Oscillators

    lib.rsi(src, len)                                    -> array, 0..100
        Wilder's relative strength index.
    lib.macd(src, fast, slow, signal)                    -> {macd, signal, hist}
        hist is macd minus signal, which is the series you draw as a histogram.
    lib.stoch(h, l, c, kLen, dLen, smooth)               -> {k, d}
        Stochastic oscillator, both lines already smoothed.
    lib.cci(h, l, c, len)                                -> array
        Commodity channel index, on the mean deviation of the typical price.
    lib.willr(h, l, c, len)                              -> array, -100..0
        Williams %R. Note the sign: 0 is the top of the range, -100 the bottom.
    lib.roc(src, len)                                    -> array, percent
        Rate of change over len bars, as a percentage.
    lib.change(src)                                      -> array
        This bar minus the previous one. NaN on the first bar.

### Range, trend and stops

    lib.atr(h, l, c, len)                                -> array
        Average true range, Wilder's - the same number MT5 prints.
    lib.bb(src, len, mult)                               -> {up, mid, lo}
        Bollinger bands: a simple moving average plus and minus mult standard deviations.
    lib.adx(h, l, c, len)                                -> {adx, pdi, ndi}
        Wilder's directional movement. adx measures how much trend there is, not which way.
    lib.psar(h, l, step, maxAf)                          -> {sar, side}
        Parabolic SAR. side is +1 while the stop is below price, -1 while it is above.
    lib.supertrend(h, l, c, atrLen, mult)                -> {line, dir}
        ATR bands that may only tighten while price stays on their side, so the line ratchets. dir is +1 up, -1 down.
    lib.highest(src, len)                                -> array
        Highest value of the last len bars, this one included.
    lib.lowest(src, len)                                 -> array
        Lowest value of the last len bars, this one included.

### Volume

    lib.vwap(t, h, l, c, v, sessionSec)                  -> array
        Volume-weighted average price, reset every sessionSec of BAR TIME (86400 = daily). On FX the weight is tick volume, because brokers publish no real volume.
    lib.obv(c, v)                                        -> array
        On-balance volume.
    lib.mfi(h, l, c, v, len)                             -> array, 0..100
        Money flow index - RSI weighted by volume.
    lib.cmf(h, l, c, v, len)                             -> array, -1..1
        Chaikin money flow.

### Structure: tops, bottoms and levels

    lib.peaks(src, k)                                    -> array of bar INDICES
        Bars higher than the k bars either side. A flat top is reported once, at the bar it began on.
    lib.troughs(src, k)                                  -> array of bar INDICES
        The same, downward.
    lib.joinAt(src, idx)                                 -> array
        A series that runs in straight segments through the given bars and is NaN outside them - so ta.joinAt(r, ta.peaks(r, 3)) is a line drawn across the tops of r.
    lib.pivothigh(src, left, right)                      -> array
        A confirmed pivot high, reported right bars LATE - at the bar where it became certain, which is the earliest a live chart could know it.
    lib.pivotlow(src, left, right)                       -> array
        The same, downward.
    lib.ichimoku(h, l, c, tenkanLen, kijunLen, senkouLen) -> {tenkan, kijun, senkouA, senkouB, chikou}
        Already displaced the way Hosoda defined it: the senkou lines are pushed FORWARD kijun bars, chikou is drawn kijun bars BACK.
    lib.pivots(t, h, l, c)                               -> {p, r1, r2, r3, s1, s2, s3}
        Classic floor pivots from the previous DAY's high, low and close, held flat through the day and NaN at each boundary so consecutive days do not join into a staircase.

There is no other helper. `srcArr`, `nz`, `barssince`, `crossover`,
`security`/`request`, `plot()`, `plotshape()`, `alertcondition()` and anything
else from another language DO NOT EXIST. Write the loop.


## 5. RETURN VALUE - whole-array form

Return ONE object. `plots` must be an array (possibly empty). Everything else
is optional. Unknown fields are dropped.

    return {
      pane:      'sub' | 'chart',   // default 'sub'
      plots:     Plot[],            // 5.1
      hlines:    (number | HLine)[],// 5.3
      labels:    Label[],           // 5.4
      table:     Table,             // 5.5
      corner:    {text, color},     // 5.5 - shorthand for a 1x1 table
      boxes:     Box[],             // 5.6
      lines:     Line[],            // 5.6
      polys:     Poly[],            // 5.6
      rightBars: number,            // 5.6
      orders:    Order[]            // 5.7 - REQUESTS a trade, replay only
    };

`pane: 'chart'` draws on the price chart in PRICE coordinates. Use it only for
series that are prices (moving averages, bands, stops, projected levels).
`pane: 'sub'` gives the indicator its own pane with its own scale. Use it for
everything else. The pane comes from the result itself, so changing it in code
takes effect the next time the script runs.

### 5.1 Plot

    {
      name:   string,      // <=60 chars, shown in the pane's name strip
      data:   number[],    // REQUIRED, one entry per bar, NaN where undefined
      style:  string,      // see 5.2; default 'line'
      color:  string,      // any CSS colour string
      colors: string[],    // per-bar colour; entry i colours the segment
                           //   ENDING at bar i; null falls back to `color`
      width:  number,
      dash:   number[],    // [on, off] pixels, up to 4 entries
      label:  boolean,     // chip this plot's NEWEST value on the scale
      pos:    string,      // 'area'/'hist' only: fill above zero
      neg:    string,      // 'area'/'hist' only: fill below zero
      o: number[], h: number[], l: number[]   // 'candle' only; `data` = close
    }

`data.length` MUST equal `d.c.length`. A shorter array is drawn against the
WRONG BARS - it is not truncated at the end, it is misaligned from bar 0.

### 5.2 Plot styles

    'line'    default. Breaks at NaN rather than dropping to zero.
    'area'    filled between the line and ZERO; `pos` above, `neg` below.
              The fill splits exactly at the zero crossing.
    'hist'    one bar per value, drawn FROM ZERO, coloured by the value's SIGN
              (`pos` / `neg`).
    'histud'  one bar per value, drawn from the BOTTOM of the pane, coloured by
              the PRICE CANDLE at that bar (up = teal, down = red). Takes no
              colour of its own - agreeing with the candles is the point.
    'candle'  OHLC bars. Needs `o`, `h`, `l` alongside `data` (the close).
              Works on both panes. `colors[i]` colours bar i.
    'dots'    one marker per bar. CHART PANE ONLY - in a sub pane the same plot
              is drawn as a line.

### 5.3 HLine - a horizontal reference level

A plain `number`, or:

    { v: number, color: string, width: number, dash: number[],
      label: true | string }

`label: true` prints the value as a chip on the pane's scale; a string prints
that text instead (<=24 chars).

Levels COUNT IN THE PANE'S AUTOMATIC SCALE, which is the only control a script
has over it (see 8).

### 5.4 Label - text pinned to a bar

    { i:     number,   // bar index
      value: number,   // position on the pane's scale
      text:  string,   // <=60 chars
      style: 'up' | 'down' | 'center' | 'circle' | 'none',   // default 'up'
      size:  'tiny' | 'small' | 'normal' | 'large',          // default 'small'
      color: string,       // badge colour
      textColor: string,
      pill:  boolean }     // rounded ends

'up' sits below the point with a pointer upward; 'down' sits above it; 'center'
sits on it; 'circle' is a filled dot; 'none' is bare text with no badge.

There is NO CAP on the number of labels. Thinning them so the chart stays
readable is the script's job. A useful default: label only the last N events,
or only events at least K bars apart.

### 5.5 Table and corner

    table: {
      position: 'top_left'|'top_center'|'top_right'
              | 'middle_left'|'middle_center'|'middle_right'
              | 'bottom_left'|'bottom_center'|'bottom_right',   // default top_right
      fontSize: number,        // clamped 8..20, default 11
      marginX: number, marginY: number,     // default 4
      bg: string | false,      // false / 'none' = no background
      border: string | false,
      cellColor: string,       // default cell colour
      rows: Cell[][]           // <=20 cells per row
    }

    Cell = string | { text: string,        // <=60 chars
                      color: string,
                      bold: boolean }

The pane prints its own name in the top-left corner, so a table at `top_left`
shares that space. `top_right` is the habit.

    corner: { text: string, color: string }

is exactly a 1x1 table at `top_right`, for a one-line readout.

### 5.6 Free drawing - CHART COORDINATES

`x` is a bar index and MAY be fractional and MAY run past the last bar into the
margin `rightBars` reserves. `y` is a value on the pane's own scale (a price on
the chart pane).

    Box  { x1, y1, x2, y2,
           bg: string, border: string, bw: number,
           text: string,               // <=32 chars
           tcolor: string,
           tsize: 'tiny'|'small'|'normal'|'large',
           halign: 'left'|'center'|'right',
           valign: 'top'|'middle'|'bottom' }

    Line { x1, y1, x2, y2, color: string, width: number, dash: number[] }

    Poly { pts: [[x,y], ...] | [{x,y}, ...],
           line: string, fill: string, width: number,
           closed: boolean }         // default true

    rightBars: number                // clamped 0..400

CAPS PER RESULT: 2500 boxes, 2500 lines, 200 polys, 4400 points per poly.
Excess is dropped silently. Primitives are redrawn every frame, so thousands of
them cost real time; prefer a plot when the thing is a time series.

Boxes and lines do NOT participate in the price pane's scale (a drawing that
reaches back to an off-screen price would otherwise squash the candles).


### 5.7 orders - asking for a trade

    Order { act: 'buy' | 'sell' | 'close' | 'closePct',
            i: number,               // the bar it belongs to - REQUIRED
            lots: number,            // buy/sell, default 0.1
            sl: number, tp: number,  // DISTANCES IN PIPS, not prices. 0 = none
            maxOpen: number,         // buy/sell: refuse if this many are open
                                     //   on that side. DEFAULT 1. 0 = no limit
            side: 'buy'|'sell'|'both',  // close/closePct, default 'both'
            pct: number }            // closePct: 0..100 of each position

An order is a REQUEST. The page decides, and by default it declines.

    1. The user arms each indicator by hand, in its properties. It starts OFF.
       A script cannot arm itself and cannot read whether it is armed. An
       installed script from the library therefore arrives inert.
    2. REPLAY ONLY. In live mode nothing is placed at all.
    3. Only an order on the LAST bar of the run is acted on, and only once for
       that bar's time. Your script is re-run over the whole series on every
       redraw, so return the whole history of signals if you like: everything
       but the live edge is ignored. This is what stops a redraw re-entering
       every trade in the history.

The bar's TIME and the `last` flag are stamped on by the sandbox, not by the
script - a script that could set them could replay one entry for ever. An `i`
that is not a real bar index never fires.

CAPS: 20 orders cross per result, 8 are placed per result. Beyond 20 the list
is trimmed to the NEWEST, so the live edge survives the trim.

NOT GUARANTEED: the script runs in a worker on its own schedule while the
replay runs on the page. At speed the worker falls behind and bars go past
unseen, so signals ARE missed; the count of missed bars is shown beside the
switch. A strategy that must not miss a bar belongs in the app's rule engine
(Settings > Rules), which runs inside the bar loop. Opening from a script and
managing or closing from rules is a normal split.


## 6. RETURN VALUE - streaming form

For scripts whose whole-array form is too slow. The engine commits every bar
older than `lag` ONCE and recomputes only the tail, so the cost per new bar is
O(lag) instead of O(bars).

    return { stream: {
      lag: 1,                       // how many trailing bars can still change
      init:     (p) => ({ ... }),   // fresh state, once per run
      newOut:   () => ({ Name: [] }),  // the output arrays step() fills
      step:     (st, d, i, out, labels) => { ... },   // once per bar
      assemble: (st, out, labels, p, d) => ({ /* a section-5 result */ })
    }};

  - `step` MUST push exactly one entry per output array per call, or the series
    stop aligning with the bars.
  - `step` must be a pure function of `st` and bars up to `i`. It is re-run on
    the tail against a COPY of the committed state, so a forming bar changing
    its mind is safe - but only if `step` writes nothing outside `st`/`out`.
  - `assemble` returns the same object shape as section 5.
  - `init` and `assemble` receive `p`, the saved settings object. Prefer
    closing over the `input.*` values declared before the `return`, which is
    what the example in section 11 does.
  - `step` and `assemble` are REQUIRED; the others have defaults.

Do not reach for this by default. The whole-array form is simpler, cannot
drift, and is fast enough for almost everything.


## 7. NOT AVAILABLE - do not generate these

  - `overlay: true` on a plot. Built-in Tick Volume rides the bottom of the
    price chart on a hidden scale; a script cannot. A custom histogram gets its
    own pane.
  - `range: [min, max]` on the result. There is no fixed pane scale for a
    script. Pin the ends with two hlines instead.
  - `fill`, `zone`, `hollow`, `wick` on a plot: built-in-only fields.
  - Network access of any kind, including loading a library.
  - Another symbol, another timeframe, or higher-timeframe aggregation. A
    script sees ONE series: the window's own bars.
  - Trades, orders, positions, balance, account, or anything about the user.
  - Persisting state between runs, writing files, or reading the clock for
    anything but the bar times in `d.t`.
  - Alerts. Alerts are set by the user on the chart, not declared in code.
  - Drawing outside the indicator's own pane.


## 8. SCALING RULES (what the pane's y-axis does)

Sub pane, in order of precedence:
  1. If the user has dragged that pane's axis, their range wins over everything.
  2. Otherwise: the min and max of every plot's VISIBLE values, plus every
     hline's value.
  3. If any plot is 'hist', 'histud' or 'area', zero is forced into the range.
  4. 10% padding is added on both sides.

Only the VISIBLE bars are scanned, so the pane rescales as the chart is
scrolled or zoomed. 'histud' bars are drawn from `max(0, min)` upward.

Chart pane: the candles decide the scale, and chart-pane plot values are
included in it. A chart-pane plot whose values are far from price will
therefore squash the candles - which is the practical reason non-price series
belong in a sub pane.


## 9. ERRORS, LIMITS, PERFORMANCE

  - A thrown error is caught. The pane keeps its space and shows the message
    next to the indicator's name; nothing else on the chart is affected.
  - A script that has not finished in 6 SECONDS is stopped, and that exact
    source is refused until it is edited. This only fires on an endless loop.
  - The editor's Test button compiles the script, runs it on the active
    window's bars, and reports: the syntax-error line where the browser
    supplies one, the runtime error otherwise, or bar count, milliseconds, and
    how many finite values each plot produced. It warns explicitly when a
    plot's length does not match the bar count.
  - Cost model: the script re-runs on every redraw and on every new bar, over
    the WHOLE history (typically thousands of bars, up to tens of thousands).
    An O(bars x window) double loop is the usual reason a script feels heavy.
    Keep running sums, or use the streaming form.
  - Prefer one pre-sized array filled in a single pass over a chain of `map`
    calls that each allocate.


## 10. RULES for generating a script

  1. Output ONE function body. No wrapper function, no markdown fence inside
     the code, no `export`, no comments claiming features that do not exist.
  2. `return` a section-5 object. `plots` must be an array even when empty.
  3. Every `data` array MUST be `d.c.length` long. Build with a pre-sized array
     and an index, or with `.map` over an existing full-length array. NEVER
     build a series with `.filter` or by conditional `push`.
  4. Use `NaN` - never `null`, never `0` - for "no value at this bar".
  5. Guard every read: `if (!isFinite(v)) continue;`. `ta` helpers return NaN
     during warm-up and a bare arithmetic chain turns one NaN into a whole NaN
     series.
  6. Expose the obvious knobs as `input.*` calls at the top, with sane `min`
     values (a length of 0 is a divide-by-zero waiting to happen). Use the
     returned values.
  7. Choose the pane deliberately: `'chart'` only for price-scaled series.
  8. Anything that is not a price and needs a zero line: add `hlines: [0]`.
     Anything bounded (0..100, -100..0, -1..1): pin BOTH ends with hlines.
  9. Do not invent `ta` helpers. If it is not in section 4, write the loop.
 10. Prefer the whole-array form. Reach for `stream` only when asked for
     performance or when the calculation is genuinely recursive over a long
     history.
 11. Say plainly, outside the code, what the indicator measures and what its
     limits are. Never claim it predicts anything or is profitable.


## 11. WORKED EXAMPLES

Every example below is run against the real sandbox by the project's test
suite, so all of them compile and return a valid result on real bars.

### hello-close - The smallest indicator there is
pane: chart

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.

    return {
      pane: 'chart',
      plots: [{ name: 'Close', data: d.c, color: '#5ab0f0', width: 1.4 }]
    };

### hello-osc - The same thing in its own pane
pane: sub

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.

    const roc = lib.roc(d.c, 20);
    return {
      pane: 'sub',
      plots: [{ name: 'ROC 20', data: roc, style: 'area', color: '#5ab0f0' }],
      hlines: [0]
    };

### style-line - style: 'line' - the default
pane: chart

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 ta helper returns NaN during its warm-up instead of a number it has not earned yet.

    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 - style: 'area' - filled to zero
pane: sub

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.

    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 - style: 'hist' - bars from zero
pane: sub

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.

    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 - style: 'histud' - bars coloured by the CANDLE
pane: sub

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.

    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 - style: 'candle' - draw bars, not lines
pane: sub

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.

    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 - style: 'dots' - one marker per bar
pane: chart

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.

    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 }]
    };

### plot-colors - One line, many colours
pane: chart

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.

    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 }]
    };

### plot-label - label: tag the current value on the scale
pane: sub

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.

    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]
    };

### hlines - Reference levels
pane: sub

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.

    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
      ]
    };

### inputs-all - Every kind of input, in one script
pane: sub

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.

    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 };

### timeframe - Know which timeframe you are on
pane: sub

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.

    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' }]
    };

### labels - Labels on bars
pane: chart

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.

    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
    };

### table - A table of numbers
pane: sub

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.
NOTE: 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.

    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]
        ]
      }
    };

### boxes-sessions - Boxes in chart coordinates
pane: chart

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.

    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 };

### profile - A volume profile in the right margin
pane: chart

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.

    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] }]
    };

### stream-ema - Form B: one bar at a time
pane: sub

The streaming form commits every bar older than lag once and only recomputes the tail, so the cost per new bar is O(lag) instead of O(all bars). Use it when the whole-array form starts to feel heavy: long histories, nested loops, or state that carries forward. step() is called once per bar and pushes into out; assemble() turns out into the same result object the simple form returns.

    const len = input.int('Length', 20, { min: 1, max: 500 });
    
    return { stream: {
      lag: 1,                                   // only the forming bar can change
    
      init: (p) => ({ k: 2 / (len + 1), e: NaN, cnt: 0, sum: 0 }),
      newOut: () => ({ Ema: [], Dist: [] }),
    
      step: (st, d, i, out, labels) => {
        const v = d.c[i];
        if (st.cnt < len) {                     // seed on the first len bars
          st.sum += v; st.cnt++;
          if (st.cnt === len) st.e = st.sum / len;
        } else {
          st.e = v * st.k + st.e * (1 - st.k);
        }
        out.Ema.push(st.cnt < len ? NaN : st.e);
        out.Dist.push(st.cnt < len ? NaN : (v - st.e) * 10000);
      },
    
      assemble: (st, out, labels, p, d) => ({
        pane: 'sub',
        plots: [{ name: 'Distance to EMA ' + len + ' (pips)',
                  data: out.Dist, style: 'area',
                  color: '#d7dde5',
                  pos: 'rgba(47,184,164,.28)', neg: 'rgba(240,84,79,.28)' }],
        hlines: [0]
      })
    }};

### sh-ema - EMA
pane: chart

The shipped EMA, whole and unedited. Everything in this chapter is the real code behind a built-in indicator, rewritten only where the built-in reaches something a script cannot (those places are called out).

    const len = input.int('len', 20, { min: 1, max: 1000 });
    const src = input.source('src', 'close');
    const col = input.color('color', '#e8a33d');
    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;
    return { pane: 'chart',
      plots: [{ name: 'EMA' + len, data: lib.ema(s, len), color: col, width: 1.4 }] };

### sh-bb - Bollinger Bands
pane: chart

Three plots off one helper. lib.bb returns {up, mid, lo}.

    const len  = input.int('len', 20, { min: 2, max: 1000 });
    const mult = input.number('mult', 2, { min: 0.1, step: 0.1 });
    const col  = input.color('color', '#5ab0f0');
    const b = lib.bb(d.c, len, mult);
    return { pane: 'chart', plots: [
      { name: 'up',  data: b.up,  color: col, width: 1 },
      { name: 'mid', data: b.mid, color: col, width: 1, dash: [4, 3] },
      { name: 'lo',  data: b.lo,  color: col, width: 1 }
    ]};

### sh-rsi - RSI
pane: sub

The built-in declares range: [0, 100] so its pane never rescales. A script cannot: range is not carried across the sandbox boundary. Two hlines at 0 and 100 do the same job, because levels count in the pane's auto-scale.

    const len = input.int('len', 14, { min: 2, max: 1000 });
    const col = input.color('color', '#c678dd');
    return { pane: 'sub',
      plots: [{ name: 'RSI' + len, data: lib.rsi(d.c, len), color: col, width: 1.3 }],
      hlines: [0, 30, 50, 70, 100] };

### sh-macd - MACD
pane: sub



    const fast = input.int('fast', 12, { min: 1, max: 500 });
    const slow = input.int('slow', 26, { min: 1, max: 500 });
    const sig  = input.int('signal', 9, { min: 1, max: 500 });
    const m = lib.macd(d.c, fast, slow, sig);
    return { pane: 'sub', plots: [
      { name: 'hist', data: m.hist,   style: 'hist' },
      { name: 'macd', data: m.macd,   color: '#5ab0f0', width: 1.2 },
      { name: 'sig',  data: m.signal, color: '#e8a33d', width: 1.2 }
    ], hlines: [0] };

### sh-stoch - Stochastic
pane: sub



    const kLen   = input.int('k', 14, { min: 1, max: 500 });
    const dLen   = input.int('d', 3,  { min: 1, max: 500 });
    const smooth = input.int('smooth', 3, { min: 1, max: 500 });
    const s = lib.stoch(d.h, d.l, d.c, kLen, dLen, smooth);
    return { pane: 'sub', plots: [
      { name: '%K', data: s.k, color: '#5ab0f0', width: 1.2 },
      { name: '%D', data: s.d, color: '#e8a33d', width: 1.2 }
    ], hlines: [0, 20, 80, 100] };

### sh-atr - ATR
pane: sub

lib.atr is Wilder's, via an EMA of 2n-1 - the same number MT5 prints.

    const len = input.int('len', 14, { min: 1, max: 1000 });
    const col = input.color('color', '#e8a33d');
    return { pane: 'sub',
      plots: [{ name: 'ATR' + len, data: lib.atr(d.h, d.l, d.c, len),
                color: col, width: 1.3, label: true }] };

### sh-vwap - VWAP
pane: chart

Session VWAP, resetting at each day boundary of the BAR TIMES. On FX the weight is tick volume, because brokers publish no real volume - which is what MT5 does too.

    const session = input.select('session', 'day', ['day', 'week']);
    const col = input.color('color', '#e8a33d');
    const secs = session === 'week' ? 604800 : 86400;
    return { pane: 'chart',
      plots: [{ name: 'VWAP', data: lib.vwap(d.t, d.h, d.l, d.c, d.v, secs),
                color: col, width: 1.5 }] };

### sh-volume - Tick Volume
pane: sub

The built-in rides the bottom slice of the price chart on a hidden scale (overlay: true). A script cannot: overlay is not carried across the sandbox boundary, so a custom volume gets a pane of its own. Everything else is identical.

    return { pane: 'sub',
      plots: [{ name: 'Vol', data: d.v, style: 'histud' }] };

### sh-range - Candle Range (mpips)
pane: sub

The whole height of the bar in mpips - a tenth of a pip - so a five-pip M1 candle reads 50. The built-in reads the instrument's point size from the feed and takes its threshold per timeframe; a script knows neither, so both become inputs. d._tf is the window's timeframe in seconds, which is how a script can still tell M1 from H1.
NOTE: The threshold is a per-timeframe field on the built-in, because a big candle is not one number: 50 on M1 and 50 on H1 are not the same statement. This preview is H1, where 50 mpips is a very small bar.

    const point  = input.number('Point size', 0.00001, { min: 1e-8 });
    const pipPts = input.int('Points per pip', 10, { min: 1, max: 100 });
    const level  = input.number('Line (mpips)', 50, { min: 0 });
    const col    = input.color('Line colour', '#e8a33d');
    
    const mpip = point * pipPts / 10;        // a tenth of a pip
    const out = d.c.map((_, i) => (d.h[i] - d.l[i]) / mpip);
    
    return { pane: 'sub',
      plots: [{ name: 'Range', data: out, style: 'histud' }],
      hlines: level > 0 ? [{ v: level, color: col, dash: [4, 3], label: true }] : [] };

### sh-supertrend - SuperTrend
pane: chart

The band-carrying rule is what makes it SuperTrend: a band may only tighten while price stays on its side, so the line ratchets. lib.supertrend returns {line, dir}.

    const len  = input.int('atr length', 10, { min: 1, max: 500 });
    const mult = input.number('multiplier', 3, { min: 0.1, step: 0.1 });
    const s = lib.supertrend(d.h, d.l, d.c, len, mult);
    const col = s.dir.map(v => v > 0 ? '#2fb8a4' : '#f0544f');
    return { pane: 'chart',
      plots: [{ name: 'SuperTrend', data: s.line, colors: col,
                color: '#7f8b99', width: 1.6 }] };

### sh-psar - Parabolic SAR
pane: chart



    const step  = input.number('step', 0.02, { min: 0.001, step: 0.001 });
    const maxAf = input.number('max', 0.2, { min: 0.01, step: 0.01 });
    const ps = lib.psar(d.h, d.l, step, maxAf);
    return { pane: 'chart',
      plots: [{ name: 'PSAR', data: ps.sar, style: 'dots',
                colors: ps.side.map(s => s > 0 ? '#2fb8a4' : '#f0544f') }] };

### sh-ichimoku - Ichimoku
pane: chart

Five series, displaced the way Hosoda defined them: the two senkou lines are pushed FORWARD kijun bars, chikou is today's close drawn kijun bars BACK. The cloud floats past the last bar, so ask for the room with rightBars.

    const tk = input.int('tenkan', 9,  { min: 1, max: 500 });
    const kj = input.int('kijun', 26,  { min: 1, max: 500 });
    const sp = input.int('senkou', 52, { min: 1, max: 500 });
    const k = lib.ichimoku(d.h, d.l, d.c, tk, kj, sp);
    return { pane: 'chart', rightBars: kj + 2, plots: [
      { name: 'tenkan',  data: k.tenkan,  color: '#5ab0f0', width: 1.1 },
      { name: 'kijun',   data: k.kijun,   color: '#f0544f', width: 1.1 },
      { name: 'senkouA', data: k.senkouA, color: '#2fb8a4', width: 1 },
      { name: 'senkouB', data: k.senkouB, color: '#e8a33d', width: 1 },
      { name: 'chikou',  data: k.chikou,  color: '#9aa5b1', width: 1, dash: [3, 3] }
    ]};

### sh-pivots - Daily floor pivots
pane: chart

Classic pivots from the PREVIOUS day's high, low and close, held flat for the day. The first bar of each day is deliberately NaN, so consecutive days plot as separate flat segments instead of one staircase.

    const p = lib.pivots(d.t, d.h, d.l, d.c);
    return { pane: 'chart', plots: [
      { name: 'R2', data: p.r2, color: 'rgba(240,84,79,.5)',  width: 1, dash: [3, 3] },
      { name: 'R1', data: p.r1, color: '#f0544f', width: 1 },
      { name: 'P',  data: p.p,  color: '#e8a33d', width: 1.3 },
      { name: 'S1', data: p.s1, color: '#2fb8a4', width: 1 },
      { name: 'S2', data: p.s2, color: 'rgba(47,184,164,.5)', width: 1, dash: [3, 3] }
    ]};

### sh-heikin - Heikin-Ashi
pane: chart

A bar TRANSFORM, not a study: every HA candle sits on exactly the bar it came from, at the same prices, so it lands on the price pane 1:1 with the real candles. Turn the real ones off with the window's candle button and the window IS a Heikin-Ashi chart. What HA is not is a price you can trade: its open and close are averages, so an HA close never happened.

    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++) {
      C[i] = (d.o[i] + d.h[i] + d.l[i] + d.c[i]) / 4;
      O[i] = i === 0 ? (d.o[i] + d.c[i]) / 2 : (O[i - 1] + C[i - 1]) / 2;
      H[i] = Math.max(d.h[i], O[i], C[i]);
      L[i] = Math.min(d.l[i], O[i], C[i]);
      col[i] = C[i] >= O[i] ? '#2fb8a4' : '#f0544f';
    }
    return { pane: 'chart',
      plots: [{ name: 'HA', style: 'candle',
                o: O, h: H, l: L, data: C, colors: col }] };

--- end of reference. Human version with pictures: /manual.php