Skip to main content

Custom studies

Custom studies allow you to create your own technical indicators for the chart. You can implement any calculation logic using price data, create multiple output series with different visualizations, and leverage built-in utility functions for common technical analysis calculations.

Overview

A custom study is defined by:

  • Metadata: Name, parameters, output lines (plots)
  • Init function (optional): One-time setup — request secondary data, declare plot displacement, or set the recalculation window
  • Main calculation function: Calculate indicator values for each candle

Custom studies are fully integrated with the chart's lifecycle, updating automatically when:

  • New price data arrives (real-time ticking)
  • Historical data is loaded
  • Study parameters are changed

Creating a custom study

Use the __CHART_REACT_API.addCustomStudy() method to register a custom study:

__CHART_REACT_API.addCustomStudy({
name: 'My Custom Indicator',
metainfo: {
id: 'MY_CUSTOM_INDICATOR',
title: 'My Custom Indicator',
overlaying: true, // true = overlay on main chart, false = separate pane
parameters: [
{
id: 'length',
title: 'Period',
type: 'INTEGER_RANGE',
defaultValue: 14,
min: 1,
max: 200,
},
{
id: 'source',
title: 'Source',
type: 'STRING',
defaultValue: 'CLOSE',
}
],
lines: [
{
id: 'output',
title: 'Output Line',
type: 'LINEAR',
color: '#2962FF',
thickness: 2,
}
],
},
constructor: {
main: function(ctx) {
const input = ctx.input;
const parameters = ctx.parameters;
const state = ctx.state;
const index = ctx.index;
const utils = ctx.utils;

// Initialize state arrays if needed
if (!state.values) {
state.values = [];
}

// Get price data
const price = input.close(0);
state.values[index] = price;

// Calculate your indicator
const result = /* your calculation */;

// Return values for each output line
return [result];
}
}
});

Study metadata

Study configuration

export interface CustomStudyChartInfo {
symbol: string;
period: number;
periodObject: AggregationPeriod;
mintick: number;
instrumentType: string;
timezone?: string;
firstVisibleIndex?: number;
lastVisibleIndex?: number;
}

Parameters

Define configurable inputs for your study:

parameters: [
{
id: 'length',
title: 'Period',
type: 'INTEGER_RANGE', // Number with min/max
defaultValue: 14,
min: 1,
max: 500,
},
{
id: 'source',
title: 'Price Source',
type: 'STRING', // Price type selector
defaultValue: 'CLOSE',
},
{
id: 'multiplier',
title: 'Multiplier',
type: 'DOUBLE_RANGE', // Floating point with min/max
defaultValue: 2.0,
min: 0.1,
max: 10.0,
},
{
id: 'showSignals',
title: 'Show Signals',
type: 'BOOLEAN',
defaultValue: true,
},
];

Available parameter types:

  • INTEGER_RANGE - Integer with min/max bounds
  • DOUBLE_RANGE - Floating point with min/max bounds
  • BOOLEAN - True/false toggle
  • STRING - Text input
  • ENUM - Custom enumeration with predefined options
  • COLOR - Color picker (hex string, e.g. '#2962FF')
  • DATE - Calendar date input (stored as YYYYMMDD)
  • TIME - Time-of-day input (stored as HHMM, 24-hour format)

Common parameter options:

  • visible - Show or hide the parameter in the settings dialog (default: true)
  • group - Collapsible section title in the settings dialog

Grouping parameters

Use the group property to organize related inputs into labeled sections in the indicator settings dialog. Group titles are persisted in the layout so the dialog keeps the same structure when reopened.

parameters: [
{
id: 'length',
title: 'Length',
type: 'INTEGER_RANGE',
defaultValue: 14,
min: 2,
max: 50,
group: 'inputs',
},
{
id: 'lineColor',
title: 'Line Color',
type: 'COLOR',
defaultValue: '#2962FF',
group: 'style',
},
],

Parameters without a group appear in the default (ungrouped) section.

Date and time parameters

DATE and TIME parameters render as calendar and clock inputs in the settings dialog. In main(), read them as strings from ctx.parameters:

parameters: [
{
id: 'startDate',
title: 'Start Date',
type: 'DATE',
defaultValue: '20240101', // YYYYMMDD
},
{
id: 'sessionStart',
title: 'Session Start',
type: 'TIME',
defaultValue: '0930', // HHMM (09:30)
},
],

DATE format: YYYYMMDD (e.g. '20240615' for June 15, 2024). Two special default values are supported:

  • '-1' — today's date (resolved at registration time)
  • '-2' — yesterday's date

TIME format: HHMM in 24-hour notation (e.g. '1430' for 2:30 PM). Seconds are not supported.

Usage in main():

main: function(ctx) {
const startDate = ctx.parameters.startDate; // e.g. '20240101'
const sessionStart = ctx.parameters.sessionStart; // e.g. '0930'

const year = parseInt(startDate.slice(0, 4), 10);
const month = parseInt(startDate.slice(4, 6), 10) - 1;
const day = parseInt(startDate.slice(6, 8), 10);

const hours = parseInt(sessionStart.slice(0, 2), 10);
const minutes = parseInt(sessionStart.slice(2, 4), 10);

const candleTime = ctx.input.time(0);
const candleDate = new Date(candleTime);
// Compare candle date/time against configured thresholds...
}

Custom enums as parameters

Define custom enumeration parameters to provide users with predefined choices:

parameters: [
{
id: 'maType',
title: 'Moving Average Type',
type: 'ENUM',
defaultValue: 'SMA',
options: ['SMA', 'EMA', 'WMA', 'DEMA', 'TEMA'],
},
{
id: 'signalType',
title: 'Signal Type',
type: 'ENUM',
defaultValue: 'BULLISH',
options: ['BULLISH', 'BEARISH', 'NEUTRAL', 'DIVERGENCE'],
},
];

Usage in the main function:

main: function(ctx) {
const { parameters } = ctx;

// Access the selected enum value
const selectedMAType = parameters.maType; // Returns: 'SMA', 'EMA', 'WMA', etc.

// Use in conditional logic
if (parameters.maType === 'SMA') {
// Use SMA calculation
} else if (parameters.maType === 'EMA') {
// Use EMA calculation
}
}

Benefits:

  • User-friendly UI: Dropdown selector instead of free text input
  • Type safety: Ensures only valid values are selected
  • Maintainability: Easy to add or modify options
  • Consistency: Users can't enter invalid values by mistake

Output lines (plots)

Define how the indicator's output is visualized:

lines: [
{
id: 'main',
title: 'Main Line',
type: 'LINEAR', // Line chart
color: '#2962FF',
thickness: 2,
},
{
id: 'signal',
title: 'Signal Line',
type: 'LINEAR',
color: '#FF6D00',
thickness: 1,
},
{
id: 'histogram',
title: 'Histogram',
type: 'HISTOGRAM', // Bar chart
color: '#00BCD4',
},
];

Available line types:

  • LINEAR - Continuous line
  • POINTS - Point markers
  • HISTOGRAM - Vertical bars from zero
  • TREND_HISTOGRAM - Histogram colored by trend
  • DIFFERENCE - Area between two lines
  • LINEAR_TREND - Line colored by trend
  • TREND_POINTS - Points colored by trend
  • ABOVE_CANDLE_TEXT - Text above candles
  • BELOW_CANDLE_TEXT - Text below candles
  • COLOR_CANDLE - Colors candles
  • UNIVERSAL_MARKER - Universal drawer — configurable marker and connecting line per plot

Universal drawer

The UNIVERSAL_MARKER plot type (shown as Universal drawer in the settings UI) combines a marker shape and an optional connecting line. Set default styles in metadata; users can change them in the indicator settings popup.

lines: [
{
id: 'signal',
title: 'Signal',
type: 'UNIVERSAL_MARKER',
color: '#CCCCCC',
markerStyle: 'DOT', // default marker shape
lineStyle: 'SOLID', // default line style
},
],

Available marker styles: NO, DOT, DASH, EXTENDED_DASH, HORIZONTAL, SQUARE, TRIANGLE_UP, TRIANGLE_DOWN, DIAMOND, CROSS, STAR, ARROW_UP, ARROW_DOWN, ARROW_UP_R, ARROW_DOWN_R, PLOT_LABEL, HISTOGRAM

Available line styles: NO, SOLID, DOTTED, SHORTDASHED, DOTDASHED, LONGDASHED, PIECEWISE

Return a plain number for the default styling, or an object to override marker, line, or color per candle:

main: function(ctx) {
const close = ctx.input.close(0);
const open = ctx.input.open(0);

if (close > open) {
return [{ value: close, color: '#00FF00', markerStyle: 'DOT' }];
} else if (close < open) {
return [{ value: close, color: '#FF0000', markerStyle: 'DOT' }];
}
return [{ value: close, color: '#FFA500' }];
}

For custom studies that declare UNIVERSAL_MARKER, the plot type selector in settings offers only this type.

Dynamic plot styling

Any plot can return per-candle styling as an object instead of a plain number:

return [{ value: 42.5, color: '#FF0000', markerStyle: 'ARROW_UP', lineStyle: 'DOTTED' }];

This works best with UNIVERSAL_MARKER, but color overrides are also supported on trend-based line types (LINEAR_TREND, TREND_POINTS, TREND_HISTOGRAM).

Init function

The optional init function runs once before the first main() call. Use it for one-time setup such as requesting secondary data, declaring plot displacement

export interface CustomStudyInitReturn {
recalcCandles?: number | 'all';
displaces?: number[];
}
constructor: {
init: function(ctx) {
// Request secondary data (see Secondary data section)
ctx.requestSecondaryData('daily', { period: { duration: 1, durationType: 'd' } });

// Return configuration for the calculation engine
return {
recalcCandles: 50, // Recalculate the last 50 candles on each tick
displaces: [0, 26, 52], // Shift each plot horizontally (one value per line)
};
},
main: function(ctx) {
// ...
},
}

init receives the same CustomStudyContext as main(), but runs at index 0 with an empty shared state. Secondary data requests must be made in init, not in main().

Controlling recalculation

By default, custom studies recalculate only the most recent candle on each tick for performance. For indicators that depend on future-looking or cross-candle state, you can widen the recalculation window so older points are recomputed as well.

It can be achieved with init() return value** — set recalcCandles to the number of trailing candles to recalculate on each update, or 'all' for a full recalculation of the entire series:

init: function(ctx) {
return { recalcCandles: 100 }; // Re-run main() for the last 100 candles on every tick
}

// Full recalculation mode — recalculate every candle on each tick:
init: function(ctx) {
return { recalcCandles: 'all' };
}

When both approaches are used, the init() return value takes precedence. Use full recalculation sparingly — it has a significant performance cost on large datasets.

Plot displacement (displace)

Displace shifts a plot's visual position horizontally on the chart without changing the underlying calculation index. This is useful for leading/lagging indicators (for example, shifting a signal line forward by N bars).

Return a displaces array from init() with one integer per output line (in the same order as lines in metadata). Each value is the number of candles to shift the plot backward on the time axis:

metainfo: {
lines: [
{ id: 'tenkan', title: 'Tenkan', type: 'LINEAR', color: '#2962FF' },
{ id: 'kijun', title: 'Kijun', type: 'LINEAR', color: '#FF6D00' },
{ id: 'chikou', title: 'Chikou', type: 'LINEAR', color: '#00BCD4' },
],
},
constructor: {
init: function(ctx) {
return {
displaces: [0, 0, 26], // Chikou line drawn 26 candles to the left
};
},
main: function(ctx) {
const close = ctx.input.close(0);
return [close, close, close]; // Values for tenkan, kijun, chikou
},
}

A displace of 26 means the value calculated at candle index i is drawn at the x-position of candle i - 26. Use 0 for plots that should not be shifted.

Study Context

The study context provides access to data and utilities:

export interface CustomStudyContext {
index: number;
candlesCount: number;
input: CustomStudyInputAccessors;
parameters: Record<string, unknown>;
state: Record<string, unknown>;
utils: CustomStudyUtils;
chart: CustomStudyChartInfo;
requestSecondaryData: (id: string, options?: SecondaryDataOptions) => void;
secondaryData: (id: string) => CustomStudyInputAccessors;
getSecondaryIndex: (id: string) => number;
events: Event[];
news: News[];
sessions: TradingSession[];
}

Input Data Access

The input object provides access to candle data and additional market information:

Chart information

The ctx.chart object provides information about the current chart configuration:

const chartInfo = ctx.chart;

// Chart symbol and period
const symbol = ctx.chart.symbol; // Current symbol (e.g., 'AAPL', 'EURUSD')
const period = ctx.chart.period; // Period in minutes
const mintick = ctx.chart.mintick; // Minimum price movement
const instrumentType = ctx.chart.instrumentType; // Instrument type
const timezone = ctx.chart.timezone; // Chart timezone (if available)

// Visible range information
const firstVisibleIndex = ctx.chart.firstVisibleIndex; // First candle in viewport
const lastVisibleIndex = ctx.chart.lastVisibleIndex; // Last candle in viewport

Example: Adjust calculations based on period

main: function(ctx) {
const period = ctx.chart.period;
let defaultLength = 20;

// Adjust MA length based on timeframe
if (period === 1) {
defaultLength = 50; // 1-minute chart
} else if (period === 5) {
defaultLength = 40; // 5-minute chart
} else if (period === 1440) {
defaultLength = 20; // Daily chart
}

const ma = utils.sma(
i => ctx.input.close(i),
defaultLength,
ctx.index
);

return [ma];
}

Basic OHLCV Data

const open = input.open(0);
const high = input.high(0);
const low = input.low(0);
const close = input.close(0);
const volume = input.volume(0);

// Previous candles (offset = 1, 2, 3...)
const prevClose = input.close(1);
const prevHigh = input.high(2);

Timestamp

const time = input.time(0); // Current candle timestamp (milliseconds)
const prevTime = input.time(1); // Previous candle timestamp
const date = new Date(time); // Convert to Date object

Advanced Market Data

// Volume Weighted Average Price (may be undefined if not sent by data provider)
const vwap = input.vwap(0);

// Open Interest (may be undefined if not sent by data provider)
const openInterest = input.openInterest(0);

// Implied Volatility (may be undefined if not sent by data provider)
const impVol = input.impVolatility(0);

Price Helpers

// Pre-calculated price combinations
const median = input.median(0); // (high + low) / 2
const typical = input.typical(0); // (high + low + close) / 3
const typicalPrice = input.typicalPrice(0); // Alias for typical()
const ohlcAvg = input.ohlcAverage(0); // (open + high + low + close) / 4

// Generic price accessor - useful with PRICE_FIELD parameters
const price = input.getPrice('CLOSE', 0);
const price = input.getPrice('TYPICAL', 0);
const price = input.getPrice('MEDIAN', 0);
// Available types: 'OPEN', 'HIGH', 'LOW', 'CLOSE', 'VOLUME', 'MEDIAN', 'TYPICAL', 'OHLC_AVERAGE'

Secondary data

Access price data from different instruments and timeframes. Secondary data requires two steps:

  1. Request the data in the init() function using requestSecondaryData()
  2. Access the data in the main() function using secondaryData()

Step 1: Request in init()

constructor: {
init: function(ctx) {
// Request secondary data from different instrument
ctx.requestSecondaryData('eurusd_data', {
symbol: 'EUR/USD',
period: { duration: 1, durationType: 'd' }, // Daily
priceType: 'CLOSE'
});

// Request secondary data from different timeframe (same instrument)
ctx.requestSecondaryData('daily_data', {
period: { duration: 1, durationType: 'd' }, // Daily candles
priceType: 'CLOSE'
});

// More timeframe examples
ctx.requestSecondaryData('hourly_data', {
period: { duration: 1, durationType: 'h' } // 1 hour
});
ctx.requestSecondaryData('weekly_data', {
period: { duration: 1, durationType: 'w' } // 1 week
});
},
main: function(ctx) {
// Access secondary data by using returned accessors
const eurusdAccessors = ctx.secondaryData('eurusd_data');
const eurusdClose = eurusdAccessors.close(0);
const eurusdHigh = eurusdAccessors.high(0);

const dailyAccessors = ctx.secondaryData('daily_data');
const dailyClose = dailyAccessors.close(0);

// ... rest of calculation
}
}

Secondary data request options:

  • symbol - Target instrument symbol (optional, omit to use current instrument)
  • period - Timeframe as AggregationPeriod object with:
    • duration: number (e.g., 1, 5, 15, 30, 60, 240, 1440, 10080)
    • durationType: string - duration type:
      • 'm' = minutes (e.g., { duration: 5, durationType: 'm' } = 5 minutes)
      • 'h' = hours (e.g., { duration: 1, durationType: 'h' } = 1 hour)
      • 'd' = days (e.g., { duration: 1, durationType: 'd' } = 1 day)
      • 'w' = weeks (e.g., { duration: 1, durationType: 'w' } = 1 week)
      • 'mo' = months (e.g., { duration: 1, durationType: 'mo' } = 1 month)
    • Common examples:
      • 1-minute: { duration: 1, durationType: 'm' }
      • 5-minute: { duration: 5, durationType: 'm' }
      • 1-hour: { duration: 1, durationType: 'h' }
      • Daily: { duration: 1, durationType: 'd' }
      • Weekly: { duration: 1, durationType: 'w' }
  • priceType - Price type from data provider: 'last', 'mark', 'bid', 'ask' (optional)

Accessing secondary data:

The secondaryData() function returns a CustomStudyInputAccessors object with the same methods as input:

const accessors = ctx.secondaryData('myId');
const close = accessors.close(0); // Current candle
const high = accessors.high(0);
const low = accessors.low(0);
const open = accessors.open(0);
const volume = accessors.volume(0);
const time = accessors.time(0);

// Previous candles
const prevClose = accessors.close(1);
const prevClose2 = accessors.close(2);

// Price helpers
const median = accessors.median(0);
const typical = accessors.typical(0);

Getting the secondary index:

When working with secondary data, you can get the corresponding index in the secondary data source:

// In main():
const secondaryIndex = ctx.getSecondaryIndex('eurusd_data');
// Returns the aligned candle index, or -1 if not available

Example: Ratio between two instruments

__CHART_REACT_API.addCustomStudy({
name: 'Currency Pair Ratio',
metainfo: {
id: 'CUR_RATIO',
title: 'EUR/USD Ratio',
overlaying: true,
parameters: [],
lines: [
{
id: 'ratio',
title: 'Ratio',
type: 'LINEAR',
color: '#2962FF',
}
],
},
constructor: {
init: function(ctx) {
// Request EUR/USD data
ctx.requestSecondaryData('eurusd', {
symbol: 'EUR/USD',
});
},
main: function(ctx) {
const { input } = ctx;

// Get current instrument close
const primaryClose = input.close(0);

// Get secondary instrument close
const eurusdAccessors = ctx.secondaryData('eurusd');
const secondaryClose = eurusdAccessors.close(0);

// Calculate ratio
if (!isNaN(primaryClose) && !isNaN(secondaryClose) && secondaryClose !== 0) {
const ratio = primaryClose / secondaryClose;
return [ratio];
}

return [NaN];
}
}
}, true);

Example: Multi-timeframe analysis

constructor: {
init: function(ctx) {
ctx.requestSecondaryData('daily', { period: { duration: 1, durationType: 'd' } });
ctx.requestSecondaryData('weekly', { period: { duration: 1, durationType: 'w' } });
},
main: function(ctx) {
const currentClose = ctx.input.close(0);
const dailyClose = ctx.secondaryData('daily').close(0);
const weeklyClose = ctx.secondaryData('weekly').close(0);

// Compare price to moving averages on different timeframes
const dailyTrend = dailyClose > weeklyClose ? 'bullish' : 'bearish';
return [currentClose];
}
}

Important notes:

  • Secondary data requests must be made in the init() function, not in main()
  • Secondary data must be available from the data provider. If requested data is not available, the function returns NaN
  • Use utils.na() to check if secondary data is available before using it
  • Secondary data requests may impact performance depending on the number and frequency of requests
  • Different symbols and periods may have different availability windows (e.g., pre-market data, weekends)

Trading Sessions Information

Access information about market trading sessions for the current instrument by using ctx.sessions:

// Get trading session information
const sessions = ctx.sessions; // Array of TradingSession objects

// Each session object contains:
// {
// from: number, // Session start time (milliseconds since epoch)
// to: number, // Session end time (milliseconds since epoch)
// type: SessionType, // Session type: 'REGULAR' | 'PRE_MARKET' | 'AFTER_MARKET' | 'NO_TRADING'
// }

// Check if there are any regular trading sessions
const hasRegularSession = sessions.some(s => s.type === 'REGULAR');

// Find all regular sessions
const regularSessions = sessions.filter(s => s.type === 'REGULAR');

// Calculate if candle is in a specific session
const candleTime = ctx.input.time(0);
const inRegularSession = sessions.some(s =>
s.type === 'REGULAR' &&
candleTime >= s.from &&
candleTime <= s.to
);

// Check for pre-market session
const preMarketSession = sessions.find(s => s.type === 'PRE_MARKET');
if (preMarketSession && candleTime >= preMarketSession.from && candleTime <= preMarketSession.to) {
// Current candle is in pre-market
}

Session Types:

  • 'REGULAR' - Normal trading hours
  • 'PRE_MARKET' - Pre-market trading (before regular hours)
  • 'AFTER_MARKET' - After-market trading (after regular hours)
  • 'NO_TRADING' - Non-trading session (holidays, weekends, etc.)

Example: Filter signals by trading session type

__CHART_REACT_API.addCustomStudy({
name: 'Session-Based Signal',
metainfo: {
id: 'SESSION_SIGNAL',
title: 'Session-Based Signal',
overlaying: true,
parameters: [
{
id: 'sessionType',
title: 'Trading Session Type',
type: 'ENUM',
defaultValue: 'REGULAR',
options: ['REGULAR', 'PRE_MARKET', 'AFTER_MARKET', 'ANY'],
}
],
lines: [
{ id: 'signal', title: 'Signal', type: 'POINTS', color: '#2962FF' }
],
},
constructor: {
main: function(ctx) {
const { input, parameters, utils, state, index } = ctx;

// Initialize state for price storage
if (!state.prices) {
state.prices = [];
}

const candleTime = input.time(0);
const close = input.close(0);
const sessions = ctx.sessions;
state.prices[index] = close;

// Check if current candle is in the selected session type
let inSelectedSession = false;

if (parameters.sessionType === 'ANY') {
inSelectedSession = sessions.length > 0;
} else {
inSelectedSession = sessions.some(s =>
s.type === parameters.sessionType &&
candleTime >= s.from &&
candleTime <= s.to
);
}

// Generate signal only during selected session type
if (inSelectedSession && index > 0) {
// Simple example signal: crossover of close and SMA
const sma = utils.sma(
function (i) {
return state.prices[i];
},
9,
index
);

if (!utils.na(sma)) {
// Signal when close crosses above SMA
const prevClose = state.prices[index - 1];
if (prevClose < sma && close >= sma) {
return [close]; // Signal at close price
}
}
}

return [NaN];
}
}
}, true);

Common Use Cases:

  • Market hours filtering: Ignore signals outside regular trading hours
  • Session-specific strategies: Different strategies for different session types
  • Session gaps: Identify gaps between sessions
  • Extended hours trading: Only trade during pre-market or after-market

Notes:

  • ctx.sessions returns [] (empty array) if session information is not available from the sessions provider
  • Session times are in milliseconds since epoch; compare with ctx.input.time(0) to determine current candle's session

Events and news data

Custom indicators have access to corporate events and news data for the current instrument by using ctx.events and ctx.news. The data is loaded automatically through Events Data Provider and News Data Provider and available in the context without any additional configuration.

Events

ctx.events is an array of Event objects. Each event has a kind field that determines its type and available properties:

KindFields
'earnings'basic, diluted, timestamp, periodEnding
'dividends'gross, timestamp
'splits'splitFrom, splitTo, timestamp
'conference-calls'referencePeriod, eventType, timestamp
// Access all events for the current instrument
const events = ctx.events;

// Filter by type
const earnings = events.filter(e => e.kind === 'earnings');
const dividends = events.filter(e => e.kind === 'dividends');

// Find events near the current candle
const candleTime = ctx.input.time(0);
const dayMs = 24 * 60 * 60 * 1000;
const nearbyEvents = events.filter(e => Math.abs(e.timestamp - candleTime) < dayMs);

News

ctx.news is an array of News objects with the following fields: title, timestamp, and sourceLink.

// Access all news for the current instrument
const news = ctx.news;

// Find news near the current candle
const candleTime = ctx.input.time(0);
const dayMs = 24 * 60 * 60 * 1000;
const nearbyNews = news.filter(n => Math.abs(n.timestamp - candleTime) < dayMs);

Example: Events and news totals

The lists ctx.events and ctx.news are for the whole symbol, so the simplest plot is two horizontal lines with total counts (length). To count only items near the current candle, use the filters from the sections above.

__CHART_REACT_API.addCustomStudy(
{
name: 'Events and news Counter',
metainfo: {
id: 'events-counter',
title: 'Events and news Counter',
overlaying: true,
parameters: [],
lines: [
{ id: 'events', title: 'Events', type: 'LINEAR', color: '#2196F3', thickness: 2 },
{ id: 'news', title: 'News', type: 'LINEAR', color: '#FF9800', thickness: 2 },
],
categories: 'Custom',
},
constructor: {
main: function (ctx) {
return [ctx.events.length, ctx.news.length];
},
},
},
true,
);
note

Events and news data is fetched when the chart loads and when the instrument changes. The same data is available for every candle in the main() function.

Utility functions

The framework provides built-in utilities for common technical analysis calculations.

tip

For detailed documentation of each function including parameters, examples, and edge cases, see the Utility Functions Reference.

Chaining calculations

You can chain calculations by using one result as input to another:

// Calculate EMA of prices
state.emaValues[index] = utils.ema(
function (i) {
return state.prices[i];
},
9,
index,
'ema1',
);

// Calculate EMA of the EMA (Double EMA) - check for undefined values
state.ema2Values[index] = utils.ema(
function (i) {
const val = state.emaValues[i];
return val !== undefined ? val : NaN;
},
9,
index,
'ema2',
);

Complete examples

Example 1: Triple EMA (TEMA)

__CHART_REACT_API.addCustomStudy({
name: 'Triple Exponential Moving Average',
metainfo: {
id: 'TEMA',
title: 'TEMA',
overlaying: true,
parameters: [
{
id: 'length',
title: 'Length',
type: 'INTEGER_RANGE',
defaultValue: 9,
min: 1,
max: 100,
},
],
lines: [
{
id: 'tema',
title: 'TEMA',
type: 'LINEAR',
color: '#2962FF',
thickness: 2,
},
],
},
constructor: {
main: function (ctx) {
const input = ctx.input;
const utils = ctx.utils;
const parameters = ctx.parameters;
const state = ctx.state;
const index = ctx.index;

// Initialize state arrays if needed
if (!state.prices) {
state.prices = [];
state.ema1Values = [];
state.ema2Values = [];
}

// Get price and store it
const price = input.close(0);
state.prices[index] = price;

// Calculate first EMA
const ema1 = utils.ema(
function (i) {
return state.prices[i];
},
parameters.length,
index,
'ema1',
);
if (utils.na(ema1)) {
return [NaN];
}
state.ema1Values[index] = ema1;

// Calculate second EMA (EMA of EMA)
const ema2 = utils.ema(
function (i) {
const val = state.ema1Values[i];
return val !== undefined ? val : NaN;
},
parameters.length,
index,
'ema2',
);
if (utils.na(ema2)) {
return [NaN];
}
state.ema2Values[index] = ema2;

// Calculate third EMA (EMA of EMA of EMA)
const ema3 = utils.ema(
function (i) {
const val = state.ema2Values[i];
return val !== undefined ? val : NaN;
},
parameters.length,
index,
'ema3',
);
if (utils.na(ema3)) {
return [NaN];
}

// Calculate TEMA: 3 * EMA1 - 3 * EMA2 + EMA3
const tema = 3 * ema1 - 3 * ema2 + ema3;
return [tema];
},
},
});

Example 2: Bollinger Bands

__CHART_REACT_API.addCustomStudy({
name: 'Bollinger Bands',
metainfo: {
id: 'CUSTOM_BB',
title: 'Bollinger Bands',
overlaying: true,
parameters: [
{
id: 'length',
title: 'Length',
type: 'INTEGER_RANGE',
defaultValue: 20,
min: 1,
max: 500,
},
{
id: 'mult',
title: 'Multiplier',
type: 'DOUBLE_RANGE',
defaultValue: 2.0,
min: 0.1,
max: 10.0,
},
],
lines: [
{
id: 'upper',
title: 'Upper Band',
type: 'LINEAR',
color: '#2962FF',
thickness: 1,
},
{
id: 'basis',
title: 'Basis',
type: 'LINEAR',
color: '#FF6D00',
thickness: 2,
},
{
id: 'lower',
title: 'Lower Band',
type: 'LINEAR',
color: '#2962FF',
thickness: 1,
},
],
},
constructor: {
main: function (ctx) {
const input = ctx.input;
const utils = ctx.utils;
const parameters = ctx.parameters;
const state = ctx.state;
const index = ctx.index;

// Initialize state arrays if needed
if (!state.prices) {
state.prices = [];
}

const price = input.close(0);
state.prices[index] = price;

const basis = utils.sma(
function (i) {
return state.prices[i];
},
parameters.length,
index,
);
const dev = utils.stdev(
function (i) {
return state.prices[i];
},
parameters.length,
index,
);

if (utils.na(basis) || utils.na(dev)) {
return [NaN, NaN, NaN];
}

const upper = basis + parameters.mult * dev;
const lower = basis - parameters.mult * dev;

return [upper, basis, lower];
},
},
});

Example 3: RSI (Relative Strength Index)

__CHART_REACT_API.addCustomStudy({
name: 'Relative Strength Index',
metainfo: {
id: 'CUSTOM_RSI',
title: 'RSI',
overlaying: false, // Separate pane
parameters: [
{
id: 'length',
title: 'Length',
type: 'INTEGER_RANGE',
defaultValue: 14,
min: 1,
max: 500,
},
],
lines: [
{
id: 'rsi',
title: 'RSI',
type: 'LINEAR',
color: '#7E57C2',
thickness: 2,
},
],
},
constructor: {
main: function (ctx) {
const input = ctx.input;
const utils = ctx.utils;
const parameters = ctx.parameters;
const state = ctx.state;
const index = ctx.index;

// Initialize state arrays if needed
if (!state.prices) {
state.prices = [];
state.gainValues = [];
state.lossValues = [];
}

const price = input.close(0);
state.prices[index] = price;

if (index === 0) {
state.gainValues[0] = 0;
state.lossValues[0] = 0;
return [50]; // Neutral RSI for first candle
}

const change = price - state.prices[index - 1];
state.gainValues[index] = change > 0 ? change : 0;
state.lossValues[index] = change < 0 ? -change : 0;

// Use RMA for smoothing
const avgGain = utils.rma(
function (i) {
return state.gainValues[i];
},
parameters.length,
index,
'gains',
);
const avgLoss = utils.rma(
function (i) {
return state.lossValues[i];
},
parameters.length,
index,
'losses',
);

if (utils.na(avgGain) || utils.na(avgLoss)) {
return [NaN];
}

if (avgLoss === 0) {
return [100];
}

const rs = avgGain / avgLoss;
const rsi = 100 - 100 / (1 + rs);

return [rsi];
},
},
});

Best practices

State management

  1. Initialize state arrays when needed:

    main: function(ctx) {
    const state = ctx.state;
    const index = ctx.index;

    // Initialize arrays on first use
    if (!state.prices) {
    state.prices = [];
    state.emaValues = [];
    }

    // Your calculations here...
    }
  2. Use unique state keys for utility functions:

    // Good - unique keys prevent collisions
    const ema1 = utils.ema(
    function (i) {
    return prices[i];
    },
    9,
    index,
    'ema_fast',
    );
    const ema2 = utils.ema(
    function (i) {
    return prices[i];
    },
    21,
    index,
    'ema_slow',
    );

    // Bad - same key will cause incorrect results
    const ema1 = utils.ema(
    function (i) {
    return prices[i];
    },
    9,
    index,
    'ema',
    );
    const ema2 = utils.ema(
    function (i) {
    return prices[i];
    },
    21,
    index,
    'ema',
    );
  3. Store values at the current index:

    state.values[index] = calculatedValue;

Performance

  1. Avoid redundant calculations:

    // Good - calculate once, store, reuse with accessor function
    const close = input.close(0);
    state.prices[index] = close;
    const result1 = utils.sma(
    function (i) {
    return state.prices[i];
    },
    20,
    index,
    );
    const result2 = utils.ema(
    function (i) {
    return state.prices[i];
    },
    20,
    index,
    'ema',
    );

    // Bad - recalculating same price multiple times
    const result1 = utils.sma(
    function (i) {
    return input.close(0);
    },
    20,
    index,
    );
    const result2 = utils.ema(
    function (i) {
    return input.close(0);
    },
    20,
    index,
    'ema',
    );
  2. Return early for invalid data:

    if (index < parameters.length - 1) {
    return [NaN];
    }

Error Handling

  1. Always check for N/A values:

    const value = utils.sma(
    function (i) {
    return state.prices[i];
    },
    period,
    index,
    );
    if (utils.na(value)) {
    return [NaN];
    }
  2. Handle edge cases:

    // Initialize state on first use
    if (!state.values) {
    state.values = [];
    }

    // First candle
    if (index === 0) {
    state.values[0] = input.close(0);
    return [NaN];
    }

    // Insufficient data
    if (index < parameters.length - 1) {
    return [NaN];
    }

Naming conventions

  1. Study IDs: Uppercase with underscores

    id: 'MY_CUSTOM_INDICATOR';
  2. Parameter IDs: camelCase

    id: 'fastLength';
  3. Line IDs: camelCase

    id: 'upperBand';
  4. State keys for utils: Descriptive and unique

    ('ema_fast', 'ema_slow', 'rma_gains', 'atr_main');

Removing custom studies

To remove a custom study:

__CHART_REACT_API.removeCustomStudy('MY_CUSTOM_INDICATOR');

Persisting custom studies

By default, custom studies are registered in-memory and lost when the page reloads. To persist custom studies across sessions (save to backend/storage, share across users, etc.), use the CustomStudiesProvider.

For complete documentation including implementation examples, localStorage and backend patterns, and security considerations, see the Custom Studies Provider page.

Limitations and considerations

  1. Calculation order: Studies calculate sequentially from oldest to newest candle
  2. Real-time updates: By default, main() is re-invoked for the last candle on each tick; use recalculation controls to widen this window
  3. State persistence: State is maintained between parameter changes but reset on study removal
  4. Displace is visual only: Plot displacement shifts rendering on the chart; ctx.index and ctx.input offsets in main() are unaffected
  5. DATE/TIME parameters: Values are strings (YYYYMMDD / HHMM); parse them manually in main() when comparing against candle timestamps

Accessing Other Studies

Custom studies cannot directly access data from other studies. If you need to combine multiple studies, create a new custom study that implements both calculations.

Troubleshooting

Study not appearing

  • Ensure the study ID is unique
  • Check that addCustomStudy() is called after the chart is initialized
  • Verify all required fields in metadata are provided

Incorrect calculations

  • Check that state keys for utility functions are unique
  • Verify array indices match the current index parameter
  • Ensure proper state initialization at the beginning of main()

Performance issues

  • Reduce complexity of calculations in the main() function
  • Use built-in utility functions instead of custom loops
  • Consider adding early returns for invalid data

Values not updating on last candle

  • Ensure you're using arrays indexed by index, not tracking "last" values manually
  • Check that state is being updated correctly at state.values[index]