Skip to content

Timeframes and replay ​

Feed the chart one stream of data, trades or bars at a base resolution such as one minute, and it builds every coarser timeframe from it, following the trading calendar of your market. The same source data drives bar replay.

Declare the source ​

Tell set() the resolution of your bars:

ts
chart.data.set(minuteBars, { interval: '1m' });

Without an interval, bars are displayed exactly as given and nothing below applies. With it, chart.data writes go to the source bars and reads (at(), last(), toArray(), length) return the displayed bars.

Intervals are written as a count and a unit: 15s, 1m, 5m, 1h, 4h, 1D, 2D, 1W, 1M, 3M (m is minute, M is month), or as objects like { unit: 'hour', count: 4 }.

Stream trades ​

ts
socket.onmessage = (message) => {
  const { time, price, size } = JSON.parse(message.data);
  chart.data.addTrades({ time, price, size });
};

A trade in the period of the last source bar updates its high, low, close and volume; a later trade opens a new bar. Trades for an earlier period throw, and trades the calendar drops (outside the sessions) are skipped: addTrades() returns how many were applied. You can start from no bars at all with chart.data.set([], { interval: '1s' }), and you can mix trades with update() calls of whole bars.

Switch timeframes ​

ts
chart.data.setTimeframe('15m');
chart.data.setTimeframe(null); // back to the source resolution

Switching re-aggregates the source in place: no refetch, and indicators, alerts and comparison series follow. A timeframe must be built from whole source bars:

SourceTimeframes
1m2m, 5m, 15m, 1h, 4h, 1D, 1W, 1M…
1h2h, 4h, 1D, 1W, 1M…
1D2D, 1W, 1M, 3M…
1M3M, 6M, 12M

90s from 1m, 1D from 7m (seven does not divide a day) or 1M from 1W throw. setTimeframe() also throws when no source interval was declared.

A displayed bar takes the first open, the highest high, the lowest low, the last close and the summed volume of its source bars. It is timed at the start of its period: intraday bars at the start of their slot, daily, weekly and monthly bars at local midnight of their trading date.

When the user scrolls back, loadMore passes the time of the first source bar, so you can keep fetching base bars:

ts
chart.on('loadMore', async ({ before }) => {
  chart.data.prepend(await api.minuteBars({ end: before, limit: 2000 }));
});

The trading calendar ​

The calendar option decides which period a time belongs to:

ts
createChart(element, {
  timeZone: 'Europe/London', // how times are displayed
  calendar: {
    timeZone: 'America/New_York', // where the market trades; defaults to timeZone
    sessions: ['09:30-16:00'],
    weekdays: [1, 2, 3, 4, 5]
  }
});
OptionDefaultMeaning
timeZonenullZone of the calendar; null uses the chart's timeZone
sessions[]'HH:MM-HH:MM' trading sessions; times outside them are dropped. Empty: around the clock
dayStart'00:00'Time a trading day starts; later times belong to the next date
weekdaysallTrading weekdays (0 = Sunday); other dates move to the next trading weekday
weekStart1First day of a week bar
bucketnullHook that overrides the period of a time, see below

Intraday bars never cross a session boundary: they start at the session's start, so 60-minute bars of ['09:30-11:30', '13:00-15:00'] start at 09:30, 10:30, 13:00 and 14:00. An end before the start crosses midnight ('21:00-02:30'). Daylight saving changes are followed.

Examples ​

ts
// Crypto: around the clock, 4-hour bars aligned to UTC.
const crypto = { timeZone: 'UTC' };

// China A-shares: a lunch break.
const aShares = {
  timeZone: 'Asia/Shanghai',
  sessions: ['09:30-11:30', '13:00-15:00'],
  weekdays: [1, 2, 3, 4, 5]
};

// China futures: the night session belongs to the next trading day, Friday night to Monday.
const chinaFutures = {
  timeZone: 'Asia/Shanghai',
  sessions: ['21:00-02:30', '09:00-10:15', '10:30-11:30', '13:30-15:00'],
  dayStart: '21:00',
  weekdays: [1, 2, 3, 4, 5]
};

// US equities: regular hours only, pre- and post-market trades are dropped.
const usEquities = {
  timeZone: 'America/New_York',
  sessions: ['09:30-16:00'],
  weekdays: [1, 2, 3, 4, 5]
};

// FX: the day rolls over at 17:00 New York time.
const fx = { timeZone: 'America/New_York', dayStart: '17:00', weekdays: [1, 2, 3, 4, 5] };

createChart(element, { calendar: chinaFutures });

Holidays and special rules ​

bucket receives the time, the interval and fallback, the built-in result for the same time. Return a period start, or null to drop the time. Periods must not go back in time as times advance.

ts
const holidays = new Set(['2024-10-01', '2024-10-02', '2024-10-03']);

chart.applyOptions({
  calendar: {
    bucket: (time, interval, fallback) => {
      const date = new Date(time + 8 * 3_600_000).toISOString().slice(0, 10);
      return holidays.has(date) ? null : fallback(time);
    }
  }
});

Changing the calendar re-aggregates the data.

Align your own data ​

createCalendar() returns the calendar the chart uses, for server responses, exports or your own aggregations:

ts
import { createCalendar } from '@kumochart-sdk/core';

const calendar = createCalendar({
  timeZone: 'Asia/Shanghai',
  sessions: ['09:30-11:30', '13:00-15:00']
});
calendar.bucket('2024-05-06T10:15:00+08:00', '1h'); // 09:30 Shanghai time, as epoch milliseconds
calendar.bucket('2024-05-06T12:00:00+08:00', '1h'); // null: lunch break

Bar types ​

A transform builds the displayed bars from the bars of the timeframe: Heikin-Ashi candles, Renko bricks or range bars. It sits after the timeframe, so Renko on a 5-minute timeframe is built from 5-minute closes, and it stays when the data or the timeframe changes. The three are extras: register them once, all together or one by one.

ts
import { registerExtras } from '@kumochart-sdk/core';

registerExtras(chart);
chart.data.setTransform({ type: 'heikin-ashi' });
chart.data.setTransform({ type: 'renko', params: { box: 50 } }); // bricks of 50
chart.data.setTransform({ type: 'range', params: { range: 120 } }); // bars spanning 120
chart.data.setTransform({ type: 'renko' }); // box from the average true range
chart.data.setTransform(null); // the timeframe's bars again
TypeParameterDisplayed bars
heikin-ashiOne smoothed candle per bar: the close averages OHLC, the open is the middle of the previous one
renkoboxBricks on the closes, on a grid of multiples of the box; a reversal takes two boxes
rangerangeBars spanning exactly range from high to low, walking each bar open, low, high, close (rising)
  • Without a size, renko and range use the average true range of the last 14 bars, rounded to two significant digits. The size holds while bars stream in and is computed again when the bars are rebuilt: new data, another timeframe or a replay start.
  • Indicators, alerts, markers and replay work on the transformed bars. A tick only re-runs the last bar of the timeframe, so streaming stays incremental.
  • A Renko brick is timed when it starts; bricks starting in the same bar are a millisecond apart. Renko and range bars are not periods of time, so the bar countdown is hidden.
  • The main series type still decides how the bars are drawn: candles suit every transform, a line of Renko closes works too.

Custom transforms ​

A transform is a stream: it receives the bars of the timeframe in order, returns the bars each one completes, and reports the bar still forming. clone() lets the chart re-run only the latest bar on every tick.

ts
import type { Bar, BarTransformStream } from '@kumochart-sdk/core';

/** A bar every `volume` traded. */
class VolumeBars implements BarTransformStream {
  constructor(
    private readonly volume: number,
    private forming: Bar | null = null
  ) {}

  next(bar: Bar): Bar[] {
    const merged = this.forming
      ? {
          ...this.forming,
          high: Math.max(this.forming.high, bar.high),
          low: Math.min(this.forming.low, bar.low),
          close: bar.close,
          volume: this.forming.volume + bar.volume
        }
      : bar;
    this.forming = merged.volume >= this.volume ? null : merged;
    return this.forming ? [] : [merged];
  }

  current(): Bar | null {
    return this.forming;
  }

  clone(): BarTransformStream {
    return new VolumeBars(this.volume, this.forming);
  }
}

chart.data.registerTransform<{ volume: number }>('volume', {
  name: 'Volume bars',
  keepsTime: false,
  create: ({ volume }) => new VolumeBars(volume)
});
chart.data.setTransform({ type: 'volume', params: { volume: 5000 } });

create(params, input) validates the parameters and may read the bars it is about to receive (input) for statistics such as an automatic size. Set keepsTime when every input bar becomes one output bar with the same time, like Heikin-Ashi; the chart then keeps the bar countdown. Output times must not decrease, and every value must be a finite number.

Replay ​

Replay hides the source data after a time cursor. Everything sees only the revealed past: the timeframe, indicators, alerts, comparison series and markers. On a coarser timeframe the bar in progress forms from the revealed source bars, so replaying minute data on an hourly chart shows each hour being built.

ts
chart.replay.start('2024-05-06T10:00:00+08:00', { speed: 2, step: 'bar' });
chart.replay.play();

chart.replay.pause();
chart.replay.step(); // one bar
chart.replay.configure({ step: 'source' });
chart.replay.step(5); // five source bars
chart.replay.seek('2024-05-07T09:30:00+08:00'); // forward or back

chart.replay.stop(); // shows everything again
  • step: 'bar' completes the displayed bar in progress or reveals the next one; step: 'source' reveals one source bar.
  • speed is in steps per second while playing.
  • state() returns { time, playing, ended, speed, step }, or null when the chart shows all data.
  • Live data keeps arriving while replaying (update(), addTrades()); it stays hidden until the cursor reaches it. Switching timeframes keeps the cursor.
  • Alerts fire as bars are revealed, with replay: true in alertTrigger.
  • clear() ends the replay.

Playback reports its progress; the calls above do not emit events, like every other API call:

ts
chart.on('replay', ({ state, reason }) => {
  progress.value = state.time;

  if (reason === 'end') {
    showSummary();
  }
});

A typical replay UI lets the user click the starting bar:

ts
chart.on('click', ({ time }) => {
  if (pickingStart && time !== null) {
    chart.replay.start(time);
    chart.replay.step(); // complete the clicked bar
    pickingStart = false;
  }
});

Vue and React ​

Both components take the source interval, the timeframe and the transform as props:

vue
<TradingChart
  :data="minuteBars"
  interval="1m"
  :timeframe="timeframe"
  :configure="registerExtras"
  :transform="{ type: 'renko' }"
/>
tsx
<TradingChart
  data={minuteBars}
  interval="1m"
  timeframe={timeframe}
  configure={registerExtras}
  transform={{ type: 'renko' }}
/>

An equal transform passed again, such as an object written inline, does not rebuild the bars. configure registers the extras before the transform applies.

Stream trades and drive replay through the chart instance (@ready / onReady or a ref); the replay event is forwarded like the others.

Released under the MIT license.