Skip to Content
API ReferenceCustom Feed

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 data
  • resolveSymbol() - Resolve symbol information

Optional:

  • searchSymbols() - Enable symbol search
  • subscribeTicks() - Provide real-time updates
  • unsubscribeTicks() - Cancel real-time subscriptions
  • getMarks() - Display marks/events on chart
  • getTimescaleMarks() - 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 from and to dates correctly

  • Return bars in correct format (BarsResult or UDF)

  • Handle errors gracefully

  • Return noData: true when 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 as 10^max_tick_precision)

  • List all supported_resolutions

  • 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


For complete working examples, see the Examples section.

Last updated on