π 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 an object that implements the Datafeed interface. 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 marksdestroy()- Cleanup resources
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) {
onResolve({
symbol: symbolName,
name: symbolName,
type: "stock",
session: "24x7",
timezone: "Etc/UTC",
minmov: 1,
pricescale: 100,
has_intraday: true,
supported_resolutions: ["1m", "5m", "15m", "1h", "1D"],
});
},
};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({
symbol: data.symbol,
name: data.name,
type: data.type || "stock",
session: data.session || "24x7",
timezone: data.timezone || "Etc/UTC",
minmov: data.minmov || 1,
pricescale: data.pricescale || 100,
has_intraday: data.has_intraday !== false,
supported_resolutions: data.supported_resolutions || ["1D"],
});
})
.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(results);
})
.catch(error => {
console.error("searchSymbols error:", error);
onResultReadyCallback([]);
});
}
}
// 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) {
onResolve({
symbol: symbolName,
name: symbolName,
type: "crypto",
session: "24x7",
timezone: "Etc/UTC",
minmov: 1,
pricescale: 100,
has_intraday: true,
supported_resolutions: ["1m", "5m", "15m", "1h", "1D"],
});
}
// 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) {
onResolve({
symbol: symbolName,
name: symbolName,
type: "stock",
session: "24x7",
timezone: "Etc/UTC",
minmov: 1,
pricescale: 100,
has_intraday: true,
supported_resolutions: ["1m", "5m", "15m", "1h", "1D"],
});
}
// 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) {
onResolve({
symbol: symbolName,
name: symbolName,
type: "stock", // "stock", "crypto", "forex", "futures", etc.
session: "24x7", // Trading hours: "24x7", "0930-1600", etc.
timezone: "America/New_York", // IANA timezone
minmov: 1, // Minimum price movement
pricescale: 100, // Price scale (100 = 2 decimals, 10000 = 4 decimals)
has_intraday: true, // Supports intraday data
supported_resolutions: ["1m", "5m", "15m", "1h", "1D"], // Available intervals
});
}π 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
- Handle
-
β
resolveSymbol()- Resolve symbol info- Call
onResolve()with symbol information - Call
onError()if symbol not found - Provide accurate
pricescaleandminmov - List all
supported_resolutions
- Call
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
-
β
destroy()- Cleanup- Close WebSocket connections
- Cancel pending requests
- Clear caches
π 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.