Customization
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.
An indicator in twenty lines
@ortex-charts/financialLoading 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.
custom-indicator.ts
import { createFinancialChart, registerIndicator } from "@ortex-charts/financial";
import { rollingMean, rollingStd } from "@ortex-charts/math";
/**
* How far the close has strayed from its own recent mean, in standard
* deviations. Twenty lines, and the engine takes care of the rest: alignment
* to the timeline, the pane, the level lines at ±2, recomputation on every new
* bar, and the legend readout under the crosshair.
*/
registerIndicator({
id: "zscore",
name: "Close z-score",
short: "Z",
pane: "separate",
levels: [-2, 0, 2],
priceFormat: { type: "price", precision: 2 },
// The last value depends on this many bars, so a live tick recomputes the
// window rather than the whole history.
lookback: (inputs) => Number(inputs.length) + 1,
inputs: [{ key: "length", name: "Length", type: "number", default: 60, min: 10, max: 250, step: 5 }],
outputs: [{ key: "z", title: "Z-score", plot: "line", color: (t) => t.palette[1], lineWidth: 1.5 }],
compute({ columns, inputs, length }) {
const window = Number(inputs.length);
const mean = rollingMean(columns.close, window);
const deviation = rollingStd(columns.close, window);
const z = new Float64Array(length).fill(NaN);
for (let i = 0; i < length; i++) {
if (deviation[i] > 0) z[i] = (columns.close[i] - mean[i]) / deviation[i];
}
return { z };
},
});
const chart = createFinancialChart(box, {
theme,
timeZone: TIME_ZONE,
resolution: "1D",
volume: false,
watermark: { text: SYMBOL, visible: true, fontSize: 36 },
data: bars,
});
// The same call a built-in takes, because there is no difference any more.
const zscore = chart.addIndicator("zscore", { length: 60 }, { paneHeightRatio: 0.34 });
chart.chart.subscribeCrosshairMove((e) => {
const value = e.index === null ? null : zscore.series.get("z")?.primaryValue(e.index);
if (value === null || value === undefined || !Number.isFinite(value)) return;
status(`Close z-score ${value >= 0 ? "+" : ""}${value.toFixed(2)} over ${zscore.inputs.length} bars`);
});