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)
  • 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

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

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: The main() function is called for each tick on the last candle
  3. State persistence: State is maintained between parameter changes but reset on study removal

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]