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 boundsDOUBLE_RANGE- Floating point with min/max boundsBOOLEAN- True/false toggleSTRING- Text inputENUM- 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 linePOINTS- Point markersHISTOGRAM- Vertical bars from zeroTREND_HISTOGRAM- Histogram colored by trendDIFFERENCE- Area between two linesLINEAR_TREND- Line colored by trendTREND_POINTS- Points colored by trendABOVE_CANDLE_TEXT- Text above candlesBELOW_CANDLE_TEXT- Text below candlesCOLOR_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:
- Request the data in the
init()function usingrequestSecondaryData() - Access the data in the
main()function usingsecondaryData()
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 asAggregationPeriodobject 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' }
- 1-minute:
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 inmain() - 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.sessionsreturns[](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:
| Kind | Fields |
|---|---|
'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,
);
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.
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
-
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...
} -
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',
); -
Store values at the current index:
state.values[index] = calculatedValue;
Performance
-
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',
); -
Return early for invalid data:
if (index < parameters.length - 1) {
return [NaN];
}
Error Handling
-
Always check for N/A values:
const value = utils.sma(
function (i) {
return state.prices[i];
},
period,
index,
);
if (utils.na(value)) {
return [NaN];
} -
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
-
Study IDs: Uppercase with underscores
id: 'MY_CUSTOM_INDICATOR'; -
Parameter IDs: camelCase
id: 'fastLength'; -
Line IDs: camelCase
id: 'upperBand'; -
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
- Calculation order: Studies calculate sequentially from oldest to newest candle
- Real-time updates: The
main()function is called for each tick on the last candle - 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
indexparameter - 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]
Read next
- Custom Studies Provider - Persist studies to backend/storage
- Utility Functions Reference - Detailed function documentation
- Quick Reference Guide - Cheat sheet