Indicators
const id = chart.indicators.add({ type: 'BOLL', params: { period: 26 } });
chart.indicators.update(id, { colors: { mid: '#f5b942' } });
chart.indicators.remove(id);Adding an indicator
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 belowOverlays 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:
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.
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
keythe indicator reads the first output and keeps following it when the upstream parameters, and so its keys (rsi6becomesrsi14), change. A namedkeymust 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
pricefollows 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:
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.
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.
| Type | Pane | Parameters (defaults) | Outputs |
|---|---|---|---|
MA | main | periods [5, 10, 20, 30] | ma5, ma10, … |
EMA | main | periods [12, 26] | ema12, ema26 |
SMMA | main | periods [7, 14] | smma7, smma14 — smoothed (Wilder) average |
BOLL | main | period 20, multiplier 2 | mid, upper, lower |
VOL | new | periods [5, 10] | volume (columns), ma5, ma10 |
MACD | new | fast 12, slow 26, signal 9 | dif, dea, macd (columns) |
RSI | new | periods [6, 12, 24] | rsi6, rsi12, rsi24 |
KDJ | new | period 9, k 3, d 3 | k, d, j |
WR | new | periods [14, 28] | wr14, wr28 (0 at the high, 100 at the low) |
CCI | new | period 14 | cci |
ROC | new | period 12 | roc |
BIAS | new | periods [6, 12, 24] | bias6, bias12, bias24 |
OBV | new | — | obv |
VR | new | period 24 | vr |
EMV | new | period 14, signal 9 | emv, maemv |
MFI | new | period 14 | mfi |
ATR | new | period 14 | atr |
STD | new | period 20 | std |
DMI | new | period 14 | pdi, mdi, adx, adxr |
periods parameters accept up to eight values and draw one line each.
Reading values
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:
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.
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
| Field | Description |
|---|---|
name | Shown 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 |
precision | Decimals, '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
plot | Draws |
|---|---|
{ 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.
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];
}
});