Charts
Indicators
The built-in library, their inputs, and how to register your own in 20 lines.
Read this page as Markdown, or take the whole documentation in one file.
Adding One
const ema = chart.addIndicator("ema", { length: 21 });
const rsi = chart.addIndicator("rsi"); // its own pane
const macd = chart.addIndicator("macd", { fast: 12, slow: 26, signal: 9 });
An indicator declares its own inputs, outputs, colors and pane, so the second argument is optional and any input you leave out takes its default. Outputs become ordinary series, so everything on Series types applies to them: they appear in the legend, they autoscale, they can be moved to another scale or pane, and they are hidden and shown like any other series.
The instance you get back is the handle for changing and removing it:
ema.setInputs({ length: 50 }); // recomputes and relabels
ema.setVisible(false);
ema.remove();
ema.id; // instance id
ema.def; // the definition: name, inputs, outputs, pane, levels
ema.paneId; // where it was placed
ema.series; // Map<outputKey, SeriesModel>
ema.inputs; // resolved inputs, defaults filled in
The Built-In Library
There are 51 as of version 0.2.0: 18 that draw on the price pane and 33 that take a pane of
their own. That is comparable to Highcharts Stock, which ships about 50, and short of
TradingView Advanced Charts, which ships roughly 110, where the long tail is mostly further
variations on what is here. Anything missing is a registerIndicator call, and the engine —
inputs, multiple outputs, bands, levels, own panes, incremental recompute — has no
privileged path for the built-ins.
The tables below are generated from the registry. listIndicators() is the same list at
runtime, and it is the one to read if you build your own menu.
Overlays on the Price Pane
| Id | Name | Inputs | Outputs |
|---|---|---|---|
sma |
Moving Average | length (20), source |
sma |
ema |
Exponential Moving Average | length (20), source |
ema |
wma |
Weighted Moving Average | length (20), source |
wma |
vwma |
Volume Weighted Moving Average | length (20), source |
vwma |
hma |
Hull Moving Average | length (9), source |
hma |
dema |
Double Exponential Moving Average | length (20), source |
dema |
tema |
Triple Exponential Moving Average | length (20), source |
tema |
bb |
Bollinger Bands | length (20), mult (2), source |
upper, basis, lower |
keltner |
Keltner Channels | length (20), mult (2), atrLength (10), source |
upper, basis, lower |
donchian |
Donchian Channels | length (20) |
upper, basis, lower |
envelope |
Envelope | length (20), percent (2.5), source |
upper, basis, lower |
atrbands |
ATR Bands | length (14), mult (2), source |
upper, lower |
linreg |
Linear Regression Channel | length (100), mult (2), source |
upper, basis, lower |
ichimoku |
Ichimoku Cloud | conversion (9), base (26), spanB (52), displacement (26) |
conversion, base, spanA, spanB, lagging |
psar |
Parabolic SAR | step (0.02), max (0.2) |
sar |
supertrend |
Supertrend | length (10), mult (3) |
supertrend |
pivots |
Pivot Points | period (day, week, month), method (classic, fibonacci) |
r3, r2, r1, p, s1, s2, s3 |
vwap |
VWAP | anchor (session, week, month, all) |
vwap |
Indicators in Their Own Pane
| Id | Name | Inputs | Outputs | Levels |
|---|---|---|---|---|
rsi |
Relative Strength Index | length (14), source |
rsi |
30, 70 |
stochrsi |
Stochastic RSI | rsiLength (14), stochLength (14), k (3), d (3), source |
k, d |
20, 80 |
macd |
MACD | fast (12), slow (26), signal (9), source |
hist, macd, signal |
0 |
ppo |
Percentage Price Oscillator | fast (12), slow (26), signal (9), source |
hist, ppo, signal |
0 |
stoch |
Stochastic | k (14), smooth (3), d (3) |
k, d |
20, 80 |
willr |
Williams %R | length (14) |
r |
−80, −20 |
cci |
Commodity Channel Index | length (20) |
cci |
−100, 100 |
cmo |
Chande Momentum Oscillator | length (9), source |
cmo |
−50, 50 |
roc |
Rate of Change | length (9), source |
roc |
0 |
mom |
Momentum | length (10), source |
mom |
0 |
trix |
TRIX | length (15), source |
trix |
0 |
dpo |
Detrended Price Oscillator | length (21), source |
dpo |
0 |
ao |
Awesome Oscillator | fast (5), slow (34) |
ao |
0 |
uo |
Ultimate Oscillator | fast (7), middle (14), slow (28) |
uo |
30, 70 |
adx |
Average Directional Index | length (14), smooth (14) |
adx, plus, minus |
25 |
aroon |
Aroon | length (14) |
up, down |
30, 70 |
vortex |
Vortex Indicator | length (14) |
plus, minus |
1 |
volume |
Volume | ma (20) |
volume, ma |
— |
rvol |
Relative Volume | length (50) |
rvol |
1 |
cumvol |
Cumulative Volume | length (20) |
sum |
— |
obv |
On Balance Volume | — | obv |
— |
ad |
Accumulation/Distribution | — | ad |
— |
pvt |
Price Volume Trend | — | pvt |
— |
mfi |
Money Flow Index | length (14) |
mfi |
20, 80 |
cmf |
Chaikin Money Flow | length (20) |
cmf |
0 |
chaikinosc |
Chaikin Oscillator | fast (3), slow (10) |
osc |
0 |
eom |
Ease of Movement | length (14), divisor (10000) |
eom |
0 |
atr |
Average True Range | length (14) |
atr |
— |
stdev |
Standard Deviation | length (20), source |
stdev |
— |
hv |
Historical Volatility | length (10), annual (365) |
hv |
— |
bbw |
Bollinger Bandwidth | length (20), mult (2), source |
bbw |
— |
delta |
Volume Delta | method (auto, aggressor, estimate) |
delta |
0 |
cvd |
Cumulative Volume Delta | method (auto, aggressor, estimate) |
cvd |
— |
delta, cvd and rvol are covered in detail on Order flow; they read
the aggressor volumes on the bars when the feed supplies them and say so in the tooltip when
they had to estimate instead.
The source input accepts close, open, high, low, hl2, hlc3 and ohlc4.
Enumerate the registry at runtime rather than hard-coding the list:
import { listIndicators, getIndicator } from "@ortex-charts/financial";
for (const def of listIndicators()) {
console.log(def.id, def.name, def.pane, def.inputs.map((i) => i.key));
}
getIndicator("rsi").levels; // [30, 70]@ortex-charts/financialPlacement and Color
chart.addIndicator("rsi", { length: 14 }, {
pane: "separate", // "overlay", "separate", or an existing pane id
paneHeightRatio: 0.25,
colors: { rsi: "#A78BFA" },
seriesOptions: { lineWidth: 1, lastValueVisible: false },
sourceSeriesId: comparisonSeries.id,
});
paneoverrides where the definition wanted to go. Passing an existing pane id puts two indicators in one pane, which is how RSI and Stochastic end up together.colorsoverrides output colors by output key; unset, each output takes the color its definition derives from the theme, so a theme switch recolors it.seriesOptionsis applied to every output series.sourceSeriesIdcomputes the indicator from another bar series instead of the main one, which is how you put an RSI of a comparison symbol on the chart.
How Live Recomputation Works
Indicators recompute whenever the series they read from changes. On a live tick, a full pass
over ten years of minute bars for every indicator would be the dominant cost, so an
indicator may declare a lookback: the number of bars its last value depends on, to the
precision that matters on screen.
When lookback is set, a tick recomputes only the last max(500, lookback) bars and
patches the final value. This is verified against a full recompute to within 1e-6 in the
test suite. Indicators that carry state from the beginning of the data — obv, cvd,
vwap, cumvol — leave lookback unset and recompute fully, which is correct and slower.
The practical consequences:
- A moving average, RSI or MACD on 100,000 bars costs the same per tick as on 1,000.
- Cumulative indicators are the ones to watch on very long histories with a fast feed.
- Changing an input forces a full recompute, which is what you want.
Writing Your Own
An indicator is one object. compute receives the aligned main-series columns and returns
one Float64Array per output key, and everything else — the legend label, the settings
dialog, the pane, the level lines, the band fill, the layout entry — follows from the
declaration.
import { registerIndicator, sourceColumn, type IndicatorDef } from "@ortex-charts/financial";
import { rollingMean, rollingStd } from "@ortex-charts/math";
const zscore: IndicatorDef = {
id: "zscore",
name: "Z-Score",
short: "Z",
pane: "separate",
levels: [-2, 0, 2],
lookback: (inputs) => 10 * Number(inputs.length),
inputs: [
{ key: "length", name: "Length", type: "number", default: 50, min: 2, max: 2000, step: 1 },
{ key: "source", name: "Source", type: "select", default: "close", options: ["close", "open", "high", "low", "hl2", "hlc3", "ohlc4"] },
],
outputs: [{ key: "z", title: "Z", plot: "line", color: (theme) => theme.palette[2] }],
compute({ columns, inputs }) {
const src = sourceColumn(columns, inputs.source);
const length = Number(inputs.length);
const mean = rollingMean(src, length);
const sd = rollingStd(src, length);
const z = new Float64Array(src.length);
for (let i = 0; i < src.length; i++) z[i] = sd[i] > 0 ? (src[i] - mean[i]) / sd[i] : NaN;
return { z };
},
};
registerIndicator(zscore);
chart.addIndicator("zscore", { length: 100 });
Registering it also puts it in the toolbar's indicator menu and lets it survive in a saved layout, because both read the same registry.
The Definition Fields
| Field | Meaning |
|---|---|
id |
Registry key, and what a layout stores. |
name |
Full name in the menu and the settings dialog. |
short |
Legend label prefix, for example EMA. Inputs are appended automatically. |
pane |
"overlay" for the price pane, "separate" for its own. |
levels |
Constant value lines drawn in a separate pane, such as 30 and 70 for RSI. |
lookback |
Bars the last value depends on, a number or a function of the inputs. Unset means full recompute. |
priceFormat |
{ type, precision } for the axis and legend of the outputs. |
inputs |
Declarations the settings dialog is generated from. |
outputs |
One per returned column. |
compute |
The calculation. |
Input Declarations
{ key: "length", name: "Length", type: "number", default: 20, min: 1, max: 2000, step: 1 }
{ key: "source", name: "Source", type: "select", default: "close", options: ["close", "hl2"] }
{ key: "showBands", name: "Show bands", type: "boolean", default: true }
The four types are number, select, boolean and source. The shell builds the settings
dialog from these declarations, so an indicator you register gets the same dialog the
built-ins have.
Output Declarations
{ key: "hist", title: "Histogram", plot: "histogram", signColors: true }
{ key: "upper", title: "Upper", plot: "band", bandWith: "lower", color: (t) => t.palette[5], lineWidth: 1 }
{ key: "vol", title: "Volume", plot: "histogram", mainDirectionColors: true }
{ key: "sum", title: "Sum", plot: "area", color: (t) => t.palette[4] }
plot is line, histogram, area or band. A band output pairs with another output
named in bandWith and the space between them is filled, which is how Bollinger Bands and
Donchian Channels get their shading from two ordinary line series. signColors colors a
histogram by sign; mainDirectionColors colors it by the main series candle direction.
color is a function of the theme rather than a string, so the indicator recolors itself
when the theme changes rather than being repainted in a color that no longer fits.
The Compute Context
compute(ctx) {
ctx.columns; // aligned main-series columns: time, open, high, low, close, volume,
// plus buyVolume and sellVolume when the feed supplied them
ctx.length; // number of bars
ctx.inputs; // resolved inputs
ctx.timeZone; // the chart time zone, for session-anchored calculations such as VWAP
return { key: Float64Array };
}
Return arrays of ctx.length. Use NaN for bars where the indicator has no value, which is
normally the warm-up period; the renderer treats NaN as a gap and the legend shows nothing
rather than a number.
@ortex-charts/math carries the primitives most indicators need: rollingSum,
rollingMean, rollingStd, rollingMin, rollingMax, ema, wilderSmooth, pctChange,
logReturns, cumulativeReturn and diff. They are all typed-array in, typed-array out.
What Is Not Here
- There is no scripting language. No Pine Script, no formula editor, no sandbox. Indicators are TypeScript or JavaScript, which is more capable and less safe: an indicator is code you ship, not code your users write. If letting end users author indicators matters to your product, this library does not solve it for you.
- The library is 51 indicators deep, not 110. Ichimoku, Supertrend, ADX, Parabolic SAR, Keltner Channels, Money Flow Index, Williams %R, Aroon and TRIX all arrived in version 0.2.0, but the far end of the Advanced Charts list is not here, and some of it never will be.
- Indicators of indicators are not declarative. Computing an RSI of a moving average
means doing both calculations inside one
compute.
The Extras Package
The long tail lives in @ortex-charts/indicators-extra, a package of its own so that the
chart you ship only carries the studies your users open: 78 more, 30 overlays and 54 in
their own pane, which with the built-in 51 is 129, more than TradingView Advanced Charts'
catalog of roughly 110. A build that imports it pays about 11 KB gzip; one that does not
pays nothing.
import { registerExtraIndicators } from "@ortex-charts/indicators-extra";
registerExtraIndicators(); // once, before the first chart
chart.addIndicator("kama", { length: 10 });
Overlays: Arnaud Legoux MA, Kaufman adaptive MA, Tillson T3, zero-lag EMA, smoothed MA, least-squares MA, McGinley Dynamic, triangular MA, sine-weighted MA, VIDYA, FRAMA, moving-average cross, moving-average ribbon, Guppy multiple MA, Williams Alligator, Chandelier exit, Chande Kroll stop, standard error bands, zig zag, median price, typical price, weighted close, Camarilla, Woodie and Fibonacci pivot points, VWAP bands, anchored VWAP, rolling VWAP.
Own pane: Connors RSI, Laguerre RSI, Fisher transform, Schaff trend cycle, Know Sure Thing, Special K, Coppock curve, relative vigor index, relative volatility index, inertia, balance of power, choppiness index, mass index, Elder Ray, force index, accelerator oscillator, gator oscillator, market facilitation index, true strength index, stochastic momentum index, absolute price oscillator, percentage volume oscillator, Klinger volume oscillator, volume oscillator, volume zone and price zone oscillators, negative and positive volume index, Aroon oscillator, Chaikin volatility, ulcer index, Bollinger %B, Qstick, Williams VIX Fix, Woodies CCI, squeeze momentum, linear regression slope, R-squared, standard error, Chande forecast oscillator, efficiency ratio, center of gravity, polarized fractal efficiency, random walk index, trend intensity index, pretty good oscillator, intraday intensity, volume rate of change, trade volume index, Donchian width, coefficient of variation, normalized ATR, true range, performance, Parkinson and Garman-Klass volatility.
Choosing What Your Users See
A catalog of 129 is for you, not necessarily for your users. Two controls decide what an end
user is offered. registerExtraIndicators takes only or except, so only the studies you
name exist at all; and the shell's indicators option curates the menu: which ids, pinned
favorites, headed groups, and the labels your users know them by.
registerExtraIndicators({ only: ["kama", "stc", "squeeze", "vwapbands"] });
createChartShell(el, {
indicators: {
featured: ["sma", "rsi", "squeeze"],
groups: [
{ title: "Trend", ids: ["sma", "ema", "kama", "supertrend"] },
{ title: "Momentum", ids: ["rsi", "macd", "stc", "squeeze"] },
{ title: "Volume", ids: ["volume", "vwapbands", "kvo"] },
],
otherTitle: "More",
labels: { rsi: "Strength" },
},
});
include and exclude narrow the list, an array of ids is a plain allowlist, and false
removes the menu. The menu reads the registry when it opens, so a study registered after the
shell was created is offered too. Search matches your labels as well as the standard names.