Skip to content

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]
An indicator in twenty lines@ortex-charts/financial
Loading an indicator in twenty lines
Register your own indicator and it appears in the menu, the legend and the saved layout like a built-in one.

Open this example with its source

Placement 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,
});
  • pane overrides 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.
  • colors overrides output colors by output key; unset, each output takes the color its definition derives from the theme, so a theme switch recolors it.
  • seriesOptions is applied to every output series.
  • sourceSeriesId computes 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.