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 CustomStudyContext {index: number;input: {open: (offset?: number) => number;high: (offset?: number) => number;low: (offset?: number) => number;close: (offset?: number) => number;volume: (offset?: number) => number;};parameters: Record<string, unknown>;state: Record<string, unknown>;utils: CustomStudyUtils;}
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 input
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;input: {open: (offset?: number) => number;high: (offset?: number) => number;low: (offset?: number) => number;close: (offset?: number) => number;volume: (offset?: number) => number;};parameters: Record<string, unknown>;state: Record<string, unknown>;utils: CustomStudyUtils;}
Input Data Access
The input object provides access to candle data and additional market information:
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'
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