Custom Datafeed Implementation Guide
This guide shows you how to implement a custom datafeed to connect the GoCharting SDK to your own data source.
Overview
A datafeed is a plain JavaScript object (no class inheritance required) that implements the Datafeed interface and is passed as datafeed to createChart() or the GoCharting component. It has 2 required methods and 5 optional methods:
Required:
getBars()- Fetch historical bar dataresolveSymbol()- Resolve symbol information
Optional:
searchSymbols()- Enable symbol searchsubscribeTicks()- Provide real-time updatesunsubscribeTicks()- Cancel real-time subscriptionsgetMarks()- Display marks/events on chartgetTimescaleMarks()- Display timescale marks
const myDatafeed = {
// Required
async getBars(symbolInfo, resolution, periodParams) { /* ... */ },
resolveSymbol(symbolName, onResolve, onError) { /* ... */ },
// Optional
searchSymbols(userInput, exchange, symbolType, onResultReadyCallback) { /* ... */ },
subscribeTicks(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) { /* ... */ },
unsubscribeTicks(subscriberUID) { /* ... */ },
getMarks(symbolInfo, startDate, endDate, onDataCallback, resolution) { /* ... */ },
getTimescaleMarks(symbolInfo, from, to, onDataCallback, resolution) { /* ... */ },
};See the Datafeed API for complete interface documentation.
Quick Start
Minimal Datafeed
Here’s the simplest possible datafeed implementation:
const myDatafeed = {
// Required: Fetch historical bars
async getBars(symbolInfo, resolution, periodParams) {
const { from, to } = periodParams;
// Fetch from your API
const response = await fetch(
`/api/bars?symbol=${symbolInfo.symbol}&from=${from.getTime()}&to=${to.getTime()}`
);
const data = await response.json();
// Return in BarsResult format
return {
bars: data.map(bar => ({
time: bar.timestamp,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
})),
};
},
// Required: Resolve symbol information
resolveSymbol(symbolName, onResolve, onError) {
const parts = String(symbolName).split(":");
const exchange = parts[0] || "NASDAQ";
const segment = parts.length >= 3 ? parts[1] : "EQUITY";
const symbol = parts[parts.length - 1] || "AAPL";
const name = symbol;
const validIntervals = ["1m", "5m", "15m", "1h", "1D"];
onResolve({
exchange,
segment,
symbol,
name,
asset_type: "EQUITY",
source_id: symbol,
tradeable: true,
is_index: false,
is_formula: false,
delay_seconds: 0,
data_status: "streaming",
contract_size: 1,
tick_size: 0.01,
display_tick_size: 0.01,
volume_size_increment: 1,
max_tick_precision: 2,
max_volume_precision: 0,
quote_currency: "USD",
supports: { footprint: false },
exchange_info: {
name: exchange.toLowerCase(),
code: exchange,
country_cd: "US",
zone: "America/New_York",
hours: [
{ open: false },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: false },
],
valid_intervals: validIntervals,
},
ticker: symbol,
full_name: `${exchange}:${segment}:${symbol}`,
description: name,
type: "stock",
session: "0930-1600",
timezone: "America/New_York",
has_intraday: true,
has_daily: true,
supported_resolutions: validIntervals,
});
},
};Using Your Datafeed
import { createChart } from "@gocharting/chart-sdk";
const chart = createChart("#chart", {
symbol: "AAPL",
interval: "1D",
datafeed: myDatafeed, // Your custom datafeed
licenseKey: "YOUR_LICENSE_KEY",
});Complete Implementation Examples
Example 1: REST API Datafeed
class RestAPIDatafeed {
constructor(apiBaseUrl) {
this.apiBaseUrl = apiBaseUrl;
}
async getBars(symbolInfo, resolution, periodParams) {
const { from, to, firstDataRequest } = periodParams;
try {
const response = await fetch(
`${this.apiBaseUrl}/bars?` +
`symbol=${symbolInfo.symbol}&` +
`interval=${resolution}&` +
`from=${from.getTime()}&` +
`to=${to.getTime()}`
);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
return {
bars: data.bars.map(bar => ({
time: bar.time,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
})),
meta: {
noData: data.bars.length === 0,
},
};
} catch (error) {
console.error("getBars error:", error);
return { bars: [], meta: { noData: true } };
}
}
resolveSymbol(symbolName, onResolve, onError) {
fetch(`${this.apiBaseUrl}/symbols/${symbolName}`)
.then(response => response.json())
.then(data => {
onResolve({
...data, // full GoCharting security from API
ticker: data.ticker || data.symbol,
full_name:
data.full_name ||
`${data.exchange}:${data.segment}:${data.symbol}`,
description: data.description || data.name,
type: (data.type || (data.asset_type || "EQUITY").toLowerCase()),
});
})
.catch(error => {
console.error("resolveSymbol error:", error);
onError("Symbol not found");
});
}
// Optional: Enable symbol search
searchSymbols(userInput, exchange, symbolType, onResultReadyCallback) {
fetch(
`${this.apiBaseUrl}/search?` +
`query=${encodeURIComponent(userInput)}&` +
`exchange=${exchange}&` +
`type=${symbolType}`
)
.then(response => response.json())
.then(data => {
const results = data.map(item => ({
symbol: item.symbol,
full_name: item.full_name,
description: item.description,
exchange: item.exchange,
type: item.type,
}));
onResultReadyCallback({ searchInProgress: false, items: results });
})
.catch(error => {
console.error("searchSymbols error:", error);
onResultReadyCallback({ searchInProgress: false, items: [] });
});
}
}
// Usage
const datafeed = new RestAPIDatafeed("https://api.example.com");
const chart = createChart("#chart", {
symbol: "AAPL",
interval: "1D",
datafeed: datafeed,
licenseKey: "YOUR_LICENSE_KEY",
});Example 2: WebSocket Datafeed with Real-time Updates
class WebSocketDatafeed {
constructor(apiBaseUrl, wsUrl) {
this.apiBaseUrl = apiBaseUrl;
this.wsUrl = wsUrl;
this.ws = null;
this.subscribers = new Map(); // subscriberUID -> callback
}
async getBars(symbolInfo, resolution, periodParams) {
const { from, to } = periodParams;
const response = await fetch(
`${this.apiBaseUrl}/bars?` +
`symbol=${symbolInfo.symbol}&` +
`interval=${resolution}&` +
`from=${from.getTime()}&` +
`to=${to.getTime()}`
);
const data = await response.json();
return {
bars: data.map(bar => ({
time: bar.time,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
})),
};
}
resolveSymbol(symbolName, onResolve, onError) {
const parts = String(symbolName).split(":");
const exchange = parts[0] || "BYBIT";
const segment = parts.length >= 3 ? parts[1] : "FUTURE";
const symbol = parts[parts.length - 1] || "BTCUSDT";
const validIntervals = ["1m", "5m", "15m", "1h", "1D"];
onResolve({
exchange,
segment,
symbol,
name: "BTC / USDT PERPETUAL FUTURES",
asset_type: "CRYPTO",
source_id: symbol,
pair: { from: "BTC", to: "USDT" },
tradeable: true,
is_index: false,
is_formula: false,
delay_seconds: 0,
data_status: "streaming",
contract_size: 1,
tick_size: 0.1,
display_tick_size: 1,
volume_size_increment: 0.001,
max_tick_precision: 1,
max_volume_precision: 3,
quote_currency: "USDT",
future_type: "PERP",
supports: { footprint: true },
exchange_info: {
name: exchange.toLowerCase(),
code: exchange,
zone: "UTC",
hours: Array.from({ length: 7 }, () => ({ open: true })),
valid_intervals: validIntervals,
},
ticker: symbol,
full_name: `${exchange}:${segment}:${symbol}`,
description: "BTC / USDT PERPETUAL FUTURES",
type: "crypto",
session: "24x7",
timezone: "UTC",
has_intraday: true,
has_daily: true,
supported_resolutions: validIntervals,
});
}
// Optional: Subscribe to real-time updates
subscribeTicks(symbolInfo, resolution, onRealtimeCallback, subscriberUID, onResetCacheNeededCallback) {
// Store callback
this.subscribers.set(subscriberUID, onRealtimeCallback);
// Connect WebSocket if not already connected
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
this.ws = new WebSocket(this.wsUrl);
this.ws.onopen = () => {
console.log("WebSocket connected");
// Subscribe to symbol
this.ws.send(JSON.stringify({
type: "subscribe",
symbol: symbolInfo.symbol,
interval: resolution,
}));
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Send update to all subscribers
this.subscribers.forEach(callback => {
callback({
time: data.time,
open: data.open,
high: data.high,
low: data.low,
close: data.close,
volume: data.volume,
});
});
};
this.ws.onerror = (error) => {
console.error("WebSocket error:", error);
};
this.ws.onclose = () => {
console.log("WebSocket disconnected");
};
}
}
// Optional: Unsubscribe from real-time updates
unsubscribeTicks(subscriberUID) {
this.subscribers.delete(subscriberUID);
// Close WebSocket if no more subscribers
if (this.subscribers.size === 0 && this.ws) {
this.ws.close();
this.ws = null;
}
}
// Optional: Cleanup
destroy() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
this.subscribers.clear();
}
}
// Usage
const datafeed = new WebSocketDatafeed(
"https://api.example.com",
"wss://ws.example.com"
);
const chart = createChart("#chart", {
symbol: "BTCUSDT",
interval: "1m",
datafeed: datafeed,
licenseKey: "YOUR_LICENSE_KEY",
});Example 3: Cached Datafeed
class CachedDatafeed {
constructor(apiBaseUrl) {
this.apiBaseUrl = apiBaseUrl;
this.cache = new Map(); // symbol+interval -> bars
}
getCacheKey(symbol, interval) {
return `${symbol}_${interval}`;
}
async getBars(symbolInfo, resolution, periodParams) {
const { from, to, firstDataRequest } = periodParams;
const cacheKey = this.getCacheKey(symbolInfo.symbol, resolution);
// Check cache for first request
if (firstDataRequest && this.cache.has(cacheKey)) {
const cachedBars = this.cache.get(cacheKey);
const filteredBars = cachedBars.filter(
bar => bar.time >= from.getTime() && bar.time <= to.getTime()
);
if (filteredBars.length > 0) {
console.log("Using cached data");
return { bars: filteredBars };
}
}
// Fetch from API
const response = await fetch(
`${this.apiBaseUrl}/bars?` +
`symbol=${symbolInfo.symbol}&` +
`interval=${resolution}&` +
`from=${from.getTime()}&` +
`to=${to.getTime()}`
);
const data = await response.json();
const bars = data.map(bar => ({
time: bar.time,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
}));
// Update cache
if (firstDataRequest) {
this.cache.set(cacheKey, bars);
}
return { bars };
}
resolveSymbol(symbolName, onResolve, onError) {
const parts = String(symbolName).split(":");
const exchange = parts[0] || "NASDAQ";
const segment = parts.length >= 3 ? parts[1] : "EQUITY";
const symbol = parts[parts.length - 1] || "AAPL";
const name = symbol;
const validIntervals = ["1m", "5m", "15m", "1h", "1D"];
onResolve({
exchange,
segment,
symbol,
name,
asset_type: "EQUITY",
source_id: symbol,
tradeable: true,
is_index: false,
is_formula: false,
delay_seconds: 0,
data_status: "streaming",
contract_size: 1,
tick_size: 0.01,
display_tick_size: 0.01,
volume_size_increment: 1,
max_tick_precision: 2,
max_volume_precision: 0,
quote_currency: "USD",
supports: { footprint: false },
exchange_info: {
name: exchange.toLowerCase(),
code: exchange,
country_cd: "US",
zone: "America/New_York",
hours: [
{ open: false },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: false },
],
valid_intervals: validIntervals,
},
ticker: symbol,
full_name: `${exchange}:${segment}:${symbol}`,
description: name,
type: "stock",
session: "0930-1600",
timezone: "America/New_York",
has_intraday: true,
has_daily: true,
supported_resolutions: validIntervals,
});
}
// Clear cache when needed
clearCache() {
this.cache.clear();
}
}
// Usage
const datafeed = new CachedDatafeed("https://api.example.com");
const chart = createChart("#chart", {
symbol: "AAPL",
interval: "1D",
datafeed: datafeed,
licenseKey: "YOUR_LICENSE_KEY",
});Implementation Tips
1. Error Handling
Always handle errors gracefully in your datafeed methods:
async getBars(symbolInfo, resolution, periodParams) {
try {
const response = await fetch(/* ... */);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
return { bars: data };
} catch (error) {
console.error("getBars error:", error);
// Return empty bars with noData flag
return { bars: [], meta: { noData: true } };
}
}2. Resolution Conversion
Convert SDK resolution format to your API format:
function convertResolution(resolution) {
// SDK uses: "1m", "5m", "15m", "1h", "1D", etc.
// Your API might use: "1", "5", "15", "60", "D", etc.
if (typeof resolution === "string") {
if (resolution.endsWith("m")) {
return resolution.slice(0, -1); // "1m" -> "1"
}
if (resolution.endsWith("h")) {
return String(parseInt(resolution) * 60); // "1h" -> "60"
}
if (resolution.endsWith("D")) {
return "D"; // "1D" -> "D"
}
}
return resolution;
}3. Timestamp Handling
Ensure timestamps are in the correct format:
// SDK expects timestamps in milliseconds
const bars = data.map(bar => ({
time: bar.timestamp * 1000, // Convert seconds to milliseconds
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
volume: bar.volume,
}));4. Symbol Information
Provide accurate symbol information:
resolveSymbol(symbolName, onResolve, onError) {
const parts = String(symbolName).split(":");
const exchange = parts[0] || "NASDAQ";
const segment = parts.length >= 3 ? parts[1] : "EQUITY";
const symbol = parts[parts.length - 1] || "AAPL";
const name = symbol;
const validIntervals = ["1m", "5m", "15m", "1h", "1D"];
onResolve({
exchange,
segment,
symbol,
name,
asset_type: "EQUITY",
source_id: symbol,
tradeable: true,
is_index: false,
is_formula: false,
delay_seconds: 0,
data_status: "streaming",
contract_size: 1,
tick_size: 0.01,
display_tick_size: 0.01,
volume_size_increment: 1,
max_tick_precision: 2,
max_volume_precision: 0,
quote_currency: "USD",
supports: { footprint: false },
exchange_info: {
name: exchange.toLowerCase(),
code: exchange,
country_cd: "US",
zone: "America/New_York",
hours: [
{ open: false },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: false },
],
valid_intervals: validIntervals,
},
ticker: symbol,
full_name: `${exchange}:${segment}:${symbol}`,
description: name,
type: "stock",
session: "0930-1600",
timezone: "America/New_York",
has_intraday: true,
has_daily: true,
supported_resolutions: validIntervals,
});
}Datafeed Checklist
Required Methods
-
getBars()- Fetch historical bars -
Handle
fromandtodates correctly -
Return bars in correct format (BarsResult or UDF)
-
Handle errors gracefully
-
Return
noData: truewhen no data available -
resolveSymbol()- Resolve symbol info -
Call
onResolve()with symbol information -
Call
onError()if symbol not found -
Provide accurate
max_tick_precision(price scale is derived as10^max_tick_precision) -
List all
supported_resolutions
Optional Methods (Recommended)
-
searchSymbols()- Enable symbol search -
Return array of search results
-
Include
symbol,full_name,description,exchange,type -
subscribeTicks()- Real-time updates -
Connect to WebSocket or polling mechanism
-
Call
onRealtimeCallback()with new bars/ticks -
Handle connection errors
-
unsubscribeTicks()- Cancel subscriptions -
Clean up WebSocket connections
-
Remove event listeners
Related Documentation
- Datafeed API - Complete Datafeed interface reference
- Helper Functions - Utility functions like
getDateRangeForDuration() - TypeScript Types - Type definitions for Bar, SymbolInfo, etc.
- Examples - Working code examples
For complete working examples, see the Examples section.