# Lumen Charts documentation

Public integration documentation for a proprietary library. Runtime access is separately authorized. Exported API signatures do not imply that all legacy capabilities are supported; preserve compatibility notes when using these documents.

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


---

<!-- Public documentation export; not library implementation source. -->
# Historical bars and countBack

`getBars(symbolInfo, resolution, period, onHistory, onError)` returns OHLCV bars in time order. `period.from` and `period.to` are in Unix seconds, but each bar's `time` is in epoch milliseconds.

## Return the history the chart asks for

The time window is `[from, to)`: it includes `from` and excludes `to`. `countBack` is the number of bars the chart actually needs, so treat it as the main target. If the window has too few bars, add earlier bars when you have them. If the window has more bars than `countBack`, it's fine to return them all; don't cut the response short.

Return bars with unique timestamps in ascending order and OHLC values that make sense. Daily, weekly and monthly bars are stamped at 00:00 UTC on the trading day. Don't use a local session-open time.

## Match historical and realtime boundaries

History and realtime updates must use the same interval boundaries, symbol, and price basis. For UTC-aligned H1 bars, each candle starts at `11:00`, `12:00`, and so on. A tick at `11:06` updates the candle starting at `11:00`; it doesn't start a new hour-long candle at `11:06`.

The chart uses the timestamps your datafeed supplies. It doesn't automatically realign provider candles. Align the provider's aggregation request **before** it calculates OHLCV. Rounding a returned candle's timestamp cannot correct its contents: a candle covering `11:06–12:06` contains different trades or quotes from one covering `11:00–12:00`.

### Example: Polygon minute multipliers

Polygon's `60/minute` aggregation can preserve a request's starting minute. A request starting at `11:06` can therefore produce H1 candles at `11:06`, `12:06`, and so on. For fixed UTC intervals, align the request start to the same boundary used by your live aggregator:

```js
const intervalMs = 60 * 60_000; // H1; use 15 * 60_000 for M15.
const fromMs = period.from * 1000; // getBars uses seconds.
const alignedFromMs = Math.floor(fromMs / intervalMs) * intervalMs;
const inclusiveToMs = period.to * 1000 - 1; // Convert [from, to) for this provider.

// Use alignedFromMs and inclusiveToMs in the provider's aggregate request.
// Keep the returned OHLCV values and candle-start timestamps together.
```

This may request an earlier candle that overlaps `period.from`; account for that when selecting enough history to satisfy `countBack`. Providers can also expand request boundaries or limit the number of base aggregates they scan. Validate the returned timestamps against your exclusive `to`, page as needed, and return bars in ascending order. See [Polygon/Massive's aggregation rules](https://massive.com/blog/aggs-api-updates) for its snapping and stretching behavior.

The [tick-to-candle helper](https://docs.lumencharts.com/charting-library/datafeed/tick-aggregation#match-your-history-boundaries) uses fixed UTC boundaries by default. For exchange-session or calendar-based intervals, use a shared market-calendar policy for historical aggregation and the helper's `bucketStart` callback instead of assuming fixed UTC blocks.

## "No more data" versus "something went wrong"

```js
onHistory([], { noData: true }); // no older data exists
onError('History is temporarily unavailable'); // request failed
```

A temporary error doesn't mean the history has run out. If you return only a few bars, the chart may ask for older data a limited number of times. When your provider pages its results, keep fetching pages until you have enough bars or you've truly reached the start of the data.

## Handle late responses safely

Keep track of which symbol, resolution and request each response belongs to. An old request that finishes late shouldn't overwrite newer data. Cache keys should include the provider and any metadata that changes what a bar means.

[Runnable example](https://docs.lumencharts.com/examples?example=minimal) · [Time and sessions](https://docs.lumencharts.com/charting-library/datafeed#time-units-and-trading-sessions)

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


---

<!-- Public documentation export; not library implementation source. -->
# Realtime subscriptions

`subscribeBars` hands you a subscriber ID and a callback. Keep separate state for each ID, because several charts can subscribe at the same time, to the same data or to different data.

## Choose your data path

The chart works with any data provider and any connection type. Your adapter takes care of the socket, sign-in, reconnects, symbol mapping and cleanup. The chart only ever receives **complete bars**, never raw WebSocket messages.

| Provider sends | Adapter does | Chart receives |
| --- | --- | --- |
| Raw trades or quotes | Normalize ticks, then call `createTickCandleAggregator` | Complete updated OHLCV bar |
| Complete candles for the selected resolution | Normalize and validate candles; skip tick aggregation | Complete updated OHLCV bar |

```text
Raw ticks → normalize → tick-to-candle helper → onRealtime(bar) → chart
Complete candles → normalize/validate → onRealtime(bar) → chart
```

Load the starting history with `getBars` before you send live updates. History and live updates need to agree on the symbol, the price basis, where intervals start and end, and Unix millisecond timestamps. Send each event to the subscription with the matching symbol and resolution, and keep separate state for each subscriber ID.

## Example: raw ticks

This M5 (5-minute) example starts the current candle from the last history bar, then updates it on every accepted tick. `transport.subscribe` is an adapter method that supplies normalized ticks and returns a cleanup function. `lastHistoryBar` and `onRealtime` come from your datafeed's history and subscription lifecycle.

```js
const candles = LumenChartingLibrary.createTickCandleAggregator({
  intervalMs: 5 * 60_000,
  volumeMode: 'incremental', // Each trade supplies a volume delta.
});
candles.reset({ ...lastHistoryBar, volume: lastHistoryBar.volume ?? 0 });

let active = true;
const stop = transport.subscribe(symbol, tick => {
  if (!active) return;
  // tick = { time: Unix milliseconds, price: trade price, volume: trade size }
  const update = candles.push(tick);
  if (update) onRealtime({ ...update.bar });
});

function unsubscribe() {
  active = false;
  stop();
}
```

If your feed only sends quotes, use `volumeMode: 'none'` and stick to one price basis, such as the bid/ask midpoint. Don't pass a running (cumulative) volume total as if it were the volume of a single tick.

With M5, a tick at 10:02 updates the candle that started at 10:00. The open stays the same, high and low stretch to the extremes, and close follows the latest price. The first tick at or after 10:05 starts a new candle and returns the finished one as `closedBar`. If no ticks arrive, no candle is created: there's no timer. M1 works the same way with `intervalMs: 60_000`.

See [Build candles from ticks](https://docs.lumencharts.com/charting-library/datafeed/tick-aggregation) for the full `subscribeBars` implementation, provider normalization, calendar alignment, and reconciliation.

## Example: complete candles

If your provider already streams complete candles for the timeframe the chart asked for, pass them straight to the callback. Don't turn their close prices into fake ticks, because you'd lose the open, high, low and volume.

In this example, the transport subscribes to the requested symbol and resolution. It checks that the OHLCV fields are numbers and converts timestamps before calling its handler. Keep provider-specific field names inside the transport.

```js
let active = true;
let latestTime = lastHistoryBar.time;
const stop = candleTransport.subscribe(symbol, resolution, candle => {
  if (!active || candle.time < latestTime) return;
  latestTime = candle.time;
  onRealtime({
    time: candle.time, // Candle START in Unix milliseconds, not arrival time.
    open: candle.open,
    high: candle.high,
    low: candle.low,
    close: candle.close,
    volume: candle.volume,
  });
});

function unsubscribe() {
  active = false;
  stop();
}
```

Pass along every update to the candle that's still forming. Don't wait for the provider to mark it “closed”. If your feed only sends finished candles, the chart can't show price movement inside a bar this way. If the feed sends M1 candles but the chart asks for M5, combine them into M5 candles first, keeping the highs, lows and volume. Never pass M1 bars off as M5 bars.

## Update the current bar

Whenever the price or volume changes, send the whole current OHLCV bar. A bar with the same timestamp replaces every OHLCV value of the last bar. It isn't a partial update of just the changed fields. A newer timestamp starts the next bar. Don't use the realtime callback to correct older history.

## Unsubscribe reliably

```js
const subscriptions = new Map();
function unsubscribeBars(id) {
  subscriptions.get(id)?.close();
  subscriptions.delete(id);
}
```

If several subscribers share a connection, count how many are using each underlying feed, but keep each subscriber's state separate. When the last one leaves, release the timers, sockets and listeners. Ignore any events that arrive after unsubscribing.

## Historical corrections

When older data changes, use the documented cache reset and reload flow. After the provider reconnects, bring the current bar up to date before you resume updates. Never fill a gap with made-up market bars.

The example below updates a sample bar once per second. It only simulates live updates; it isn't connected to a real market.

[Interactive chart example](https://docs.lumencharts.com/charting-library/datafeed/realtime) — runs in a browser with synthetic data.

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


---

<!-- Public documentation export; not library implementation source. -->
# Build candles from ticks

`createTickCandleAggregator` turns a stream of price ticks into the complete OHLCV bars that `subscribeBars` needs. You'll find it in the licensed browser bundle as `LumenChartingLibrary.createTickCandleAggregator`, and as a named export from the ESM and CommonJS entry points. It doesn't need any provider SDK or framework. The licensed distribution also includes a small standalone script, `assets/tick-candle-aggregator.js`. It exposes `LumenChartingLibraryDatafeed.createTickCandleAggregator` so you can use the helper without loading a chart widget.

The helper only builds candles. Your adapter still handles symbol mapping, WebSocket sign-in, reconnects, history requests and subscription cleanup. Create **one aggregator for each subscriber ID**, even when subscribers share a socket.

## Quick start

If your provider already sends complete candles for the requested timeframe, skip this helper and use the [complete-candle example](https://docs.lumencharts.com/charting-library/datafeed/realtime#example-complete-candles). Both paths feed the same realtime callback.

```js
const candles = LumenChartingLibrary.createTickCandleAggregator({
  intervalMs: 5 * 60_000,
  volumeMode: 'incremental',
});

candles.push({ time: 1_000, price: 100, volume: 2 });
candles.push({ time: 2_000, price: 103, volume: 1 });
const current = candles.push({ time: 3_000, price: 101, volume: 4 });
// current.bar: { time: 0, open: 100, high: 103, low: 100, close: 101, volume: 7 }
// current.isNewBar: false

const next = candles.push({ time: 300_000, price: 102, volume: 1 });
// next.bar.time: 300000; next.isNewBar: true
// next.closedBar: the completed candle starting at 0
```

All times are Unix **milliseconds**, candle timestamps included. By default a candle starts at `floor(tick.time / intervalMs) * intervalMs`. The open stays the same, high and low stretch to the extremes, and close follows the latest accepted tick. The first tick in a later interval returns the finished candle as `closedBar`. The helper has no timer, so it never calls back during a quiet interval and never invents candles to fill gaps.

## Match your history boundaries

The helper's default UTC buckets must match the historical candles you supply through `getBars`. With `intervalMs: 3_600_000`, ticks at `11:06` and `11:59` update the H1 candle starting at `11:00`. The first tick at or after `12:00` starts the next candle.

Align historical aggregation requests to those boundaries too. For example, a Polygon `60/minute` request starting at `11:06` can produce offset candles that don't match the helper's whole-hour buckets. See the [historical alignment example](https://docs.lumencharts.com/charting-library/datafeed/history#example-polygon-minute-multipliers) for the request conversion.

Don't fix a mismatch by rounding returned candle timestamps or passing a misaligned seed to `reset` or `reconcile`. Those methods don't rebuild OHLCV from the original trades or quotes. Request correctly aligned history, or aggregate the underlying data using the same boundary policy. The chart itself doesn't realign your provider's candles.

For exchange-session or calendar-based intervals, configure [custom boundaries](https://docs.lumencharts.com/charting-library/datafeed/tick-aggregation#exchange-sessions-and-calendar-bars) with `bucketStart` and use the same calendar policy in your history adapter.

## API

| Method / option | Behavior |
| --- | --- |
| `intervalMs` | Required positive integer. Supports fixed durations, including seconds, minutes, and UTC days. |
| `volumeMode` | `'none'` (default) ignores tick volume; `'incremental'` adds each tick's volume delta. |
| `bucketStart(time)` | Optional function returning an integer bucket start at or before the tick. Use for session or calendar alignment. |
| `push({ time, price, volume? })` | Returns `{ bar, isNewBar, closedBar? }`, or `null` for an older tick/bucket. |
| `reset(lastBar?)` | Clears ordering and current state, optionally seeding a complete history bar. |
| `reconcile(bar, preserveLive = true)` | Merges a history/provider snapshot. Same-bucket live high/low and close are preserved by default; volume is replaced. Older bars return `null`. |
| `snapshot()` | Returns a frozen copy of the current bar, or `null`. |

`bar` contains `time`, `open`, `high`, `low`, `close` and `volume`. Changing an update or snapshot can't affect the helper's internal state. Invalid timestamps, non-finite prices, negative volume, impossible OHLC values or invalid options all throw an error before anything changes. Negative prices are allowed for markets that have them, as long as they're finite.

Ticks with the same timestamp are applied in the order they arrive. The helper can't spot duplicate provider events without an ID, so remove duplicate trade IDs before passing incremental volume. Don't pass a running session or per-minute volume total as if it were the volume of a single tick.

## Connect it to subscribeBars

This example assumes your history adapter caches its latest bar per symbol/resolution, and your transport's `subscribe` method returns an unsubscribe function. The socket can be shared; each subscriber still gets its own aggregator.

```js
const subscriptions = new Map();
const lastHistoryBars = new Map(); // Populate from successful getBars responses.
const intervals = { '1': 60_000, '5': 300_000, '15': 900_000, '60': 3_600_000, '1D': 86_400_000 };

function subscribeBars(symbolInfo, resolution, onRealtime, subscriberId) {
  unsubscribeBars(subscriberId);
  const symbol = symbolInfo.ticker || symbolInfo.name;
  const intervalMs = intervals[resolution];
  if (!intervalMs) throw new Error(`Unsupported resolution: ${resolution}`);
  const candles = LumenChartingLibrary.createTickCandleAggregator({ intervalMs });
  const last = lastHistoryBars.get(`${symbol}|${resolution}`);
  if (last) candles.reset({ ...last, volume: last.volume ?? 0 });

  const entry = { candles, close: () => {} };
  subscriptions.set(subscriberId, entry);
  entry.close = transport.subscribe(symbol, tick => {
    if (subscriptions.get(subscriberId) !== entry) return;
    const update = candles.push(tick);
    if (update) onRealtime({ ...update.bar });
  });
}

function unsubscribeBars(subscriberId) {
  const entry = subscriptions.get(subscriberId);
  subscriptions.delete(subscriberId);
  entry?.close();
}
```

The library gets a full bar every time. If the timestamp is the same, the current candle is updated; if it's newer, a new candle is added. How often ticks arrive has nothing to do with the selected timeframe.

## Normalize different providers

The aggregator never sees provider-specific messages. Your transport translates them first:

```js
// Provider A: executed trade, already timestamped in milliseconds.
const tradeTick = { time: message.timestampMs, price: message.tradePrice, volume: message.tradeSize };

// Provider B: bid/ask quote, timestamped in seconds. Choose a consistent price basis.
const quoteTick = { time: Math.trunc(message.timestampSeconds * 1000), price: (message.bid + message.ask) / 2 };

// Provider C: cumulative volume is NOT a per-trade delta. Omit it here.
const priceTick = { time: message.timeMs, price: message.lastPrice };
```

Check provider message formats and trade conditions in your transport. Keep in mind that a forex quote's midpoint isn't an actual trade. Also, providers sometimes leave certain stock trades out of their aggregate bars. If you include those trades, your live OHLC can differ from the provider's historical bars until they're reconciled.

## Volume and history reconciliation

For quote feeds, or feeds that mix ticks with aggregate bars, keep the default volume mode. Apply provider OHLCV snapshots with `reconcile`. Their volume replaces the current volume, so it isn't counted twice.

```js
const merged = candles.reconcile(providerBar);
if (merged) onRealtime({ ...merged });
```

Sometimes a snapshot for the current interval should win over the live values. In that case, call `reconcile(providerBar, false)`. This also resets tick ordering, so only do it once you've decided the snapshot replaces the live data.

Don't call `reset` on every tick. Seed the helper once before subscribing, then use `reconcile` only for history or aggregate updates. When you reconnect, fetch any missing history and use your subscription ID or a generation counter to ignore results that arrive too late. The helper can't tell whether a REST response was fetched before or after a given WebSocket tick. If older candles need correcting, resetting the chart's history cache is still up to your adapter.

## Exchange sessions and calendar bars

By default, intervals are fixed blocks of UTC time. That doesn't account for exchange holidays, daylight-saving changes, weekly or monthly calendars, or bars based on tick counts. If any of those apply, pass a `bucketStart` function that uses your market calendar. Its results must match the timestamps your history provider uses.

```js
const candles = LumenChartingLibrary.createTickCandleAggregator({
  intervalMs: 5 * 60_000,
  bucketStart(time) {
    const sessionStart = marketCalendar.sessionStartFor(time);
    return sessionStart + Math.floor((time - sessionStart) / 300_000) * 300_000;
  },
});
```

Filter out ticks from outside the trading session before calling `push`. The helper keeps prices exactly as precise as your adapter sends them. Rounding and display precision are up to your provider and display settings.

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