Charts
Indicators
The built-in library, their inputs, and how to register your own in twenty lines.
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 18. That is a deliberate statement of fact rather than a headline: TradingView
Advanced Charts ships roughly 110 studies and Highcharts Stock about 50. What is here is
the set that a price chart is unusable without, plus the order-flow three. Everything else
is a registerIndicator call, and the engine — inputs, multiple outputs, bands, levels,
own panes, incremental recompute — has no privileged path for the built-ins.
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 |
bb |
Bollinger Bands | length (20), mult (2), source |
upper, basis, lower |
donchian |
Donchian Channels | length (20) |
upper, basis, lower |
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 |
macd |
MACD | fast (12), slow (26), signal (9), source |
hist, macd, signal |
0 |
stoch |
Stochastic | k (14), smooth (3), d (3) |
k, d |
20, 80 |
atr |
Average True Range | length (14) |
atr |
— |
cci |
Commodity Channel Index | length (20) |
cci |
−100, 100 |
roc |
Rate of Change | length (9), source |
roc |
0 |
obv |
On Balance Volume | — | obv |
— |
volume |
Volume | ma (20) |
volume, ma |
— |
cumvol |
Cumulative Volume | length (20) |
sum |
— |
delta |
Volume Delta | method (auto, aggressor, estimate) |
delta |
0 |
cvd |
Cumulative Volume Delta | method |
cvd |
— |
rvol |
Relative Volume | length (50) |
rvol |
1 |
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 18 indicators deep, not 110. Ichimoku, Supertrend, ADX, Parabolic SAR, Keltner Channels, Money Flow Index, Williams %R, Aroon, TRIX and the rest of the long tail are not built in.
- Indicators of indicators are not declarative. Computing an RSI of a moving average
means doing both calculations inside one
compute.