Writing an indicator
Build a custom indicator from scratch — the IndicatorPlugin contract, compute pipeline, fixture-based testing.
ChartForge's indicator pipeline is open. Implementing a custom
indicator means writing a pure function (bars, params) => series
and wrapping it in the IndicatorPlugin contract. This guide walks
through a complete example — a "Price Above EMA" boolean indicator
that paints a green dot below every bar where close > EMA.
Quick example
import type { IndicatorPlugin, Bar } from '@elitechart/core';
interface PriceAboveEmaParams {
readonly period: number;
}
export const priceAboveEma: IndicatorPlugin<PriceAboveEmaParams> = {
id: 'price-above-ema',
name: 'Price Above EMA',
paneKind: 'overlay',
defaultParams: { period: 20 },
compute(bars: ReadonlyArray<Bar>, params: PriceAboveEmaParams) {
const { period } = params;
const out: Array<{ time: number; value: number }> = [];
let ema = 0;
const k = 2 / (period + 1);
for (let i = 0; i < bars.length; i += 1) {
const close = bars[i].close as number;
ema = i === 0 ? close : k * close + (1 - k) * ema;
out.push({ time: bars[i].time as number, value: close > ema ? 1 : 0 });
}
return [{ id: 'flag', name: 'Above', kind: 'marker', data: out }];
},
};
Register and add to the chart:
import { useChartStore } from '@elitechart/elitechart';
import { priceAboveEma } from './price-above-ema';
useChartStore.getState().registerIndicator(priceAboveEma);
useChartStore.getState().addIndicator({ id: 'pae-1', name: 'Price Above EMA', params: { period: 20 } });
How it works
The contract has five fields:
id — unique string identifier.
name — display name in the Indicators modal.
paneKind — 'overlay' or 'pane'. Overlay draws on the
price pane; pane gets its own.
defaultParams — shape + default values shown in the modal.
compute(bars, params) — pure function. Must be
deterministic and side-effect-free.
compute returns an array of output series. Each series has its own
id, name, and kind — line, histogram, marker, or band.
The renderer picks the right drawing strategy from the kind.
The pipeline runs compute on every bar update. For O(N) algorithms
(EMA, SMA, etc.) this is fine — 10k bars × 30 indicators × 60 fps
holds the budget. For O(N²) algorithms, hoist state into a closure:
compute(bars, params) {
const cache: number[] = [];
// ...your loop reuses cache...
}
Testing with fixtures
Every indicator in @elitechart/indicators ships with a fixture
file at __fixtures__/<name>.json — input bars + expected output.
The unit test asserts byte-for-byte equality.
// __tests__/price-above-ema.test.ts
import { describe, it, expect } from 'vitest';
import { priceAboveEma } from '../src/price-above-ema';
import fixture from './__fixtures__/price-above-ema.json';
describe('price-above-ema', () => {
it('matches reference fixture (period=20)', () => {
const result = priceAboveEma.compute(fixture.bars, { period: 20 });
expect(result).toEqual(fixture.expected);
});
});
Cite your fixture source in a top-of-file comment — a published
paper, a reference implementation you can link, or your own
verified output. Reviewers check this.
Variations
Multi-output indicator (band)
compute(bars, params) {
return [
{ id: 'mid', name: 'Mid', kind: 'line', data: midSeries },
{ id: 'upper', name: 'Upper', kind: 'line', data: upperSeries },
{ id: 'lower', name: 'Lower', kind: 'line', data: lowerSeries },
{ id: 'band', name: 'Band', kind: 'band', data: { upper: upperSeries, lower: lowerSeries } },
];
}
Pane indicator with a horizontal threshold
{
paneKind: 'pane',
defaultParams: { period: 14, overbought: 70, oversold: 30 },
compute(bars, params) {
return [
{ id: 'rsi', name: 'RSI', kind: 'line', data: rsiSeries },
{ id: 'ob', name: 'Overbought', kind: 'horizontal', value: params.overbought },
{ id: 'os', name: 'Oversold', kind: 'horizontal', value: params.oversold },
];
}
}
API
| Field | Type | Required |
|---|
id | string | yes |
name | string | yes |
paneKind | 'overlay' | 'pane' | yes |
defaultParams | object | yes |
compute | (bars, params) => series[] | yes |
validate? | (params) => string | null | no |
description? | string | no |