Skip to content

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/financial
Loading 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`);
});