Skip to main content

Write a Custom Indicator

TradeScript chart showing the expected subpane output from a registered indicator
Expected result: the indicator is registered once, added to a chart, and visible with its configured inputs.

The finished indicator is a 20-period EMA: a green line over the candles on the main price pane, a legend entry reading Tutorial EMA, and a Length field in its Settings dialog.

Integration steps

Add custom indicator modules to your application build. Register each module before adding it to a chart. The integration has five steps:

StageWhat happensWhat can go wrong
SourceYou author a TypeScript module with one export default indicator(...)Importing anything outside the documented indicator packages
DefinitionThe options object (id, compactLabel, pane, ...) and input.* calls define the indicator's identity, Settings fields, and defaultsMissing stable ids on inputs; invalid option values
RegistrationregisterCustomIndicator prepares the definition for useInvalid options or inputs prevent registration
Addchart.addIndicator({ name }) creates an instance using the registered nameThe module was not registered before it was added
VerifyThe plot appears, the legend lists the indicator, and Settings shows its inputsThe calculation reports an error or has not received enough bars

1. Source — create the indicator module

src/indicators/tutorialEma.ts
import { close, indicator, input, plot } from '@tradescript/pro/sdk/indicators'
import * as ta from '@tradescript/pro/sdk/indicators/ta'

const tutorialEma = indicator('Tutorial EMA', () => {
const length = input.int('Length', 20, {
id: 'length',
min: 1,
max: 500,
})

plot.line(ta.ema(close, length), {
key: 'ema',
title: 'EMA',
color: '#22c55e',
})
}, {
id: 'tutorial-ema',
compactLabel: 'EMA',
description: 'Exponential moving average tutorial',
pane: 'price',
})

export default tutorialEma

Import only the documented indicator packages:

  • @tradescript/pro/sdk/indicators
  • @tradescript/pro/sdk/indicators/ta
  • @tradescript/pro/sdk/indicators/math

2. Definition — options and inputs drive the chart UI

The third argument to indicator(...) plus your input.* calls define the indicator's chart behavior:

  • id: 'tutorial-ema' keeps saved layouts stable across renames.
  • compactLabel: 'EMA' labels the legend and price scale.
  • pane: 'price' overlays the main series; 'separate' and 'volume' create their own panes.
  • Every input's id (here length) becomes the key hosts use in inputs: { length: 21 }.

3. Registration — register during application startup

import { registerCustomIndicator } from '@tradescript/pro/sdk/indicators'
import tutorialEma from './indicators/tutorialEma'

export const tutorialEmaName = registerCustomIndicator(tutorialEma)

Register each definition once before a chart tries to add it. If registration throws, check the indicator options and input definitions before starting the chart.

4. Add — create an instance on a chart

const instanceId = await chart.addIndicator({
name: tutorialEmaName,
paneId: 'candle_pane',
inputs: { length: 21 },
})

Use the exact string returned from registerCustomIndicator as name.

pane optionPlacement
'price'Overlay the main series with paneId: 'candle_pane'
'separate'Create a separate pane
'volume'Create a volume-style pane

5. Verify — what you should see

With bars loaded, the chart now shows:

  • a green EMA line tracking the candles on the main price pane, with NaN gaps for the first 20 warm-up bars
  • a legend entry reading Tutorial EMA (compact label EMA) with the current length value
  • a Settings dialog for the instance with a Length integer field (min 1, max 500) on the Inputs tab

Confirm programmatically:

chart.updateIndicator(instanceId, { inputs: { length: 50 } }) // line recomputes with the longer lookback
chart.removeIndicator(instanceId) // line and legend entry disappear

Diagnosing

Nothing renders after addIndicator:

  • Register the module during application startup, and pass the value returned by registerCustomIndicator as name.

registerCustomIndicator throws:

  • Check that the options object and every input.* call carry stable, unique id values and valid option values, then register the corrected module.

The indicator is listed but plots nothing (or partially):

  • Check the message passed to error('...') in your application logs or Settings UI.
  • Leading NaN values are normal warm-up for lookback windows, not an error.

Next steps