Lumen ChartsLumen Charts
Connect data

The datafeed contract

The callbacks your datafeed implements, plus symbol search and metadata, time and sessions, and event marks.

Download Markdown

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.

// 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

FieldMeaning
name / tickerThe label people see, and the stable ID used in requests
descriptionText shown in search and on the chart
exchange / listed_exchangeWhich market the symbol trades on
typeKind of instrument
sessionTrading hours; the sample uses 24x7
timezoneIANA or exchange timezone supported by the contract
minmov / pricescaleTick size and price formatting
supported_resolutionsResolutions you can deliver for this symbol
has_intraday / has_dailyWhether you have intraday and daily data
volume_precisionDecimal 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.

ValueUnit
Historical request from / toUnix seconds
Bar.timeEpoch milliseconds
Public drawing point timeUnix seconds
Server time callbackUnix seconds
Visible time rangeUse 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 · Realtime updates · Datafeed API

On this page