Initializing...
```
## Step 2: Create Custom Datafeed
Create `src/datafeed.js`:
```javascript
// Create a simple datafeed object (no inheritance needed)
export const myDatafeed = {
initialized: false,
cache: new Map(),
// Optional initialization method
async _init(datacenter, token) {
this.datacenter = datacenter;
this.token = token;
this.initialized = true;
console.log(" Datafeed initialized");
},
async getBars(symbolInfo, resolution, periodParams) {
const { from, to, firstDataRequest } = periodParams;
console.log(
` Fetching bars for ${symbolInfo.name}, resolution: ${resolution}`
);
try {
// Check cache first
const cacheKey = `${symbolInfo.name}-${resolution}-${from}-${to}`;
if (this.cache.has(cacheKey)) {
console.log(" Returning cached data");
return this.cache.get(cacheKey);
}
// For demo purposes, we'll generate sample data
// In production, replace this with your actual API call
const bars = this.generateSampleData(
symbolInfo,
from,
to,
resolution
);
const result = {
bars: bars,
meta: {
noData: bars.length === 0,
},
};
// Cache the result
this.cache.set(cacheKey, result);
console.log(` Fetched ${bars.length} bars`);
return result;
} catch (error) {
console.error(" Error fetching bars:", error);
return {
bars: [],
meta: { noData: true },
};
}
}
async resolveSymbol(symbolName, onResolve, onError) {
console.log(`Resolving symbol: ${symbolName}`);
try {
// "NASDAQ:AAPL" or "BYBIT:FUTURE:BTCUSDT"
const parts = symbolName.split(":");
const exchange = parts[0] || "NASDAQ";
const symbol = parts[parts.length - 1];
const segment =
parts.length >= 3 ? parts[1] : this.getDefaultSegment(exchange);
const name = this.getSymbolDescription(symbol);
const assetType = this.getAssetType(exchange);
const zone = this.getTimezone(exchange);
const isCrypto = assetType === "CRYPTO";
const validIntervals = isCrypto
? ["1m", "5m", "15m", "30m", "1h", "4h", "1D", "1W", "1M"]
: ["1m", "5m", "15m", "30m", "1h", "1D", "1W", "1M"];
// GoCharting security / SymbolInfo shape
const symbolInfo = {
exchange,
segment,
symbol,
name,
asset_type: assetType,
source_id: symbol,
pair: isCrypto
? { from: symbol.replace(/USDT$|USD$/i, ""), to: "USDT" }
: undefined,
tradeable: true,
is_index: false,
is_formula: false,
delay_seconds: 0,
data_status: "streaming",
industry: "technology",
symbol_logo_urls: [],
contract_size: 1,
tick_size: isCrypto ? 0.1 : 0.01,
display_tick_size: isCrypto ? 1 : 0.01,
volume_size_increment: isCrypto ? 0.001 : 1,
max_tick_precision: isCrypto ? 1 : 2,
max_volume_precision: isCrypto ? 3 : 0,
quote_currency: isCrypto ? "USDT" : "USD",
future_type: segment === "FUTURE" ? "PERP" : undefined,
supports: { footprint: isCrypto },
exchange_info: {
name: exchange.toLowerCase(),
code: exchange,
country_cd: "US",
zone,
has_unique_trade_id: true,
logo_url: undefined,
holidays: null,
hours: isCrypto
? Array.from({ length: 7 }, () => ({ open: true }))
: [
{ open: false }, // Sunday
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: false }, // Saturday
],
contains_ambiguous_symbols: false,
valid_intervals: validIntervals,
},
// Compatibility fields
ticker: symbol,
full_name: `${exchange}:${segment}:${symbol}`,
description: name,
type: this.getSymbolType(exchange),
session: this.getSessionHours(exchange),
timezone: zone,
has_intraday: true,
has_daily: true,
supported_resolutions: validIntervals,
};
console.log("Symbol resolved:", symbolInfo);
onResolve(symbolInfo);
} catch (error) {
console.error("Error resolving symbol:", error);
onError("Symbol not found");
}
}
async searchSymbols(userInput, callback) {
console.log(`Searching symbols: ${userInput}`);
// Sample symbols for demo
const allSymbols = [
{
symbol: "AAPL",
full_name: "NASDAQ:AAPL",
description: "Apple Inc.",
exchange: "NASDAQ",
ticker: "AAPL",
type: "stock",
},
{
symbol: "MSFT",
full_name: "NASDAQ:MSFT",
description: "Microsoft Corporation",
exchange: "NASDAQ",
ticker: "MSFT",
type: "stock",
},
{
symbol: "GOOGL",
full_name: "NASDAQ:GOOGL",
description: "Alphabet Inc.",
exchange: "NASDAQ",
ticker: "GOOGL",
type: "stock",
},
{
symbol: "TSLA",
full_name: "NYSE:TSLA",
description: "Tesla Inc.",
exchange: "NYSE",
ticker: "TSLA",
type: "stock",
},
{
symbol: "BTCUSDT",
full_name: "BINANCE:BTCUSDT",
description: "Bitcoin / Tether",
exchange: "BINANCE",
ticker: "BTCUSDT",
type: "crypto",
},
];
const filteredSymbols = allSymbols.filter(
(symbol) =>
symbol.symbol.toLowerCase().includes(userInput.toLowerCase()) ||
symbol.description
.toLowerCase()
.includes(userInput.toLowerCase())
);
console.log(` Found ${filteredSymbols.length} symbols`);
callback(filteredSymbols);
}
// Helper methods
generateSampleData(symbolInfo, from, to, resolution) {
const bars = [];
const interval = this.getIntervalMs(resolution);
const startTime = from * 1000;
const endTime = to * 1000;
let currentTime = startTime;
let price = 100 + Math.random() * 100; // Random starting price
while (currentTime <= endTime) {
const change = (Math.random() - 0.5) * 5; // Random price change
const open = price;
const close = Math.max(0.01, price + change);
const high = Math.max(open, close) + Math.random() * 3;
const low = Math.min(open, close) - Math.random() * 3;
const volume = Math.floor(Math.random() * 1000000) + 100000;
bars.push({
time: currentTime,
open: Math.round(open * 100) / 100,
high: Math.round(high * 100) / 100,
low: Math.round(Math.max(0.01, low) * 100) / 100,
close: Math.round(close * 100) / 100,
volume: volume,
});
price = close;
currentTime += interval;
}
return bars;
}
getIntervalMs(resolution) {
const intervals = {
1: 60 * 1000, // 1 minute
5: 5 * 60 * 1000, // 5 minutes
15: 15 * 60 * 1000, // 15 minutes
30: 30 * 60 * 1000, // 30 minutes
60: 60 * 60 * 1000, // 1 hour
240: 4 * 60 * 60 * 1000, // 4 hours
"1D": 24 * 60 * 60 * 1000, // 1 day
"1W": 7 * 24 * 60 * 60 * 1000, // 1 week
"1M": 30 * 24 * 60 * 60 * 1000, // 1 month (approximate)
};
return intervals[resolution] || intervals["1D"];
}
getSymbolDescription(ticker) {
const descriptions = {
AAPL: "Apple Inc.",
MSFT: "Microsoft Corporation",
GOOGL: "Alphabet Inc.",
TSLA: "Tesla Inc.",
BTCUSDT: "Bitcoin / Tether",
};
return descriptions[ticker] || `${ticker} Stock`;
}
getSymbolType(exchange) {
const types = {
NASDAQ: "stock",
NYSE: "stock",
BINANCE: "crypto",
BYBIT: "crypto",
FOREX: "forex",
};
return types[exchange] || "stock";
}
getAssetType(exchange) {
const types = {
NASDAQ: "EQUITY",
NYSE: "EQUITY",
BINANCE: "CRYPTO",
BYBIT: "CRYPTO",
FOREX: "FOREX",
};
return types[exchange] || "EQUITY";
}
getDefaultSegment(exchange) {
const segments = {
NASDAQ: "EQUITY",
NYSE: "EQUITY",
BINANCE: "SPOT",
BYBIT: "FUTURE",
FOREX: "SPOT",
};
return segments[exchange] || "EQUITY";
}
getSessionHours(exchange) {
const sessions = {
NASDAQ: "0930-1600",
NYSE: "0930-1600",
BINANCE: "24x7",
BYBIT: "24x7",
FOREX: "24x5",
};
return sessions[exchange] || "0930-1600";
}
getTimezone(exchange) {
const timezones = {
NASDAQ: "America/New_York",
NYSE: "America/New_York",
BINANCE: "Etc/UTC",
BYBIT: "Etc/UTC",
FOREX: "Etc/UTC",
};
return timezones[exchange] || "America/New_York";
}
}
```
## Step 3: Main Application
Create `src/index.js`:
```javascript
import { createChart } from "@gocharting/chart-sdk";
import { MyDataProvider } from "./dataProvider.js";
import "./styles.css";
class TradingApp {
constructor() {
this.chart = null;
this.dataProvider = null;
this.currentSymbol = "NASDAQ:AAPL";
this.currentInterval = "1D";
this.currentTheme = "dark";
this.init();
}
async init() {
try {
this.updateStatus("Initializing application...");
// Create data provider
this.dataProvider = new MyDataProvider();
// Create chart
await this.createChart();
// Set up event listeners
this.setupEventListeners();
this.updateStatus("Application ready");
} catch (error) {
console.error(" Failed to initialize app:", error);
this.updateStatus("Failed to initialize application", "error");
}
}
async createChart() {
this.updateStatus("Creating chart...");
const chartConfig = {
theme: { name: this.currentTheme },
chart: {
chartType: "candlestick",
animations: true,
grid: {
horizontal: true,
vertical: true,
},
},
ui: {
showToolbar: true,
showVolumePanel: true,
showDrawingTools: true,
},
trading: {
enableTrading: false, // Disable trading for basic integration
},
performance: {
maxCandles: 5000,
animations: true,
},
};
// Use the simplified createChart function
this.chart = createChart("#chart-container", {
symbol: this.currentSymbol,
interval: this.currentInterval,
datafeed: this.dataProvider,
licenseKey: "demo-550e8400-e29b-41d4-a716-446655440000", // Replace with your license key
config: chartConfig,
});
// Set up chart event handlers
this.setupChartEvents();
}
setupChartEvents() {
this.chart.onChartReady(() => {
console.log(" Chart is ready");
this.updateStatus(
`Chart loaded: ${this.currentSymbol} (${this.currentInterval})`
);
});
this.chart.onSymbolChange((symbolInfo) => {
console.log(" Symbol changed:", symbolInfo.name);
this.currentSymbol = symbolInfo.name;
this.updateSymbolSelector();
this.updateStatus(`Symbol: ${symbolInfo.description}`);
});
this.chart.onIntervalChange((interval) => {
console.log(" Interval changed:", interval);
this.currentInterval = interval;
this.updateIntervalSelector();
this.updateStatus(`Interval changed to ${interval}`);
});
this.chart.onError((error) => {
console.error(" Chart error:", error);
this.updateStatus(`Error: ${error.message}`, "error");
});
}
setupEventListeners() {
// Symbol selector
document
.getElementById("symbol-selector")
.addEventListener("change", (e) => {
this.changeSymbol(e.target.value);
});
// Interval selector
document
.getElementById("interval-selector")
.addEventListener("change", (e) => {
this.changeInterval(e.target.value);
});
// Theme selector
document
.getElementById("theme-selector")
.addEventListener("change", (e) => {
this.changeTheme(e.target.value);
});
// Refresh button
document.getElementById("refresh-btn").addEventListener("click", () => {
this.refreshChart();
});
}
async changeSymbol(symbol) {
if (this.chart && symbol !== this.currentSymbol) {
this.updateStatus(`Loading ${symbol}...`);
try {
await this.chart.setSymbol(symbol);
this.currentSymbol = symbol;
} catch (error) {
console.error(" Failed to change symbol:", error);
this.updateStatus("Failed to change symbol", "error");
}
}
}
async changeInterval(interval) {
if (this.chart && interval !== this.currentInterval) {
this.updateStatus(`Changing interval to ${interval}...`);
try {
await this.chart.setInterval(interval);
this.currentInterval = interval;
} catch (error) {
console.error(" Failed to change interval:", error);
this.updateStatus("Failed to change interval", "error");
}
}
}
changeTheme(theme) {
if (this.chart && theme !== this.currentTheme) {
this.updateStatus(`Changing theme to ${theme}...`);
try {
this.chart.setTheme(theme);
this.currentTheme = theme;
this.updateBodyTheme(theme);
} catch (error) {
console.error(" Failed to change theme:", error);
this.updateStatus("Failed to change theme", "error");
}
}
}
refreshChart() {
if (this.chart) {
this.updateStatus("Refreshing chart data...");
// Clear cache and refresh
this.dataProvider.cache.clear();
this.chart.refreshData();
}
}
updateStatus(message, type = "info") {
const statusElement = document.getElementById("status");
statusElement.textContent = message;
statusElement.className = `status ${type}`;
console.log(` Status: ${message}`);
}
updateSymbolSelector() {
const selector = document.getElementById("symbol-selector");
selector.value = this.currentSymbol;
}
updateIntervalSelector() {
const selector = document.getElementById("interval-selector");
selector.value = this.currentInterval;
}
updateBodyTheme(theme) {
document.body.className = `theme-${theme}`;
}
destroy() {
if (this.chart) {
this.chart.destroy();
this.chart = null;
}
}
}
// Initialize app when DOM is loaded
document.addEventListener("DOMContentLoaded", () => {
console.log(" Starting Trading App...");
window.tradingApp = new TradingApp();
});
// Cleanup on page unload
window.addEventListener("beforeunload", () => {
if (window.tradingApp) {
window.tradingApp.destroy();
}
});
```
## Step 4: Styling
Create `src/styles.css`:
```css
/* Additional styles for the application */
.theme-light {
background-color: #ffffff;
color: #333333;
}
.theme-light .controls select,
.theme-light .controls button {
background-color: #f5f5f5;
color: #333333;
border-color: #ddd;
}
.theme-light #chart-container {
background-color: #ffffff;
border-color: #ddd;
}
.theme-light .status {
background-color: #f5f5f5;
color: #333333;
}
/* Loading animation */
.loading::after {
content: "";
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #333;
border-radius: 50%;
border-top-color: #00d4aa;
animation: spin 1s ease-in-out infinite;
margin-left: 10px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Responsive design */
@media (max-width: 768px) {
.controls {
flex-direction: column;
align-items: center;
}
.controls select,
.controls button {
width: 200px;
}
#chart-container {
height: 400px;
}
}
```
## Step 5: Build Configuration
Create `webpack.config.js`:
```javascript
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
clean: true,
},
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"],
},
},
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
devServer: {
static: "./dist",
port: 3000,
open: true,
hot: true,
},
resolve: {
fallback: {
fs: false,
path: false,
},
},
};
```
## Step 6: Run Your Application
Add scripts to `package.json`:
```json
{
"scripts": {
"start": "webpack serve",
"build": "webpack --mode=production",
"dev": "webpack serve --mode=development"
}
}
```
Run the application:
```bash
# Start development server
npm start
# Or build for production
npm run build
```
## Next Steps
Congratulations! You now have a working GoCharting SDK integration. Here's what to explore next:
1. **[Trading Integration](./trading-integration.md)** - Add real trading capabilities
2. **[Performance Optimization](../guides/performance.md)** - Optimize for production
3. **[Configuration Guide](../api/configuration.md)** - Customize chart appearance
## Related Documentation
- **[API Reference](../api/)** - Complete API documentation
- **[Configuration Guide](../api/configuration.md)** - Chart configuration options
- **[Examples](../examples/)** - Framework-specific examples
---
_Ready to add more features? Check out our [advanced tutorials](./advanced-features.md)!_
File: docs/tutorials/implement-datafeed/datafeed-implementation.md
=============================================================================
# Datafeed implementation
Implement history and symbol resolution. After this step the chart loads candles; live updates come in [Streaming implementation](./streaming-implementation.md).
## Required methods
| Method | Role |
| ------ | ---- |
| `resolveSymbol` | Map a symbol string → GoCharting security / `SymbolInfo` (precision, session, intervals, exchange_info, …) |
| `getBars` | Return historical OHLCV for a time range |
Optional but useful: `searchSymbols` so the top-bar search works.
## `resolveSymbol`
`onResolve` receives a **security / `SymbolInfo` object** in GoCharting’s native shape (not a minimal ticker stub). Production backends typically return this JSON as-is from symbol lookup.
```javascript
resolveSymbol(symbolName, onResolve, onError) {
// Example: "BYBIT:FUTURE:BTCUSDT"
const parts = symbolName.split(":");
const exchange = parts[0] || "BYBIT";
const segment = parts[1] || "FUTURE";
const symbol = parts[parts.length - 1] || "BTCUSDT";
try {
// Prefer fetching this object from your symbol API.
// Shape below matches what GoCharting uses internally.
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", // or "delayed_streaming" | "endofday" | "pulsed"
industry: "technology",
symbol_logo_urls: [
"https://upload.wikimedia.org/wikipedia/commons/a/a4/Flag_of_the_United_States.svg",
],
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",
data_source_location: "blr1",
supports: {
footprint: true,
},
exchange_info: {
name: "bybit",
code: "BYBIT",
country_cd: "US",
zone: "UTC",
has_unique_trade_id: true,
logo_url: "https://gocharting.com/avatar/Bybit_Logo.svg",
holidays: null,
hours: [
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
{ open: true },
],
contains_ambiguous_symbols: false,
valid_intervals: [
"1m",
"3m",
"5m",
"10m",
"15m",
"30m",
"1h",
"2h",
"4h",
"12h",
"1D",
"1W",
"1M",
],
},
// Compatibility fields some chart paths still read
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: [
"1m",
"3m",
"5m",
"10m",
"15m",
"30m",
"1h",
"2h",
"4h",
"12h",
"1D",
"1W",
"1M",
],
});
} catch (e) {
onError(String(e?.message || e));
}
}
```
Call **exactly one** of `onResolve` or `onError`.
Important fields for chart behaviour:
| Field | Why it matters |
| ----- | -------------- |
| `exchange` / `segment` / `symbol` | Identity used in streams and history requests |
| `name` | Display title (also used as fallback if `symbol` is missing) |
| `tick_size`, `max_tick_precision`, `max_volume_precision` | Price/volume formatting |
| `exchange_info.valid_intervals` | Interval picker allowlist |
| `exchange_info.hours` / `zone` | Session and timezone |
| `data_status`, `delay_seconds` | Live vs delayed labelling |
| `supports.footprint` | Whether footprint features are enabled for the symbol |
See [Datafeed API — Symbol Info Format](../../api/datafeed.md#symbol-info-format-resolvesymbol-return) for the full field reference.
## `getBars`
```javascript
async getBars(symbolInfo, resolution, periodParams) {
const { from, to, firstDataRequest, rows } = periodParams;
const qs = new URLSearchParams({
symbol: symbolInfo.full_name || symbolInfo.name,
interval: String(resolution),
from: String(from.getTime()),
to: String(to.getTime()),
});
if (rows != null) qs.set("rows", String(rows));
const res = await fetch(`https://api.example.com/bars?${qs}`);
if (!res.ok) {
return { bars: [], meta: { noData: true } };
}
const raw = await res.json();
const bars = (raw.bars || raw).map((b) => ({
time: b.time ?? b.timestamp, // Unix timestamp in milliseconds
open: Number(b.open),
high: Number(b.high),
low: Number(b.low),
close: Number(b.close),
volume: Number(b.volume ?? 0),
}));
return {
bars,
meta: {
noData: bars.length === 0,
// nextTime: optional unix ms hint for pagination
},
};
}
```
### Period params
| Field | Meaning |
| ----- | ------- |
| `from` / `to` | `Date` range requested |
| `firstDataRequest` | `true` on the first load for this symbol/interval |
| `rows` | Optional bar count hint |
Return `{ bars: [], meta: { noData: true } }` when there is nothing in range (do not throw for empty history).
For the hosted demo history/tick protocol, see [Demo WebSocket](../../guides/demo-websocket.md).
## Optional: `searchSymbols`
```javascript
searchSymbols(userInput, exchange, symbolType, onResultReadyCallback) {
onResultReadyCallback({ searchInProgress: true, items: [] });
fetch(`https://api.example.com/search?q=${encodeURIComponent(userInput)}`)
.then((r) => r.json())
.then((items) => {
onResultReadyCallback({
searchInProgress: false,
items: items.map((it) => ({
symbol: it.symbol,
full_name: it.full_name,
description: it.description,
exchange: it.exchange,
type: it.type || "crypto",
})),
});
})
.catch(() => {
onResultReadyCallback({ searchInProgress: false, items: [] });
});
}
```
The callback receives an **object** `{ searchInProgress, items }` — not a bare array.
## Optional: `onReady` / UDF adapter
Implement `onReady` to advertise exchanges, symbol types, and `supported_resolutions` (used by `getIntervals()` when the symbol has no `valid_intervals`). Or use `UDFCompatibleDatafeed` against a UDF HTTP backend. Lab: [`phase6-onReady-udf-lab.html`](../../examples/phase6-onReady-udf-lab.html).
**Limitation:** GoCharting does not client-aggregate higher timeframes from daily the way TradingView does with `daily_multipliers` (`2D`, `6M`, …). Return bars for the requested resolution. Details: [TV → GC migration — Known limitations](../../resources/tv-to-gocharting-migration.md#known-limitations).
## Putting it together
```javascript
const datafeed = {
resolveSymbol(symbolName, onResolve, onError) {
/* as above */
},
async getBars(symbolInfo, resolution, periodParams) {
/* as above */
},
searchSymbols(userInput, exchange, symbolType, onResultReadyCallback) {
/* as above */
},
};
createChart("#chart", {
datafeed,
symbol: "BYBIT:FUTURE:BTCUSDT",
interval: "1m",
licenseKey: "demo-550e8400-e29b-41d4-a716-446655440000",
});
```
## Checklist
- [ ] `resolveSymbol` always calls `onResolve` or `onError`
- [ ] Resolved security includes `exchange` / `segment` / `symbol`, precision fields, and preferably `exchange_info`
- [ ] Bars include `open` / `high` / `low` / `close` (and usually `volume` + time)
- [ ] Empty ranges use `meta.noData: true`
- [ ] Search (if implemented) uses `{ searchInProgress, items }`
## Next
→ [Streaming implementation](./streaming-implementation.md) — push live ticks with `subscribeTicks`
## Related
- [Datafeed API](../../api/datafeed.md)
- [Widget setup](./widget-setup.md)
File: docs/tutorials/implement-datafeed/README.md
============================================================
# Implement a datafeed
Build a custom data source for the GoCharting chart in three steps: create the widget, serve history, then stream live updates.
Realtime is one of two paths (pick one — not both):
- **`subscribeTicks`** — push trades; the SDK aggregates ticks into the active candle.
- **`subscribeBars`** — push complete OHLCV bars for the chart resolution; the SDK applies them directly (no tick aggregation).
If both are implemented, the SDK prefers ticks and ignores bars.
## Tutorial path
| Step | Page | What you build |
| ---- | ---- | -------------- |
| 1 | [Widget setup](./widget-setup.md) | Container + `createChart` + stub datafeed |
| 2 | [Datafeed implementation](./datafeed-implementation.md) | `resolveSymbol`, `getBars`, optional `searchSymbols` / `searchSymbolsPaginated` |
| 3 | [Streaming implementation](./streaming-implementation.md) | `subscribeTicks` / `unsubscribeTicks` **or** `subscribeBars` / `unsubscribeBars` |
## Prerequisites
- GoCharting SDK installed (`@gocharting/chart-sdk` or UMD)
- A license key (use the demo key for local testing)
- A backend (or mock) that can return OHLCV history and, for live charts, trade ticks
## Related API
- [Datafeed API](../api/datafeed.md) — method contracts and payload shapes (`onReady`, UDF adapter, bars/ticks)
- [Demo WebSocket](../guides/demo-websocket.md) — hosted demo stream for ticks and history
- [Configuration](../api/configuration.md) — construct options (`symbol`, `interval`, `theme`, …)
- Labs: [`phase6-onReady-udf-lab.html`](../../examples/phase6-onReady-udf-lab.html), [`phase6-subscribeBars-lab.html`](../../examples/phase6-subscribeBars-lab.html), [`phase6-searchSymbolsPaginated-lab.html`](../../examples/phase6-searchSymbolsPaginated-lab.html)
File: docs/tutorials/implement-datafeed/streaming-implementation.md
==============================================================================
# Streaming implementation
Add live updates with **either** `subscribeTicks` / `unsubscribeTicks` **or** `subscribeBars` / `unsubscribeBars` (not both).
- **Ticks path:** push trade ticks; the SDK aggregates them into the active candle.
- **Bars path:** push complete OHLCV bars for the chart resolution; the SDK applies them directly (no tick aggregation).
If both are implemented on the datafeed, the SDK prefers ticks and ignores bars.
Labs: [`phase6-subscribeBars-lab.html`](../../examples/phase6-subscribeBars-lab.html), [`phase6-searchSymbolsPaginated-lab.html`](../../examples/phase6-searchSymbolsPaginated-lab.html), [`phase6-onReady-udf-lab.html`](../../examples/phase6-onReady-udf-lab.html).
## How it fits together (ticks)
1. History loads via `getBars` ([previous step](./datafeed-implementation.md)).
2. After the symbol is ready, the chart calls `subscribeTicks(...)`.
3. For each trade, call `onRealtimeCallback(tradeMessage)`.
4. On symbol change / destroy, the chart calls `unsubscribeTicks(subscriberUID)`.
```text
Your WebSocket / bus → subscribeTicks → onRealtimeCallback(tick)
↓
SDK aggregates into live bars
```
## How it fits together (bars)
1. History loads via `getBars`.
2. The chart calls `subscribeBars(...)` with the active chart resolution.
3. For each bar update, call `onRealtimeCallback({ time, open, high, low, close, volume })`.
4. On symbol change / destroy, the chart calls `unsubscribeBars(subscriberUID)`.
```text
Your WebSocket / bus → subscribeBars → onRealtimeCallback(bar)
↓
SDK applies host OHLC (no tick aggregation)
```
## Trade message shape
```javascript
{
type: "trade",
productId: "BYBIT:FUTURE:BTCUSDT",
symbol: "BTCUSDT",
exchange: "BYBIT",
segment: "FUTURE",
timeStamp: new Date(), // Date object
tradeID: "abc123",
price: 45123.45,
quantity: 0.123,
amount: 5555.67, // price * quantity
side: "BUY", // or "SELL" (uppercase)
}
```
## Implement `subscribeTicks` / `unsubscribeTicks`
```javascript
const datafeed = {
subscriptions: new Map(), // subscriberUID → cleanup
// … resolveSymbol + getBars from the previous page …
subscribeTicks(
symbolInfo,
resolution,
onRealtimeCallback,
subscriberUID,
onResetCacheNeededCallback,
) {
const productId =
symbolInfo.full_name ||
`${symbolInfo.exchange}:${symbolInfo.segment}:${symbolInfo.symbol}`;
const ws = new WebSocket("wss://stream.example.com/trades");
ws.onopen = () => {
ws.send(
JSON.stringify({
op: "subscribe",
symbol: productId,
}),
);
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
const trades = Array.isArray(msg.data) ? msg.data : [msg];
for (const t of trades) {
onRealtimeCallback({
type: "trade",
productId,
symbol: symbolInfo.symbol || symbolInfo.ticker,
exchange: symbolInfo.exchange,
segment: symbolInfo.segment,
timeStamp: new Date(t.time ?? t.T ?? Date.now()),
tradeID: String(t.id ?? t.i ?? `${t.time}-${t.price}`),
price: Number(t.price ?? t.p),
quantity: Number(t.qty ?? t.v ?? 0),
amount:
Number(t.price ?? t.p) * Number(t.qty ?? t.v ?? 0),
side: String(t.side ?? t.S ?? "BUY").toUpperCase(),
});
}
};
ws.onerror = () => {
// Optional: ask the chart to clear cache / refetch
onResetCacheNeededCallback?.();
};
this.subscriptions.set(subscriberUID, () => {
try {
ws.close();
} catch {
/* ignore */
}
});
},
unsubscribeTicks(subscriberUID) {
const stop = this.subscriptions.get(subscriberUID);
if (stop) stop();
this.subscriptions.delete(subscriberUID);
},
};
```
Key rules:
- Key subscriptions by **`subscriberUID`** (not by symbol alone) — multiple charts can share one datafeed instance.
- Always clean up in **`unsubscribeTicks`**.
- Push **ticks**, not completed OHLC bars (unless you deliberately synthesize ticks from bars — see [Datafeed API](../../api/datafeed.md)).
## Demo WebSocket
For a ready-made stream while you integrate, use GoCharting’s demo endpoint and map its trade frames into the trade message above:
- Guide: [Demo WebSocket API](../../guides/demo-websocket.md)
- URL: `wss://gocharting.com/sdk/ws`
## Checklist
- [ ] `subscribeTicks` stores cleanup keyed by `subscriberUID` **or** you use `subscribeBars` instead (not both)
- [ ] Each trade calls `onRealtimeCallback` with `type: "trade"` and numeric `price` (ticks path)
- [ ] `unsubscribeTicks` / `unsubscribeBars` closes the socket / cancels the listener
- [ ] You implement **only one** realtime path: ticks **or** bars
## Related
- [Datafeed API](../../api/datafeed.md)
- [Datafeed implementation](./datafeed-implementation.md)
- [Advanced Features](../advanced-features.md) — fuller WebSocket example
- [CodePen Advanced](https://codepen.io/Admin-GoCharting/pen/vEKdyBW)
File: docs/tutorials/implement-datafeed/widget-setup.md
==================================================================
# Widget setup
Create a chart container and mount the widget with a datafeed object. This page ends with a stub that paints an empty chart; the next pages fill in history and streaming.
## 1. Page shell
```html
Estimated Cost:
$0.00
Available Balance:
$0.00
`;
document.body.appendChild(panel);
this.panel = panel;
}
setupEventListeners() {
// Side selection
this.panel.querySelectorAll("[data-side]").forEach((btn) => {
btn.addEventListener("click", (e) => {
this.selectSide(e.target.dataset.side);
});
});
// Order type change
this.panel
.querySelector("#order-type-select")
.addEventListener("change", (e) => {
this.updateOrderType(e.target.value);
});
// Price/size input changes
["price-input", "size-input"].forEach((id) => {
this.panel.querySelector(`#${id}`).addEventListener("input", () => {
this.updateOrderSummary();
});
});
// Place order button
this.panel
.querySelector(".place-order-btn")
.addEventListener("click", () => {
this.placeOrder();
});
// Cancel button
this.panel
.querySelector(".cancel-btn")
.addEventListener("click", () => {
this.hide();
});
// Close button
this.panel.querySelector(".close-btn").addEventListener("click", () => {
this.hide();
});
}
show(symbol, price) {
this.panel.querySelector("#symbol-input").value = symbol;
this.panel.querySelector("#price-input").value = price;
this.updateOrderSummary();
this.panel.style.display = "block";
}
hide() {
this.panel.style.display = "none";
}
selectSide(side) {
// Update button states
this.panel.querySelectorAll("[data-side]").forEach((btn) => {
btn.classList.toggle("active", btn.dataset.side === side);
});
this.updateOrderSummary();
}
updateOrderType(orderType) {
const priceInput = this.panel.querySelector("#price-input");
priceInput.disabled = orderType === "market";
if (orderType === "market") {
priceInput.value = "Market Price";
}
this.updateOrderSummary();
}
updateOrderSummary() {
const price =
parseFloat(this.panel.querySelector("#price-input").value) || 0;
const size =
parseFloat(this.panel.querySelector("#size-input").value) || 0;
const estimatedCost = price * size;
this.panel.querySelector(
"#estimated-cost"
).textContent = `$${estimatedCost.toFixed(2)}`;
}
async placeOrder() {
try {
const orderRequest = this.getOrderRequest();
// Show loading state
const placeBtn = this.panel.querySelector(".place-order-btn");
placeBtn.disabled = true;
placeBtn.textContent = "Placing...";
// Place order
const order = await this.tradingProvider.placeOrder(orderRequest);
// Show success message
this.showNotification(`Order placed: ${order.orderId}`, "success");
// Hide panel
this.hide();
} catch (error) {
this.showNotification(
`Failed to place order: ${error.message}`,
"error"
);
} finally {
// Reset button state
const placeBtn = this.panel.querySelector(".place-order-btn");
placeBtn.disabled = false;
placeBtn.textContent = "Place Order";
}
}
getOrderRequest() {
const activeSideBtn = this.panel.querySelector("[data-side].active");
return {
accountId: this.panel.querySelector("#account-select").value,
symbol: this.panel.querySelector("#symbol-input").value,
side: activeSideBtn.dataset.side,
orderType: this.panel.querySelector("#order-type-select").value,
price: parseFloat(this.panel.querySelector("#price-input").value),
size: parseFloat(this.panel.querySelector("#size-input").value),
timeInForce: this.panel.querySelector("#tif-select").value,
stopLoss:
parseFloat(
this.panel.querySelector("#stop-loss-input").value
) || null,
takeProfit:
parseFloat(
this.panel.querySelector("#take-profit-input").value
) || null,
};
}
showNotification(message, type) {
// Implementation for showing notifications
console.log(`${type.toUpperCase()}: ${message}`);
}
}
```
## Position Management
### Position Tracker
```javascript
class PositionTracker {
constructor(chart, tradingProvider) {
this.chart = chart;
this.tradingProvider = tradingProvider;
this.positions = new Map();
this.setupEventListeners();
}
setupEventListeners() {
this.tradingProvider.on("positionUpdate", (position) => {
this.updatePosition(position);
});
this.tradingProvider.on("tradeExecution", (trade) => {
this.handleTradeExecution(trade);
});
}
updatePosition(position) {
this.positions.set(position.symbol, position);
this.updatePositionDisplay(position);
this.updateChartPositionLines(position);
}
updatePositionDisplay(position) {
// Update position in UI table
const positionRow = document.querySelector(
`[data-symbol="${position.symbol}"]`
);
if (positionRow) {
this.updatePositionRow(positionRow, position);
} else {
this.createPositionRow(position);
}
}
updateChartPositionLines(position) {
if (position.size === 0) {
// Remove position line if position is closed
this.chart.removePositionLine(position.symbol);
} else {
// Add or update position line
this.chart.addPositionLine({
symbol: position.symbol,
entryPrice: position.entryPrice,
currentPrice: position.currentPrice,
size: position.size,
side: position.side,
pnl: position.pnl,
style: {
color: position.pnl >= 0 ? "#4caf50" : "#f44336",
width: 2,
style: "solid",
},
});
}
}
calculatePositionMetrics(position) {
const { entryPrice, currentPrice, size, side } = position;
const multiplier = side === "long" ? 1 : -1;
const priceDiff = (currentPrice - entryPrice) * multiplier;
const pnl = priceDiff * size;
const pnlPercent = (priceDiff / entryPrice) * 100;
return {
pnl: pnl,
pnlPercent: pnlPercent,
unrealizedPnl: pnl,
marketValue: currentPrice * size,
};
}
closePosition(symbol, size = null) {
const position = this.positions.get(symbol);
if (!position) return;
const closeSize = size || position.size;
const closeSide = position.side === "long" ? "sell" : "buy";
// Create market order to close position
const closeOrder = {
symbol: symbol,
side: closeSide,
orderType: "market",
size: closeSize,
reduceOnly: true,
};
return this.tradingProvider.placeOrder(closeOrder);
}
}
```
## Related Documentation
- **[Performance Optimization](../guides/performance.md)** - Optimize for production
- **[API Reference](../api/)** - Complete API documentation
- **[Configuration Guide](../api/configuration.md)** - Trading configuration
- **[Production Guide](../guides/production.md)** - Deploy trading applications
---
_Ready to deploy your trading application? Check out our [production deployment guide](../guides/production.md)!_
File: src/types/index.d.ts
==========================
/**
* GoCharting SDK - Complete TypeScript Type Definitions
* @version 1.0.5
*/
import type * as React from "react";
import type * as ReactDOM from "react-dom";
// ============================================================================
// Symbol and Market Data Types
// ============================================================================
/**
* Symbol type classification (legacy compatibility)
*/
export type SymbolType =
| "stock"
| "forex"
| "crypto"
| "futures"
| "options"
| "index"
| "bond";
/**
* Asset type classification (modern API format)
*/
export type AssetType =
| "CRYPTO"
| "EQUITY"
| "FOREX"
| "COMMODITY"
| "INDEX"
| "BOND";
/**
* Data streaming status
*/
export type DataStatus =
| "streaming"
| "endofday"
| "pulsed"
| "delayed_streaming";
/**
* Exchange information object
* Contains metadata about the exchange
*/
export type ExchangeInfo = {
/** Exchange name (lowercase) */
name: string;
/** Exchange code (uppercase) */
code: string;
/** Country code */
country_cd?: string;
/** Timezone */
zone: string;
/** Whether exchange provides unique trade IDs */
has_unique_trade_id?: boolean;
/** Exchange logo URL */
logo_url?: string;
/** Array of holiday dates (null if 24/7) */
holidays?: string[] | null;
/** Trading hours for each day (0=Sunday, 6=Saturday) */
hours?: Array<{ open: boolean }>;
/** Whether exchange has ambiguous symbol names */
contains_ambiguous_symbols?: boolean;
/** Supported timeframes */
valid_intervals?: string[];
};
/**
* Symbol pair information (for crypto/forex)
*/
export type SymbolPair = {
/** Base currency (e.g., "BTC") */
from: string;
/** Quote currency (e.g., "USDT") */
to: string;
};
/**
* Symbol information object
* Contains all metadata about a trading symbol.
* This is returned from the datafeed's resolveSymbol method.
*/
export type SymbolInfo = {
// ========================================================================
// Required Symbol Information
// ========================================================================
/** Trading symbol (e.g., "BTCUSDT", "AAPL") — avoid `/`, `:`, and other path-unsafe chars */
symbol: string;
/** Full symbol name with exchange (e.g., "BYBIT:FUTURE:BTCUSDT") */
full_name: string;
/** Display name (e.g., "BTC / USDT PERPETUAL FUTURES") — preferred UI title */
name?: string;
/** Human-readable description (often same as `name`) */
description: string;
/** Exchange name (e.g., "BYBIT", "NASDAQ") */
exchange: string;
/** Symbol type (e.g., "crypto", "stock", "futures") */
type: string;
/** Trading session (e.g., "24x7", "0930-1600") */
session: string;
/** Timezone (e.g., "Etc/UTC", "America/New_York") */
timezone: string;
/** Ticker symbol (usually same as symbol) */
ticker: string;
/** Supports intraday data */
has_intraday: boolean;
/** Supported time intervals/resolutions (e.g., ["1", "5", "15", "60", "1D"]) */
supported_resolutions: string[];
// ========================================================================
// Optional Symbol Information
// ========================================================================
/** Market segment (e.g., "FUTURE", "SPOT", "OPTION") */
segment?: string;
/** Asset type classification (e.g., "CRYPTO", "EQUITY") */
asset_type?: string;
/** Trading session label */
session_label?: string;
// ========================================================================
// Symbol Pair Information (for crypto/forex)
// ========================================================================
/** Currency pair information */
pair?: SymbolPair;
// ========================================================================
// Trading Information
// ========================================================================
/** Whether symbol is tradeable */
tradeable?: boolean;
/** Whether symbol is an index */
is_index?: boolean;
/** Whether symbol is a formula/calculated */
is_formula?: boolean;
// ========================================================================
// Data Feed Information
// ========================================================================
/** Data delay in seconds (0 for real-time) */
delay_seconds?: number;
/** Data streaming status */
data_status?: DataStatus;
/** Data source location/datacenter */
data_source_location?: string;
// ========================================================================
// Display Information
// ========================================================================
/** Industry classification */
industry?: string;
/** Symbol logo URLs */
symbol_logo_urls?: string[];
// ========================================================================
// Price & Volume Precision
// ========================================================================
/** Contract size (for futures/options) */
contract_size?: number;
/** Minimum price movement (tick size) */
tick_size?: number;
/** Display tick size */
display_tick_size?: number;
/** Minimum volume increment */
volume_size_increment?: number;
/** Maximum decimal places for price */
max_tick_precision?: number;
/** Maximum decimal places for volume */
max_volume_precision?: number;
/** Quote currency */
quote_currency: string;
// ========================================================================
// Futures-specific (if applicable)
// ========================================================================
/** 'PERP' for perpetual, or expiry date for dated futures */
future_type?: string;
// ========================================================================
// Exchange Information
// ========================================================================
/** Exchange information object */
exchange_info?: ExchangeInfo;
// ========================================================================
// Additional Optional Fields
// ========================================================================
/** Supports daily data */
has_daily?: boolean;
/** Intraday multipliers (e.g., ["1", "5", "15", "30", "60", "240"]) */
intraday_multipliers?: string[];
/** Volume decimal places */
volume_precision?: number;
/** Source identifier from exchange */
source_id?: string;
/** Feature flags for this symbol (e.g. footprint eligibility) */
supports?: {
footprint?: boolean;
[key: string]: boolean | undefined;
};
};
/**
* Bar/Candle data structure (OHLCV)
*/
export type Bar = {
/** Unix timestamp in milliseconds */
time: number;
/** Opening price */
open: number;
/** Highest price */
high: number;
/** Lowest price */
low: number;
/** Closing price */
close: number;
/** Volume */
volume: number;
};
/**
* Real-time tick data (extends Bar with additional fields)
*/
export type Tick = Bar & {
/** Price change from previous close */
change?: number;
/** Price change percentage */
changePercent?: number;
};
/**
* Trade message from real-time data feed
* Used in subscribeTicks callback for raw trade data
*/
export type TradeMessage = {
/** Message type - always "trade" */
type: "trade";
/** Full product identifier (EXCHANGE:SEGMENT:SYMBOL) */
productId: string;
/** Symbol name */
symbol: string;
/** Exchange name */
exchange: string;
/** Market segment (e.g., "FUTURE", "SPOT", "OPTION") */
segment: string;
/** Trade timestamp */
timeStamp: Date;
/** Unique trade identifier */
tradeID: string;
/** Trade price */
price: number;
/** Trade quantity */
quantity: number;
/** Trade amount (price * quantity) */
amount: number;
/** Trade side: "BUY" or "SELL" */
side: "BUY" | "SELL";
/** Best bid price (optional, used for accurate PNL calculation on long positions) */
bid?: number;
/** Best ask price (optional, used for accurate PNL calculation on short positions) */
ask?: number;
/** Live PnL multiplier for cross-currency positions.
* When provided, the SDK updates matching positions' pnlMultiplier on every tick,
* keeping chart P&L in sync with the host app's Positions tab.
* The host app computes this from live conversion rates (e.g. contractSize * conversion). */
pnlMultiplier?: number;
};
/**
* Search symbols result wrapper returned via callback
*/
export type SearchSymbolsResult = {
/** Indicates if search is still in progress */
searchInProgress: boolean;
/** Array of search result items */
items: SearchResult[];
};
/**
* Search result for symbol search
*/
export type SearchResult = {
/** Symbol name */
symbol: string;
/** Full symbol name with exchange */
full_name: string;
/** Human-readable description */
description: string;
/** Exchange name */
exchange: string;
/** Ticker symbol */
ticker: string;
/** Symbol type */
type: SymbolType;
/**
* Unique id passed into resolve / setSymbol when the user picks a hit.
* For UDF backends this should be the bare ticker (e.g. `AAPL`), not a
* GoCharting `EXCHANGE:SEGMENT:SYMBOL` combo key.
*/
key?: string;
/** Optional segment (EQUITY / FUTURE / …); defaults to FUTURE in UI if omitted */
segment?: string;
};
/**
* Options for paginated symbol search (`searchSymbolsPaginated`).
* When implemented, the SDK prefers this over `searchSymbols` for the first page.
*/
export type SearchSymbolsPaginatedOptions = {
/** User search query */
userInput: string;
/** Exchange filter (can be empty string) */
exchange: string;
/** Symbol type filter (can be empty string) */
symbolType: string;
/** Max results to return for this page */
limit: number;
/** Zero-based offset into the result set */
offset: number;
};
// ============================================================================
// Datafeed Types
// ============================================================================
/**
* Period parameters for historical data requests
* Note: At runtime, the SDK passes Date objects for from/to, not numbers
*/
export type PeriodParams = {
/** Start time (Date object) */
from: Date;
/** End time (Date object) */
to: Date;
/** Whether this is the first data request */
firstDataRequest: boolean;
/** Number of bars to fetch (optional) */
countBack?: number;
/** Number of rows to fetch (optional) */
rows?: number;
/**
* Duration string when duration buttons are clicked (optional)
* When provided, from/to dates are calculated based on this duration.
* Use this to identify which duration button was clicked.
*/
duration?: DurationString;
};
/**
* Result from getBars method (simple format with bars array)
*/
export type BarsResult = {
/** Array of bar data */
bars: Bar[];
/** Optional metadata (TV HistoryMetadata-compatible) */
meta?: HistoryMetadata;
};
/**
* TV HistoryMetadata — history pagination / empty-range flags on getBars.
* - `noData`: feed has no bars in the requested range
* - `nextTime`: next available bar time (Unix seconds) when noData
*/
export type HistoryMetadata = {
/** Indicates no data available for the requested range */
noData?: boolean;
/** Next available data time (Unix seconds) */
nextTime?: number;
};
/**
* UDF (Universal Data Feed) format response
* Standard OHLCV column arrays used by many datafeeds (`t`/`o`/`h`/`l`/`c`/`v`)
*/
export type UDFResponse = {
/** Status: "ok", "error", or "no_data" */
s: "ok" | "error" | "no_data";
/** Error message (when s === "error") */
errmsg?: string;
/** Next available time (when s === "no_data") */
nextTime?: number | null;
/** Time array (Unix timestamps in seconds) */
t?: number[];
/** Open prices array */
o?: number[];
/** High prices array */
h?: number[];
/** Low prices array */
l?: number[];
/** Close prices array */
c?: number[];
/** Volume array */
v?: number[];
};
/**
* Resolution/Interval object
*/
export type Resolution = {
/** Scale value (e.g., 1, 5, 15) */
scale: number;
/** Time unit (e.g., "m", "h", "D", "W", "M") */
units: string;
/** Original interval string (e.g., "1m", "5m", "15m", "30m", "1h", "4h", "1D", "1W", "1M") - set internally by SDK */
type?: string;
};
export type RealtimeCallback = (data: Bar | Tick | TradeMessage) => void;
/**
* Exchange descriptor for symbol-search filters (TV DatafeedConfiguration.exchanges).
*/
export type DatafeedExchangeDescriptor = {
/** Value passed as `exchange` to searchSymbols / searchSymbolsPaginated */
value: string;
/** Display name in the search UI */
name: string;
/** Optional longer description */
desc?: string;
};
/**
* Symbol-type descriptor for symbol-search filters (TV DatafeedConfiguration.symbols_types).
*/
export type DatafeedSymbolTypeDescriptor = {
/** Display name */
name: string;
/** Value passed as `symbolType` to search */
value: string;
};
/**
* Feed-level configuration returned from datafeed `onReady` (TV DatafeedConfiguration).
* Used for the exchanges / symbol-types / resolutions handshake.
*/
export type DatafeedConfiguration = {
/** Exchanges for the symbol-search filter (empty → no exchange filter) */
exchanges?: DatafeedExchangeDescriptor[];
/** Symbol types for the symbol-search filter */
symbols_types?: DatafeedSymbolTypeDescriptor[];
/**
* Resolutions the chart should offer globally.
* Accepts TV (`"1"`, `"60"`, `"1D"`) or GC (`"1m"`, `"1h"`, `"1D"`) strings.
* Used by `getIntervals()` when the active symbol has no `valid_intervals`.
*/
supported_resolutions?: string[];
/** Symbol search via searchSymbols / searchSymbolsPaginated */
supports_search?: boolean;
/** UDF group-request mode (`/symbol_info`) — prefer search for large catalogs */
supports_group_request?: boolean;
/** Chart bar marks (`getMarks`) */
supports_marks?: boolean;
/** Timescale marks (`getTimescaleMarks`) */
supports_timescale_marks?: boolean;
/** Server clock (`getServerTime` / UDF `/time`) */
supports_time?: boolean;
/** Optional currency codes for currency conversion UI */
currency_codes?: string[];
/** Optional unit ids for unit conversion UI */
units?: Array<{ id: string; name: string; description?: string }>;
};
/** Callback for datafeed `onReady` */
export type OnDatafeedReadyCallback = (configuration: DatafeedConfiguration) => void;
/**
* Datafeed type - implement this to provide data to the chart
* This is a simple object type, no class inheritance required
*
* REQUIRED METHODS: getBars, resolveSymbol
* OPTIONAL METHODS: onReady, searchSymbols, searchSymbolsPaginated, subscribeTicks, unsubscribeTicks,
* subscribeBars, unsubscribeBars, getMarks, getTimescaleMarks, destroy
*
* Realtime exclusivity: implement either subscribeTicks/unsubscribeTicks OR
* subscribeBars/unsubscribeBars — not both. If both are present, the SDK prefers
* ticks, warns, and ignores bars.
*/
export type Datafeed = {
// ========================================================================
// REQUIRED METHODS
// ========================================================================
/**
* Fetch historical bar data (REQUIRED)
*
* Can return either:
* - BarsResult format: { bars: Bar[], meta?: {...} }
* - UDF format: { s: "ok", t: [...], o: [...], h: [...], l: [...], c: [...], v: [...] }
*
* @param symbolInfo - Symbol information
* @param resolution - Time interval (can be string or Resolution object)
* @param periodParams - Time period parameters
* @returns Promise resolving to bars result in either BarsResult or UDF format
*/
getBars(
symbolInfo: SymbolInfo,
resolution: string | Resolution,
periodParams: PeriodParams,
): Promise