DocumentationIndicators

Indicators

There are two ways to put an indicator on a chart, and they suit different jobs.

chart.addIndicator(id) is the managed path: the chart creates the series, picks the pane, draws reference levels, recomputes on every data change, and cleans up on removal. The 18 built-ins live in the lazy openalgo-charts/indicators tier.

import { createChart } from 'openalgo-charts';
import 'openalgo-charts/indicators';   // registers all 18 built-ins
 
const chart = createChart(el);
chart.addSeries('candlestick').setData(bars);
 
chart.addIndicator('bollinger');                       // overlays the price pane
const macd = chart.addIndicator('macd', { fastPeriod: 8 });  // gets its own pane
macd.setSettings({ fastPeriod: 12 });
macd.remove();

The raw functions (ema, rsi, atr, supertrend) stay in the base bundle for when you want to compute a value and plot it yourself. They use Wilder semantics to match openalgo.ta; the *Series helpers return Bar[] with NaN in warmup slots, which the line renderer skips as whitespace.

import {
  ema, emaSeries,            // EMA: raw values, and a plottable line series
  rsi, rsiSeries,            // RSI
  supertrend, supertrendSeries,
  atr, trueRange,            // ATR (raw values) + true range
} from 'openalgo-charts';

The built-in catalog

const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 220, 3600);
chart.addSeries('candlestick').setData(bars);
chart.addIndicator('bollinger');
chart.addIndicator('macd');
chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…
IndicatoridPlacementKey settings
Simple Moving Averagesmaonchartlength, source
Exponential Moving Averageemaonchartlength, source
Weighted Moving Averagewmaonchartlength, source
VWAPvwaponchartanchor (session / continuous), source
Bollinger Bandsbollingeronchartlength, stdDev, source
Supertrendsupertrendonchartperiod, multiplier
Parabolic SARparabolic-saronchartstart, increment, maximum
Ichimoku CloudichimokuonchartconversionPeriod, basePeriod, laggingSpanPeriod, displacement
RSIrsipanelength, source, overbought, oversold
MACDmacdpanefastPeriod, slowPeriod, signalPeriod
StochasticstochasticpanekPeriod, kSmoothing, dPeriod
ADX / DMIadxpaneperiod, adxPeriod
CCIccipaneperiod, constant
Money Flow Indexmfipaneperiod
ATRatrpaneperiod
Volumevolumepanecolor
On-Balance Volumeobvpanecolor
Accumulation/Distributionadlpanecolor

onchart indicators overlay the price pane; pane indicators create their own. Override with chart.addIndicator('rsi', {}, { paneIndex: 2 }).

VWAP anchors on the IST trading day by default, matching the rest of the engine’s session handling. Pass { anchor: 'continuous' } for a running VWAP over all loaded data.

The IndicatorApi handle

addIndicator returns a handle:

MemberTypeDescription
idstringUnique instance id (several instances of one indicator can coexist).
indicatorIdstringThe descriptor id, e.g. 'macd'.
namestringDisplay name.
paneIndexnumberPane the indicator drew into.
settings()IndicatorSettingsCurrent settings, as a copy.
setSettings(patch)voidMerge a patch, recompute, and restyle.
series(plotKey)SeriesApi | undefinedThe series behind one plot, for direct styling.
values()IndicatorValuesLatest computed columns.
remove()voidRemove every series and level it created.

chart.indicators() lists the live instances; chart.removeIndicator(id) removes one by id.

Writing your own

An indicator is data, not code in the core — the chart never switches on an id. Each plot names a registered chart type, so your indicator draws through the same renderers as any other series and adds no drawing code.

import { registerIndicator, sourceValues } from 'openalgo-charts';
 
registerIndicator({
  id: 'momentum',
  name: 'Momentum',
  category: 'Momentum',
  placement: 'pane',
  inputs: [
    { key: 'length', type: 'number', label: 'Length', default: 10, min: 1, max: 500 },
    { key: 'source', type: 'source', label: 'Source', default: 'close' },
    { key: 'color',  type: 'color',  label: 'Color',  default: '#4f8cff' },
  ],
  plots: [{ key: 'mom', type: 'line', title: 'Momentum', colorKey: 'color' }],
  calc: (bars, s) => {
    const v = sourceValues(bars, s.source);
    const n = s.length;
    return { mom: v.map((x, i) => (i >= n ? x - v[i - n] : null)) };
  },
  levels: () => [{ price: 0, color: '#5a6b8c', dashed: true }],
});
 
chart.addIndicator('momentum', { length: 14 });

calc must return one array per plot key, the same length as bars. Use null for warmup slots — the line renderer breaks across them and autoscale skips them, so a gap draws as nothing rather than a spike to zero.

Descriptor reference

FieldTypeDescription
idstringRegistry key.
namestringDisplay name.
categorystring?Grouping for a picker UI.
placement'onchart' | 'pane'Overlay the price pane, or take a new one.
inputsIndicatorInput[]Tunable settings; type is what a settings UI renders.
plotsIndicatorPlot[]One per drawn series. type is any registered chart type.
calc(bars, settings, store) => IndicatorValuesFull recompute.
calcTail(bars, settings, fromIndex, previous, store) => IndicatorValues | nullOptional incremental path.
levels(settings) => IndicatorLevel[]Optional horizontal reference lines.
range(settings) => \{ min, max \} | nullOptional fixed pane range (RSI 0..100).
attach(ctx) => (() => void) | voidOptional lifecycle for external data (Tier 2).

Input types are number, boolean, color, text, select, and source. A source input picks a price series; INDICATOR_SOURCES is the canonical option list for a UI.

⚠️

calcTail matters for live charts. Without it every incoming tick costs a full recompute — a few hundred microseconds for one indicator over 50k bars, but O(n) per tick per indicator. Implement it for anything meant to run in a busy live pane: return values for [fromIndex, bars.length) and the runtime splices them onto the previous result, or return null to fall back to a full calc.

Tier 2: indicators with their own data

Some series aren’t derived from the chart’s OHLCV at all — open interest, cumulative volume delta, PCR, an external analytics feed. createTier2Indicator wraps a fetch / subscribe lifecycle into an ordinary descriptor, so the runtime, settings, panes, and removal all work identically. There is no second runtime.

import { registerIndicator } from 'openalgo-charts';
import { createTier2Indicator } from 'openalgo-charts/indicators';
 
registerIndicator(createTier2Indicator({
  id: 'open-interest',
  name: 'Open Interest',
  placement: 'pane',
  inputs: [{ key: 'symbol', type: 'text', label: 'Symbol', default: 'NIFTY' }],
  plots: [{ key: 'oi', type: 'line', title: 'OI' }],
  refetchOn: ['symbol'],                       // these settings invalidate the data
  fetch: async ({ settings, from, to }) => {
    const rows = await loadOpenInterest(settings.symbol, from, to);
    return rows.map((r) => ({ time: r.time, values: { oi: r.oi } }));
  },
  subscribe: (ctx, push) => streamOi(ctx.settings.symbol, (r) =>
    push({ time: r.time, values: { oi: r.oi } })),
}));

Alignment rule. External points carry their own timestamps, which rarely match bar times. Each bar takes the most recent point at or before it — last-known-value, never interpolated and never forward-looking. Bars before the first point are null. A failed fetch leaves the previous points on screen rather than blanking the pane.

Supertrend

supertrendSeries(bars, period, multiplier) returns { up, down } so you can color the uptrend and downtrend legs differently (direction -1 = up, +1 = down).

const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 220, 3600);
chart.addSeries('candlestick').setData(bars);

const st = lib.supertrendSeries(bars, 10, 3);
chart.addSeries('line', { style: { color: '#26a69a', lineWidth: 2 } }).setData(st.up);
chart.addSeries('line', { style: { color: '#ef5350', lineWidth: 2 } }).setData(st.down);

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…

Return type: SupertrendPoint

supertrend(bars, period?, multiplier?) returns SupertrendPoint[]. Each element carries:

FieldTypeMeaning
valuenumberThe band price level. NaN during ATR warmup (first period - 1 bars).
direction-1 or 1-1 = uptrend: band is support below price (bullish). 1 = downtrend: band is resistance above price (bearish).

supertrendSeries converts that array into { up: Bar[], down: Bar[] }. The up series carries value only while direction === -1; the down series only while direction === 1. Non-active slots hold NaN so the renderer breaks the line at each trend flip, producing two separately colored segments. SupertrendPoint is a named export; see Types for the full type catalogue.

RSI

Plot RSI in its own pane with 70/30 guide lines:

const chart = lib.createChart(el);
const bars = lib.generateBars(1700000000, 220, 3600);
chart.addSeries('candlestick').setData(bars);

chart.addSeries('line', { paneIndex: 1, style: { color: '#e0b020', lineWidth: 2 } })
   .setData(lib.rsiSeries(bars, 14));
chart.addPriceLine({ price: 70, color: '#ef5350', lineWidth: 1, dashed: true, id: 'r70' }, 1);
chart.addPriceLine({ price: 30, color: '#26a69a', lineWidth: 1, dashed: true, id: 'r30' }, 1);

chart.timeScale.fitContent(bars.length);
return chart;
live
Rendering live chart…

EMA

emaSeries(bars, period) returns a line you overlay on price (see Series & Styling). Stack several periods for a ribbon.

Need an indicator that is not built in? Compute it however you like - these helpers just return Bar[] (set close to the indicator value). Plot the result as a line series. For an entirely new on-chart drawing, write a primitive.

ATR

The ATR family consists of two functions that build on each other. Both accept flat readonly number[] arrays rather than Bar objects, so they work on any OHLC source.

trueRange

trueRange(
  high: readonly number[],
  low: readonly number[],
  close: readonly number[],
): number[]

Returns the raw true range per bar with no smoothing. The first bar is high[0] - low[0] because there is no previous close. Every subsequent bar is the maximum of:

  • high[i] - low[i]
  • Math.abs(high[i] - close[i - 1])
  • Math.abs(low[i] - close[i - 1])

Every output slot is finite; there is no warmup period.

atr

atr(
  high: readonly number[],
  low: readonly number[],
  close: readonly number[],
  period?: number,   // default 14
): number[]

Smooths trueRange output using Wilder’s formula. The first output lands at index period - 1 (a plain average of the first period true ranges); from there each value is (prev * (period - 1) + tr[i]) / period. Slots before period - 1 are NaN, which the line renderer skips as whitespace.

import { atr, trueRange } from 'openalgo-charts';
 
const rawTR   = trueRange(highs, lows, closes);   // finite for every bar, no warmup
const atrVals = atr(highs, lows, closes, 14);     // NaN for bars 0-12, then Wilder ATR

atr is consumed internally by supertrend. Call it directly when you need a volatility measure for position sizing or stop distances, without computing the full band.