Skip to content

Indicators ​

ts
const id = chart.indicators.add({ type: 'BOLL', params: { period: 26 } });
chart.indicators.update(id, { colors: { mid: '#f5b942' } });
chart.indicators.remove(id);

Adding an indicator ​

ts
interface IndicatorInput {
  id?: string; // generated, e.g. 'macd-1'
  type: string; // 'MA', 'MACD', … or a registered custom type
  params?: Record<string, number | number[]>; // overrides of the defaults below
  pane?: string; // 'main', 'new' (a pane of its own) or an existing pane id
  source?: PriceSource | IndicatorReference; // default 'close'
  visible?: boolean;
  colors?: Record<string, string>; // per output key
}

type PriceSource = 'open' | 'high' | 'low' | 'close' | 'hl2' | 'hlc3' | 'ohlc4';
type IndicatorReference = { kind: 'indicator'; id: string; key?: string }; // see below

Overlays default to the main pane, oscillators to a new pane. To put two oscillators in one pane, place the second one in the pane of the first:

ts
const rsi = chart.indicators.add({ type: 'RSI' });
chart.indicators.add({ type: 'WR', pane: chart.indicators.get(rsi)!.pane });

Changing params or source recomputes the indicator; parameters are validated and unknown keys are rejected.

Indicators on indicators ​

An indicator can read another indicator's output instead of a price: a moving average of RSI, Bollinger Bands on volume, an EMA of the MACD signal line.

ts
const rsi = chart.indicators.add({ type: 'RSI', params: { periods: [14] } });
chart.indicators.add({
  type: 'MA',
  params: { periods: [9] },
  source: { kind: 'indicator', id: rsi }
});

const macd = chart.indicators.add({ type: 'MACD' });
chart.indicators.add({ type: 'EMA', source: { kind: 'indicator', id: macd, key: 'dea' } });
  • Without key the indicator reads the first output and keeps following it when the upstream parameters, and so its keys (rsi6 becomes rsi14), change. A named key must keep existing: an update that would remove it throws.
  • Overlays go to the pane of the indicator they read; oscillators to a new pane. Precision price follows the upstream indicator.
  • The calculator receives the upstream value as the price, and as open, high, low and close of the bar. Bars without an upstream value (its warm-up) are skipped, so averages start once the upstream has values.
  • Streaming stays incremental, and changing the upstream indicator recomputes the ones that read it.
  • An indicator cannot be removed while another reads it, and indicators cannot read each other in a loop.
  • The legend names the input, e.g. MA 9 · RSI.

Placing indicators ​

pane chooses the pane and scale the price scale in it: right (default), left, or an overlay scale of the indicator's own. Volume under the bars of the main pane:

ts
chart.indicators.add({ type: 'VOL', pane: 'main', scale: 'volume' });
chart.priceScale('main', 'volume').setMargins({ top: 0.8, bottom: 0 });

Coloring bars ​

mainSeries.barColor colors single bars of the main series by any condition: return a color, or null for the usual up or down color. It receives the bar and reads indicators at it, and runs for the visible bars whenever the chart repaints. Candles, hollow candles, OHLC and high-low bars use it.

ts
const ma = chart.indicators.add({ type: 'MA', params: { periods: [20] } });

chart.applyOptions({
  mainSeries: {
    barColor: ({ bar, indicator }) => (bar.close > indicator(ma) ? '#0d9488' : null)
  }
});

indicator(id, key?) reads an output (the first by default) and returns NaN where the indicator has no value. A callback returning anything but a string or null, or reading an unknown indicator, throws when the chart paints.

Built-in indicators ​

Formulas follow the conventions of Chinese charting software (同花顺 / 通达信): EMAs and SMA(X, N, 1) smoothing are seeded with the first value, the MACD histogram is doubled, K and D of KDJ start at 50, and standard deviations are population deviations. A value that cannot be computed (warm-up, division by zero) is left empty rather than replaced by a number.

TypePaneParameters (defaults)Outputs
MAmainperiods [5, 10, 20, 30]ma5, ma10, …
EMAmainperiods [12, 26]ema12, ema26
SMMAmainperiods [7, 14]smma7, smma14 — smoothed (Wilder) average
BOLLmainperiod 20, multiplier 2mid, upper, lower
VOLnewperiods [5, 10]volume (columns), ma5, ma10
MACDnewfast 12, slow 26, signal 9dif, dea, macd (columns)
RSInewperiods [6, 12, 24]rsi6, rsi12, rsi24
KDJnewperiod 9, k 3, d 3k, d, j
WRnewperiods [14, 28]wr14, wr28 (0 at the high, 100 at the low)
CCInewperiod 14cci
ROCnewperiod 12roc
BIASnewperiods [6, 12, 24]bias6, bias12, bias24
OBVnew—obv
VRnewperiod 24vr
EMVnewperiod 14, signal 9emv, maemv
MFInewperiod 14mfi
ATRnewperiod 14atr
STDnewperiod 20std
DMInewperiod 14pdi, mdi, adx, adxr

periods parameters accept up to eight values and draw one line each.

Reading values ​

ts
chart.indicators.valuesAt(id); // latest bar
chart.indicators.valuesAt(id, chart.crosshair.get()?.index ?? chart.data.length - 1);
// [{ key: 'dif', label: 'DIF', color: '#f5b942', value: 0.0293, text: '0.0293' }, …]

The chart also shows these values in each pane's legend (options.legend.visible). Alerts can watch any output; see Trading.

Custom indicators ​

Register a type on a chart, then add it like a built-in one. The simplest form derives each value from the latest bars:

ts
chart.indicators.register<{ period: number }>('MOM', {
  name: 'MOM',
  pane: 'new',
  precision: 2,
  params: { period: { default: 10, min: 1, integer: true } },
  outputs: () => [{ key: 'mom', label: 'MOM', plot: { kind: 'line' } }],
  levels: () => [0],
  lookback: ({ period }) => period + 1,
  compute: (window) => [window.price(window.length - 1) - window.price(0)]
});

chart.indicators.add({ type: 'MOM', params: { period: 20 } });

compute() receives the last lookback bars, oldest first, and returns one number per output (NaN for none). Until the window is full the outputs stay empty; from then on it is called once per bar.

For indicators whose value depends on the whole history, such as exponential averages, implement a streaming calculator instead. next() consumes one bar; clone() must copy all state, because the chart keeps a checkpoint before the latest bar and replays only that bar on every tick.

ts
import type { Bar, IndicatorStream } from '@kumochart-sdk/core';

class EmaStream implements IndicatorStream {
  constructor(
    private readonly alpha: number,
    private value = Number.NaN
  ) {}

  next(_bar: Bar, price: number): number[] {
    this.value = Number.isNaN(this.value) ? price : this.value + this.alpha * (price - this.value);
    return [this.value];
  }

  clone(): EmaStream {
    return new EmaStream(this.alpha, this.value);
  }
}

chart.indicators.register<{ period: number }>('MY_EMA', {
  name: 'EMA',
  pane: 'main',
  precision: 'price',
  params: { period: { default: 20, min: 1, integer: true } },
  outputs: () => [{ key: 'ema', label: 'EMA', plot: { kind: 'line' } }],
  create: ({ period }) => new EmaStream(2 / (period + 1))
});

Definition reference ​

FieldDescription
nameShown in the legend
pane'main' or 'new': the default placement
params{ key: { default, min?, max?, integer?, label? } }; an array default makes a list parameter
outputs(params)[{ key, label, plot, color?, legend? }]; see Plots. legend: false keeps a value out of the legend
levels?(params)Horizontal reference lines
fills?[{ from, to }]: shade between two outputs
precisionDecimals, 'price' (follow the main series) or 'volume' (compact, e.g. 1.2M)
range?Fixed scale range of the pane, e.g. { min: 0, max: 100 }
create(params) or lookback(params) + compute(window, params)The calculation

Plots ​

plotDraws
{ kind: 'line', style? }A line; style is 'solid', 'dashed' or 'dotted'
{ kind: 'histogram', colorBy }Columns from zero, colored by the 'sign' of the value, its 'change' or the 'bar' direction
{ kind: 'columns', from, width?, hollow? }Columns in the output's color from from (another output's key, or a number) to the value, where both are defined
{ kind: 'marker', shape?, text?, size? }A shape (as for markers), a text or both at the value, where it is defined
{ kind: 'hidden' }Nothing: the value is only in the legend and available to alerts

Values that are NaN are not drawn, so columns and markers appear only on the bars that have them: signals, highlighted bars, bodies of custom candles. The price scale makes room for both ends of columns. Upward shapes point up at the value from below and downward shapes down at it from above, so an arrow at a low sits under the bar; circles and squares sit on the value. A marker's text goes beyond its shape, or on the value without one. Without a color, arrows and triangles take the theme's buy and sell colors.

ts
chart.indicators.register('SIGNALS', {
  name: 'Signals',
  pane: 'main',
  precision: 'price',
  params: {},
  outputs: () => [
    // An arrow pointing at the low of the bars that close up three times in a row.
    {
      key: 'buy',
      label: 'Buy',
      plot: { kind: 'marker', shape: 'arrow-up', text: 'B' },
      legend: false
    },
    // Bodies of those bars, from open to close.
    { key: 'open', label: 'Open', plot: { kind: 'hidden' }, legend: false },
    { key: 'close', label: 'Close', plot: { kind: 'columns', from: 'open' }, legend: false }
  ],
  lookback: () => 4,
  compute: (window) => {
    const bars = [0, 1, 2, 3].map((index) => window.bar(index));
    const rising = bars
      .slice(1)
      .every((bar, index) => bar.close > (bars[index]?.close ?? Infinity));
    const last = bars[3];
    return rising && last ? [last.low, last.open, last.close] : [NaN, NaN, NaN];
  }
});

Released under the MIT license.