Custom indicators

Write your own indicators in the script editor.

Overview

A CandL indicator is a short JavaScript script. It reads the chart’s candles, calculates with the ta library, and draws with functions such as plot(). Every built-in indicator is written the same way, so any built-in’s script is a working example you can open and read.

Scripts run in a separate, sandboxed worker in your browser. They can’t reach the network, the page or your data, and they’re stored in your browser with the rest of your workspace.

The editor

Open Indicators in the top bar and choose the Custom tab.

  • My scripts lists your saved scripts. New starts one from a simple moving average.
  • Name and On chart / New pane set what the indicator is called and where it draws.
  • Changes preview on the chart as you type. The status line under the code shows what the script draws, or its error.
  • Save keeps the script; Save & add also puts it on the chart. The bin deletes it.

On the chart, a custom indicator works like a built-in: its inputs appear in its settings, and Edit script in its legend row reopens it here.

Editing a built-in

Edit script in a built-in’s legend row opens its script, read-only at first.

  • Edit, then Save: your version replaces the original for every copy of that indicator in this workspace.
  • View diff shows your changes against the original.
  • Save as copy makes a custom script from it and leaves the built-in alone.
  • Revert to original deletes your edits and brings the original back exactly.
  • If a newer original ships after you edited one, CandL keeps running yours and lets you view the difference, revert to the new original, or keep yours.

Script structure

A script is the body of a strict-mode function. Optional header lines at the top describe it; inputs, calculations and drawing calls follow.

//@range 0 100
const length = input.int('length', 'Length', 14, { min: 1 })
const rsi = ta.rsi(close, length)
plot(rsi, { name: 'RSI', color: '#7E57C2' })
hline(70)
hline(30)
HeaderMeaning
//@range 0 100Fixes the pane’s scale to this range (lower then upper).
//@needs volumeThe indicator needs real volume data.
//@name Relative Strength IndexName (up to 80 characters).
//@short RSIShort name for the legend (up to 16 characters).
//@placement paneoverlay (on the price chart) or pane (its own pane).
//@category OscillatorsThe group it’s listed under.

In your own scripts the editor’s Name field and placement buttons set the name and placement, and //@range and //@needs volume apply. Built-in scripts set all of these in the header.

Inputs

Inputs become the indicator’s settings. Each takes a key first: an identifier of up to 40 letters, digits or underscores, unique within the script. Saved charts and templates store settings by key, so keep keys the same when you edit a script. A script can declare up to 64 inputs.

FunctionReturns
input.int(key, title, default, options?)A whole number. options: min, max, step, legend.
input.float(key, title, default, options?)A number. Same options.
input.bool(key, title, default, options?)true or false. Option: legend.
input.source(key, title, default?)A price series: 'close' (default), 'open', 'high', 'low', 'hl2', 'hlc3' or 'ohlc4'.
input.ma(key, title, default?)A moving-average type for ta.ma(): 'SMA' (default), 'EMA', 'SMMA (RMA)', 'WMA' or 'VWMA'.
input.select(key, title, choices, default?, options?)One of 1–32 short strings; the first is the default. Option: legend.

Numbers outside min/max are clamped. legend: false leaves the input out of the indicator’s label in the legend.

Series

Series are arrays with one value per bar, oldest first: close[0] is the oldest bar and close[bar_count - 1] the newest. Unlike some charting languages, an index counts from the start, not back from the current bar.

NameValue
open, high, low, closePrices.
volumeVolume.
buyVolumeTaker buy volume, where the market provides it (otherwise missing).
timeEach bar’s open time, in milliseconds since 1 January 1970 (UTC).
hl2, hlc3, ohlc4(high + low) / 2, (high + low + close) / 3, (open + high + low + close) / 4.
bar_countThe number of bars.

The price columns are Float64Arrays, where a missing value is NaN. Library functions return ordinary arrays, with null where there is no value yet (for example, before a moving average has enough bars). Everything that takes a series accepts either.

The ta library

Every series function returns an array the same length as its input. A missing input gives a missing output, and smoothed averages carry on across gaps rather than restarting. len is a number of bars.

FunctionReturns
ta.sma(src, len)Simple moving average.
ta.ema(src, len)Exponential moving average (α = 2 / (len + 1)), seeded with an SMA.
ta.rma(src, len)Wilder’s moving average (α = 1 / len), as used by RSI and ATR.
ta.wma(src, len)Linearly weighted moving average, newest bar heaviest.
ta.hma(src, len)Hull moving average.
ta.vwma(src, vol, len)Volume-weighted moving average.
ta.dema(src, len)Double exponential moving average.
ta.tema(src, len)Triple exponential moving average.
ta.alma(src, len, offset = 0.85, sigma = 6)Arnaud Legoux moving average.
ta.swma(src)Symmetrically weighted average of 4 bars (weights 1-2-2-1).
ta.linreg(src, len, offset = 0)Linear regression line through the last len values, read offset bars back.
ta.ma(type, src, len, vol?)A moving average by type name, as returned by input.ma(). VWMA uses vol.
ta.stdev(src, len)Standard deviation of the window (population).
ta.dev(src, len)Mean absolute deviation from the window’s average.
ta.median(src, len)Median of the window.
ta.highest(src, len)Highest value in the window.
ta.lowest(src, len)Lowest value in the window.
ta.highestbars(src, len)Offset to the highest value (0 or negative).
ta.lowestbars(src, len)Offset to the lowest value (0 or negative).
ta.sum(src, len)Rolling sum of the window.
ta.cum(src)Running total from the first bar.
ta.change(src, len = 1)The value minus the value len bars earlier.
ta.mom(src, len)Momentum: the value minus the value len bars earlier.
ta.roc(src, len)Rate of change, in percent, against len bars earlier.
ta.rsi(src, len)Relative Strength Index (0–100).
ta.stoch(src, high, low, len)Stochastic: where src sits in the window’s high–low range, 0–100.
ta.cci(src, len)Commodity Channel Index.
ta.cmo(src, len)Chande Momentum Oscillator.
ta.tsi(src, short, long)True Strength Index (−1 to 1).
ta.tr(high, low, close)True range.
ta.atr(high, low, close, len)Average true range.
ta.percentrank(src, len)Percentage of the previous len values at or below the current one.
ta.correlation(a, b, len)Correlation of two series over the window (−1 to 1).
ta.crossover(a, b)true on bars where a crosses above b.
ta.crossunder(a, b)true on bars where a crosses below b.
ta.rising(src, len)true where src rose on each of the last len bars.
ta.falling(src, len)true where src fell on each of the last len bars.
ta.barssince(cond)Bars since cond was last true.
ta.na(value)true if value is missing (not a finite number).
ta.nz(src, replacement = 0)The series with missing values replaced.

Math and missing values

  • math is JavaScript’s Math: math.max, math.abs, math.sqrt, math.log and the rest.
  • na(value) and nz(series, replacement) are the same as ta.na and ta.nz.
  • sma, ema, wma, highest, lowest and change also work without ta., as in early scripts.
  • Standard JavaScript works as usual: Array.from, map, loops and so on.

Plots and fills

Values that are missing are simply not drawn.

FunctionDraws
plot(series, options?)A series. style: 'line' (default), 'stepline', 'circles', 'cross', 'columns' or 'histogram'.
histogram(series, options?)Bars from zero. upColor colors bars at or above zero and downColor those below; both default to color.
fill(series, fillTo, options?)The area between two series. fillOpacity: 0–1 (default 0.15).
hline(value, options?)A horizontal level. dash: 'dashed' (default), 'solid' or 'dotted'.
bgfill(from, to, options?)A shaded band between two levels. opacity: 0–1 (default 0.1).
barcolor(colors, options?)Recolors the candles: one color per bar, null to keep a candle’s own.

Options for plot, histogram and fill:

  • name: shown in the legend and settings (up to 60 characters).
  • color: see Colors. colors: an array with a color per bar.
  • lineWidth: 0.5–8 (default 1.5) for lines, step lines and crosses. lineStyle: 'dashed' or 'dotted' for lines and step lines.
  • offset: shifts the plot by up to 500 bars; a positive offset draws past the last bar.

hline also takes name and color; bgfill takes color.

Markers, lines, labels and boxes

Positions are bar indexes (as in close[i]) and prices. Indexes may run up to 500 bars past either end of the data.

FunctionOptions
plotshape(condition, options?)A marker on every bar where condition is true. location: 'abovebar' (default), 'belowbar' or 'absolute' (at price, a series). Plus the marker options below.
marker(index, options?)position: 'above' (default), 'below' or 'price' (with price). shape, color, text (up to 40 characters), textColor.
line(i1, p1, i2, p2, options?)color, width (0.5–6), dash ('solid', 'dashed', 'dotted'), extend ('none', 'left', 'right', 'both').
label(index, price, text, options?)color, textColor, anchor ('above', 'below', 'center', 'left', 'right'). Text up to 200 characters.
box(i1, p1, i2, p2, options?)border, fill, fillOpacity (0–1), text (up to 80 characters), textColor.

Marker shapes: 'triangleUp', 'triangleDown', 'arrowUp', 'arrowDown', 'circle', 'cross', 'diamond', 'square', 'labelUp', 'labelDown'.

Colors

Any of: hex ('#7E57C2', with an optional alpha like '#7E57C280'), rgb(), rgba(), hsl(), hsla(), or a color name such as 'orange'. Anything else falls back to the default color.

Limits and errors

LimitValue
Time per run2 seconds. A slower script is stopped.
Script length40,000 characters
Inputs64
Plots, histograms and fills64
Levels (hline) and level fills (bgfill)32 each
Markers, lines, labels, boxes5,000, 2,000, 1,000 and 500

Scripts can’t use the network, timers, import(), eval or the page. When a script throws an error or passes something invalid, the editor’s status line shows the message, and the indicator draws nothing until the script is fixed.

Examples

Moving average cross

On the chart. Marks where the fast average crosses the slow one.

const fastLength = input.int('fast', 'Fast length', 9, { min: 1 })
const slowLength = input.int('slow', 'Slow length', 21, { min: 1 })
const type = input.ma('type', 'Average', 'EMA')

const fast = ta.ma(type, close, fastLength, volume)
const slow = ta.ma(type, close, slowLength, volume)

plot(fast, { name: 'Fast', color: '#2962FF' })
plot(slow, { name: 'Slow', color: '#FF6D00' })
plotshape(ta.crossover(fast, slow), { location: 'belowbar', shape: 'triangleUp', color: '#26A69A' })
plotshape(ta.crossunder(fast, slow), { location: 'abovebar', shape: 'triangleDown', color: '#EF5350' })

Oscillator in its own pane

Choose New pane. The scale is fixed from 0 to 100.

//@range 0 100
const length = input.int('length', 'Length', 14, { min: 1 })
const source = input.source('source', 'Source', 'close')

const rsi = ta.rsi(source, length)

plot(rsi, { name: 'RSI', color: '#7E57C2' })
hline(70, { name: 'Overbought', color: '#787B86' })
hline(30, { name: 'Oversold', color: '#787B86' })
bgfill(70, 30, { color: '#7E57C2', opacity: 0.08 })

Bands with a fill

On the chart. An average with bands a set number of standard deviations away.

const length = input.int('length', 'Length', 20, { min: 1 })
const width = input.float('width', 'Width', 2, { min: 0.1, step: 0.1 })

const basis = ta.sma(close, length)
const deviation = ta.stdev(close, length)
const upper = basis.map((value, i) => (value === null || deviation[i] === null ? null : value + width * deviation[i]))
const lower = basis.map((value, i) => (value === null || deviation[i] === null ? null : value - width * deviation[i]))

plot(basis, { name: 'Basis', color: '#FF6D00' })
plot(upper, { name: 'Upper', color: '#2962FF' })
plot(lower, { name: 'Lower', color: '#2962FF' })
fill(upper, lower, { name: 'Band', color: '#2962FF', fillOpacity: 0.1 })

Volume colored by candle

Choose New pane. Green where the candle closed up, red where it closed down.

//@needs volume
const colors = Array.from(close, (value, i) => (value >= open[i] ? '#26A69A' : '#EF5350'))

plot(volume, { name: 'Volume', style: 'columns', colors })