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 boundsDOUBLE_RANGE- Floating point with min/max boundsBOOLEAN- True/false toggleSTRING- Text inputENUM- Custom enumeration with predefined optionsCOLOR- Color picker (hex string, e.g.'#2962FF')DATE- Calendar date input (stored asYYYYMMDD)TIME- Time-of-day input (stored asHHMM, 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 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 candlesUNIVERSAL_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:
- 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 useif (!state.prices) {state.prices = [];state.emaValues = [];}// Your calculations here...} -
Use unique state keys for utility functions:
// Good - unique keys prevent collisionsconst 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 resultsconst 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 functionconst 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 timesconst 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 useif (!state.values) {state.values = [];}// First candleif (index === 0) {state.values[0] = input.close(0);return [NaN];}// Insufficient dataif (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: By default,
main()is re-invoked for the last candle on each tick; use recalculation controls to widen this window - State persistence: State is maintained between parameter changes but reset on study removal
- Displace is visual only: Plot displacement shifts rendering on the chart;
ctx.indexandctx.inputoffsets inmain()are unaffected - DATE/TIME parameters: Values are strings (
YYYYMMDD/HHMM); parse them manually inmain()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
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