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
- 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 - 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.
- Paste the answer into ChartSynq: + Ind → My 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.
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