<!-- Public documentation export; not library implementation source. -->
# The datafeed contract

The chart gets symbols, historical bars and live updates by asking your datafeed. A datafeed is just an object with callback methods. The chart never connects to an exchange on its own.

## The order of requests

1. `onReady` tells the chart what your datafeed supports overall.
2. `searchSymbols` returns search results when the user searches.
3. `resolveSymbol` returns the details for one symbol.
4. `getBars` returns historical OHLCV bars.
5. `subscribeBars` streams live updates, and `unsubscribeBars` stops each stream.

Call the success and error callbacks asynchronously, so requests don't stack up recursively. Call back exactly once for each request. If your cache shares mutable objects, hand the chart copies so that later changes don't leak back into your cache.

## Start from a working datafeed

This sample datafeed follows the same public contract and needs no network. It only returns history for a fixed period, and it tells the chart clearly when there's no more data.

```ts
// Import these public types from the declarations in your authorized distribution.

export const symbols = ['AAPL', 'MSFT', 'BTCUSD'] as const
export const resolutions = ['15', '60', '1D'] as ResolutionString[]
const day = 86_400_000
export const sampleEnd = Date.UTC(2026, 9, 15)
const sampleStart = sampleEnd - 730 * day

export function resolutionMs(resolution: string): number {
  if (resolution === '1D' || resolution === 'D') return day
  const minutes = Number(resolution)
  if (!Number.isFinite(minutes) || minutes <= 0) throw new Error('Unsupported sample interval')
  return minutes * 60_000
}
export function sampleBar(symbol: string, time: number, step: number): Bar {
  const index = (time - sampleStart) / step
  const base = symbol === 'BTCUSD' ? 62000 : symbol === 'MSFT' ? 400 : 180
  const wave = Math.sin(index * .17) * base * .025 + Math.cos(index * .043) * base * .04
  const open = base + wave + index * base * .00002
  const close = open + Math.sin(index * 1.71) * base * .008
  return { time, open, close, high: Math.max(open, close) + base * .003, low: Math.min(open, close) - base * .003, volume: Math.round(100000 + Math.abs(Math.sin(index)) * 400000) }
}
export function historicalBars(symbol: string, resolution: string, from: number, to: number, countBack: number): Bar[] {
  const step = resolutionMs(resolution)
  const end = Math.min(Math.ceil(to * 1000 / step) * step, sampleEnd)
  const requestedStart = Math.min(from * 1000, end - Math.min(Math.max(0, countBack), 5000) * step)
  const start = Math.max(sampleStart, Math.ceil(requestedStart / step) * step, end - 5000 * step)
  const bars: Bar[] = []
  for (let time = start; time < end; time += step) bars.push(sampleBar(symbol, time, step))
  return bars
}
export function createDemoDatafeed(options: { realtime?: boolean; empty?: boolean } = {}) {
  const timers = new Map<string, ReturnType<typeof setInterval>>()
  let disposed = false
  const later = (callback: () => void) => setTimeout(() => { if (!disposed) callback() }, 0)
  const datafeed: IBasicDataFeed = {
    onReady(callback) { later(() => callback({ supported_resolutions: resolutions, supports_marks: false, supports_timescale_marks: false, supports_time: true })) },
    getServerTime(callback) { later(() => callback(sampleEnd / 1000)) },
    searchSymbols(input, _exchange, _type, callback) {
      later(() => callback(symbols.filter(symbol => symbol.includes(input.toUpperCase())).map(symbol => ({ symbol, full_name: symbol, ticker: symbol, description: `${symbol} · synthetic sample`, exchange: 'DEMO', type: 'stock' }))))
    },
    resolveSymbol(name, success, failure) {
      if (!symbols.includes(name as typeof symbols[number])) { later(() => failure('unknown_symbol')); return }
      const info: LibrarySymbolInfo = { name, ticker: name, description: `${name} · sample data`, type: 'stock', exchange: 'DEMO', listed_exchange: 'DEMO', session: '24x7', timezone: 'Etc/UTC', minmov: 1, pricescale: 100, has_intraday: true, has_daily: true, supported_resolutions: resolutions, volume_precision: 0, format: 'price', data_status: 'streaming' }
      later(() => success(info))
    },
    getBars(info, resolution, period, success, failure) {
      try {
        const bars = options.empty ? [] : historicalBars(info.ticker ?? info.name, resolution, period.from, period.to, period.countBack)
        later(() => success(bars, { noData: bars.length === 0 }))
      } catch (error) { later(() => failure(error instanceof Error ? error.message : 'Sample data unavailable')) }
    },

    subscribeBars(info, resolution, callback, id) {
      datafeed.unsubscribeBars(id)
      if (!options.realtime || options.empty || disposed) return
      const step = resolutionMs(resolution)
      let sequence = 0
      const bar = sampleBar(info.ticker ?? info.name, sampleEnd - step, step)
      timers.set(id, setInterval(() => {
        sequence += 1
        const close = bar.close + Math.sin(sequence * .4) * bar.close * .0005
        callback({ ...bar, close, high: Math.max(bar.high, close), low: Math.min(bar.low, close), volume: (bar.volume ?? 0) + sequence * 100 })
      }, 1000))
    },
    unsubscribeBars(id) { const timer = timers.get(id); if (timer) clearInterval(timer); timers.delete(id) },

  }
  return { datafeed, activeSubscriptions: () => timers.size, dispose() { disposed = true; for (const id of timers.keys()) datafeed.unsubscribeBars(id) } }
}
```

## Your responsibilities in production

Your service should check that the user is signed in and allowed to see the market data before it returns anything. Keep API keys on your server. Convert vendor symbols, sessions and timestamps inside your datafeed. Report errors through the matching callback.

## Symbol search and metadata

`searchSymbols` returns the symbols that match a search. `resolveSymbol` returns enough detail for the chart to format prices, follow trading sessions and offer the right resolutions.

### Fields to fill in

| Field | Meaning |
| --- | --- |
| `name` / `ticker` | The label people see, and the stable ID used in requests |
| `description` | Text shown in search and on the chart |
| `exchange` / `listed_exchange` | Which market the symbol trades on |
| `type` | Kind of instrument |
| `session` | Trading hours; the sample uses `24x7` |
| `timezone` | IANA or exchange timezone supported by the contract |
| `minmov` / `pricescale` | Tick size and price formatting |
| `supported_resolutions` | Resolutions you can deliver for this symbol |
| `has_intraday` / `has_daily` | Whether you have intraday and daily data |
| `volume_precision` | Decimal places for volume |

For decimal prices, `minmov: 1` and `pricescale: 100` give a price step of 0.01. Check each instrument's real tick size rather than copying these values everywhere.

### Unknown symbols

If a symbol isn't found, call the error callback. Don't fall back to some other symbol. Search results should resolve to the same canonical ID they were listed under. If one symbol has several aliases, make sure they don't open competing subscriptions.

The sample uses the labels AAPL, MSFT and BTCUSD with made-up 24×7 data. Its session is only for illustration. It doesn't reflect the real trading hours of those instruments.

## Time units and trading sessions

Convert all times inside your datafeed. Passing a number without knowing its unit (seconds or milliseconds) is one of the most common integration mistakes.

| Value | Unit |
| --- | --- |
| Historical request `from` / `to` | Unix seconds |
| `Bar.time` | Epoch milliseconds |
| Public drawing point `time` | Unix seconds |
| Server time callback | Unix seconds |
| Visible time range | Use the public range contract; seconds |

### Sessions and timezone

Include the exchange session and timezone in your symbol metadata. Changing the display timezone doesn't change the original timestamps. A 24×7 sample feed can't stand in for a real exchange calendar.

Stamp daily bars at the UTC trading-day boundary the contract uses. Test weekends, session changes and daylight-saving changes against real records from your provider. Don't shift bars to match the browser's local timezone.

### Keep resolutions consistent

Every bar must line up with the resolution you advertised. Convert resolution aliases once, inside your datafeed, and use the same IDs for history and realtime subscriptions. If an interval isn't supported, reject it clearly.

## Event marks

Event marks add context such as news or earnings to specific dates or bars. Only provide the mark callbacks if you also say your datafeed supports marks.

### Bar marks and timescale marks

Bar marks sit on the chart's bars. Timescale marks sit on the time axis. Their data and IDs are kept separate from price history. The datafeed reference lists their callbacks.

Give marks stable IDs and valid timestamps. Keep labels short and descriptions useful. If marks show richer content, check provider URLs and text in your datafeed before they reach the chart.

### Refresh and clear

Call `refreshMarks()` to fetch the latest marks and `clearMarks(mode)` to clear one kind of mark. An empty list of marks is a valid answer. After a symbol or resolution change, ignore mark responses that arrive for the old one.

The public demo doesn't offer marks by default. Only turn them on when your provider actually implements the callbacks.

Next: [Historical bars](https://docs.lumencharts.com/charting-library/datafeed/history) · [Realtime updates](https://docs.lumencharts.com/charting-library/datafeed/realtime) · [Datafeed API](https://docs.lumencharts.com/charting-library/api/IDatafeedChartApi)

---
Documentation page: https://docs.lumencharts.com/charting-library/datafeed/
