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 an object that implements the Datafeed interface. 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
  • destroy() - 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 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 pricescale and minmov
    • 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
  • ⭐ destroy() - Cleanup

    • Close WebSocket connections
    • Cancel pending requests
    • Clear caches

For complete working examples, see the Examples section.

Last updated on