Custom Studies Provider
The Custom Studies Provider enables persistence, sharing, and management of custom study definitions across sessions and users.
Overview
By default, custom studies are registered in-memory using __CHART_REACT_API.addCustomStudy() and are lost when the page reloads. The CustomStudiesProvider allows you to:
- Persist study definitions to localStorage or backend
- Load studies automatically when the chart initializes
- Share studies across users or sessions
- Version control study definitions
- Manage studies programmatically (CRUD operations)
Understanding CustomStudyDTO
The provider works with CustomStudyDTO objects, which represent serialized custom studies:
interface CustomStudyDTO {
name: string; // Study name for registration
metainfo: {
id: string; // Unique identifier
title: string; // Display name
shortTitle?: string; // Short display name (optional)
description?: string; // Description (optional)
overlaying?: boolean; // true = overlay on price, false = separate pane
categories?: string; // Category for grouping (optional)
parameters: CustomStudyParameter[];
lines: CustomStudyLine[];
};
calculationCode: {
init?: string; // Serialized init function (optional)
main: string; // Serialized main function (required)
};
}
Important: The calculationCode functions are stored as strings (serialized code), not as actual functions.
Implementation examples
Example 1: LocalStorage Provider
Simple implementation using browser localStorage:
import { CustomStudiesProvider } from '@dx-private/dxchart5-react/dist/providers/custom-studies-provider';
import { CustomStudyDTO } from '@dx-private/dxchart5-react/dist/chart/model/custom-js-studies.model';
const STORAGE_KEY = 'custom_studies';
export const localStorageCustomStudiesProvider: CustomStudiesProvider = {
async getAllStudies(): Promise<CustomStudyDTO[]> {
const stored = localStorage.getItem(STORAGE_KEY);
if (!stored) {
return [];
}
try {
return JSON.parse(stored);
} catch (error) {
console.error('Failed to parse custom studies:', error);
return [];
}
},
async getStudy(id: string): Promise<CustomStudyDTO | undefined> {
const studies = await this.getAllStudies();
return studies.find(s => s.metainfo.id === id);
},
async createStudy(study: CustomStudyDTO): Promise<void> {
const studies = await this.getAllStudies();
// Check for duplicate ID
if (studies.some(s => s.metainfo.id === study.metainfo.id)) {
throw new Error(`Study with ID '${study.metainfo.id}' already exists`);
}
studies.push(study);
localStorage.setItem(STORAGE_KEY, JSON.stringify(studies));
},
async updateStudy(study: CustomStudyDTO): Promise<void> {
const studies = await this.getAllStudies();
const index = studies.findIndex(s => s.metainfo.id === study.metainfo.id);
if (index === -1) {
throw new Error(`Study with ID '${study.metainfo.id}' not found`);
}
studies[index] = study;
localStorage.setItem(STORAGE_KEY, JSON.stringify(studies));
},
async deleteStudy(id: string): Promise<void> {
const studies = await this.getAllStudies();
const filtered = studies.filter(s => s.metainfo.id !== id);
localStorage.setItem(STORAGE_KEY, JSON.stringify(filtered));
}
};
Example 2: Backend API Provider
Implementation with a REST API backend:
import { CustomStudiesProvider } from '@dx-private/dxchart5-react/dist/providers/custom-studies-provider';
import { CustomStudyDTO } from '@dx-private/dxchart5-react/dist/chart/model/custom-js-studies.model';
const API_BASE_URL = 'https://api.example.com/custom-studies';
export const backendCustomStudiesProvider: CustomStudiesProvider = {
async getAllStudies(): Promise<CustomStudyDTO[]> {
const response = await fetch(API_BASE_URL, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
},
});
if (!response.ok) {
throw new Error('Failed to fetch custom studies');
}
return response.json();
},
async getStudy(id: string): Promise<CustomStudyDTO | undefined> {
const response = await fetch(`${API_BASE_URL}/${id}`, {
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
},
});
if (response.status === 404) {
return undefined;
}
if (!response.ok) {
throw new Error('Failed to fetch study');
}
return response.json();
},
async createStudy(study: CustomStudyDTO): Promise<void> {
const response = await fetch(API_BASE_URL, {
method: 'POST',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(study),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Failed to create study');
}
},
async updateStudy(study: CustomStudyDTO): Promise<void> {
const response = await fetch(`${API_BASE_URL}/${study.metainfo.id}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(study),
});
if (!response.ok) {
throw new Error('Failed to update study');
}
},
async deleteStudy(id: string): Promise<void> {
const response = await fetch(`${API_BASE_URL}/${id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${getAuthToken()}`,
},
});
if (!response.ok) {
throw new Error('Failed to delete study');
}
}
};
function getAuthToken(): string {
// Return your authentication token
return sessionStorage.getItem('auth_token') || '';
}
Usage in chart configuration
Provide the custom studies provider to the chart component:
import { ChartReactApp } from '@dx-private/dxchart5-react/dist/chart/chart-react-app';
import { localStorageCustomStudiesProvider } from './custom-studies-provider';
function MyChart() {
return (
<ChartReactApp
dependencies={{
customStudiesProvider: localStorageCustomStudiesProvider,
// ... other dependencies
}}
// ... other props
/>
);
}
Working with studies
Loading studies
Studies are automatically loaded and deserialized when the chart initializes if a provider is configured. The chart will:
- Call
getAllStudies()to fetch all study definitions - Deserialize the function strings using
new Function('return ' + code)() - Register each study internally
You don't need to manually deserialize when using the provider - the chart handles this automatically.
Saving a new study
To save a study after creating it programmatically:
const studyDTO: CustomStudyDTO = {
name: 'My Custom Indicator',
metainfo: {
id: 'MY_CUSTOM_INDICATOR',
title: 'My Custom Indicator',
overlaying: true,
parameters: [
{
id: 'length',
title: 'Length',
type: 'INTEGER_RANGE',
defaultValue: 14,
min: 1,
max: 200,
}
],
lines: [
{
id: 'output',
title: 'Output',
type: 'LINEAR',
color: '#2962FF',
thickness: 2,
}
]
},
calculationCode: {
main: `(ctx) => {
const { input, utils, parameters, state, index } = ctx;
// Initialize state array if needed
if (!state.prices) {
state.prices = [];
}
const price = input.close(0);
state.prices[index] = price;
const sma = utils.sma(function(i) {
return state.prices[i];
}, parameters.length, index);
return [sma];
}`
}
};
// Save via provider
await customStudiesProvider.createStudy(studyDTO);
Note: The study will be automatically loaded and available after reloading the page. For immediate registration without reload, see the Deserialization section below.
Updating a study
// Get the existing study
const existingStudy = await customStudiesProvider.getStudy('MY_CUSTOM_INDICATOR');
if (existingStudy) {
const updatedStudy: CustomStudyDTO = {
...existingStudy,
metainfo: {
...existingStudy.metainfo,
title: 'Updated Title',
}
};
await customStudiesProvider.updateStudy(updatedStudy);
}
Important: After updating a study in the provider, changes will only take effect after reloading the page. For immediate updates without reload, you need to manually remove and re-register the study (see Deserialization section).
Deleting a Study
// Delete from provider storage
await customStudiesProvider.deleteStudy('MY_CUSTOM_INDICATOR');
// Optional: Remove immediately from the running chart
__CHART_REACT_API.removeCustomStudy('MY_CUSTOM_INDICATOR');
Note: Deleting from the provider removes it from storage. The study will be gone after reloading the page. To remove it immediately from the current chart session, also call removeCustomStudy().
Code serialization
Functions must be serialized as strings for storage. There are two approaches:
Approach 1: Function.toString()
const mainFunction = (ctx) => {
const { input, state, index } = ctx;
state.prices[index] = input.close(0);
return [state.prices[index]];
};
const studyDTO: CustomStudyDTO = {
name: 'My Study',
metainfo: { /* ... */ },
calculationCode: {
main: mainFunction.toString()
}
};
Approach 2: String templates
const studyDTO: CustomStudyDTO = {
name: 'My Study',
metainfo: { /* ... */ },
calculationCode: {
main: `(ctx) => {
const { input, utils, parameters, state, index } = ctx;
state.prices[index] = input.close(0);
const result = utils.sma(function(i) {
return state.prices[i];
}, parameters.length, index);
return [result];
}`
}
};
Deserialization
The chart automatically deserializes studies from the provider during initialization. You typically don't need to manually deserialize.
Manual deserialization is only needed for immediate registration without reloading:
// Get the study DTO from provider
const studyDTO = await customStudiesProvider.getStudy('MY_CUSTOM_INDICATOR');
if (studyDTO) {
// Deserialize the main function string
const mainFn = new Function('return ' + studyDTO.calculationCode.main)();
// Deserialize init function if it exists (optional)
const initFn = studyDTO.calculationCode.init
? new Function('return ' + studyDTO.calculationCode.init)()
: undefined;
// Register immediately in the running chart
__CHART_REACT_API.addCustomStudy({
name: studyDTO.name,
metainfo: studyDTO.metainfo,
constructor: {
...(initFn && { init: initFn }), // Only include if exists
main: mainFn,
}
});
}
Note: The chart uses new Function() internally for automatic deserialization. This example is only needed for immediate registration without page reload.
Security considerations (Backend/shared storage only)
This section only applies if you're storing custom studies on a backend where multiple users can share studies.
Possible risks
Custom studies contain JavaScript code that executes automatically when the chart loads. If your backend accepts studies from untrusted users, malicious code could be stored and executed by other users.
For localStorage-only implementations (Example 1), there is no cross-user security risk.
Recommendations for backend storage
- Implement access controls - Only allow trusted/authenticated users to create studies
- Use per-user storage - Store studies per user account, not globally
- Require approval - Implement a review process before studies can be shared
- Track ownership - Always store who created each study for auditing
Example: Backend with access control
// Only allow authenticated users to create studies
app.post('/api/custom-studies', authenticate, async (req, res) => {
// Validate structure
if (!req.body?.name || !req.body?.calculationCode?.main) {
return res.status(400).json({ error: 'Invalid study structure' });
}
// Store with user ownership
const study = {
...req.body,
createdBy: req.user.id,
createdAt: new Date()
};
await db.customStudies.insert(study);
res.status(201).json({ message: 'Study created' });
});
// Users can only load their own studies
app.get('/api/custom-studies', authenticate, async (req, res) => {
const studies = await db.customStudies.find({
createdBy: req.user.id
});
res.json(studies);
});
Implementation best practices
- Validate structure: Check required fields before saving
- Error handling: Wrap provider calls in try-catch blocks
- Caching: Cache loaded studies to reduce API calls
- Offline support: Implement fallback to localStorage when backend is unavailable
- Audit logging: Track who creates/modifies studies for accountability
- Testing: Test study functions before saving to avoid runtime errors
API reference
CustomStudiesProvider
Provider for persisting and managing custom study definitions
- CustomStudiesProvider.getAllStudies
- Returns
- Promise<CustomStudyDTO[]>
CustomStudiesProvider.getAllStudies(): Promise<CustomStudyDTO[]>
Fetches all studies from backend
- CustomStudiesProvider.getStudy
- Parameters
- id: string
- - unique study identifier
- Returns
- Promise<CustomStudyDTO>
CustomStudiesProvider.getStudy(id: string): Promise<CustomStudyDTO>
Gets a single study data by ID
- CustomStudiesProvider.createStudy
- Parameters
- study: CustomStudyDTO
- - study DTO to create
- Returns
- Promise<void>
CustomStudiesProvider.createStudy(study: CustomStudyDTO): Promise<void>
Creates a new study
- CustomStudiesProvider.updateStudy
- Parameters
- study: CustomStudyDTO
- - study DTO to update
- Returns
- Promise<void>
CustomStudiesProvider.updateStudy(study: CustomStudyDTO): Promise<void>
Updates an existing study
- CustomStudiesProvider.deleteStudy
- Parameters
- id: string
- - unique study identifier to delete
- Returns
- Promise<void>
CustomStudiesProvider.deleteStudy(id: string): Promise<void>
Deletes a study by ID