Candles

The chart's candles as data: a plain array, oldest first, at the chart's display interval. Each candlestick carries its OHLC and its per-price traded volume, split by aggressor side. Unlike tape.tick() (this update only), this is the running history - the data source for volume profiles, VWAP, and per-candle stats.

candles

An ordinary array, so slice, filter and reduce are the API. candles.at(-1) is the newest candle.

  • Aligned with the rendered columns for both time intervals (30s, 1m, ...) and tick intervals (133t, ...), so candle.time feeds straight into chart.mark() / panel.cells() times.
  • When the indicator is enabled on a chart this is that chart's live interval. In the code panel there is no chart binding, so it falls back to the 30s base.
  • Prices are real prices (e.g. 5000.25), like everywhere else in the API - never scale them.
API
candles // Candlestick[], oldest first

interface Candlestick {
  time: number       // candle start (ms)
  interval: number   // ms this candle covers
  increment: number  // instrument tick size, real price step
  ohlc: { open: number; high: number; low: number; close: number } | null
  levels: { price: number; buys: number; sells: number }[]
}

candle.levels

The candle's per-price traded volume, sorted by price ascending. One entry per price level that traded; buys / sells are executed volume by aggressor side.

Everything else is a fold over this array - derived stats deliberately have no fields of their own, so there is exactly one source of truth:

Derived stats
const volume = candle.levels.reduce((s, l) => s + l.buys + l.sells, 0)
const delta = candle.levels.reduce((s, l) => s + l.buys - l.sells, 0)

// POC: the highest-volume price level of the candle
const poc = candle.levels.reduce((a, b) =>
  b.buys + b.sells > a.buys + a.sells ? b : a
)

// The candle's traded range (levels is sorted by price)
const lo = candle.levels[0].price
const hi = candle.levels[candle.levels.length - 1].price

candle.ohlc

Real open/high/low/close for the candle, built from the same trade stream as levels. null when the dataset carries no OHLC for that bucket - guard before reading, don't treat zeros as prices.

High/low can extend past the traded levels: a trade with an unclassified aggressor moves price (and OHLC) but is never attributed to buys or sells.

Example
for (const candle of candles) {
  if (!candle.ohlc) continue
  if (candle.ohlc.close > candle.ohlc.open) {
    // up candle
  }
}

candle.interval / candle.increment

candle.interval is the display interval in ms - the width of one candle column, e.g. for a chart.box spanning whole candles. On tick intervals it is the synthetic per-candle span, so spans built from it still line up with the rendered columns.

candle.increment is the instrument tick size as a real price step (e.g. 0.25 for ES) - one price level is one increment apart. Use it to size drawings to a row, e.g. a box spanning price ± increment / 2.

candle.startMs / candle.endMs

The candle's span in market time: the clock the replay runs on, the one the tape is stamped with. This is what matches a trade to a candle.

  • On a time interval startMs is the same number as candle.time. On a tick interval (133t) time counts trades rather than ms, so these are the candle's only real timestamps.
  • Anything you draw takes market time too, so a trade's own timestamp can go straight into a drawing on any interval - no conversion, no flooring.
Example: the biggest trade in each candle
for (const c of candles.slice(-20)) {
  const inCandle = tape.range(c.startMs, c.endMs)
  if (inCandle.length === 0) continue

  const biggest = inCandle.reduce((a, b) => (b.size > a.size ? b : a))
  chart.mark({
    price: biggest.levels[0].price,
    time: biggest.timestamp,
    radius: 5,
    onPress: { text: `biggest ${biggest.size}` },
  })
}

volumeByPrice()

The volume-by-price histogram both data sources share: takes anything carrying levels - candlesticks or tape trades - and folds it into rows of { price, buys, sells }, sorted by price ascending.

  • A volume profile is volumeByPrice of whatever slice you care about - the session, the last N candles, only up-candles.
  • Tape trades attribute their fills by the trade's side. Side-none trades are skipped, matching candle levels, so a tape-derived profile equals a candle-derived one over the same range.
  • Filtering the tape first is the profile footprint data can never express: per-trade attribution is gone once trades are summed into a level.
  • Pair it with chart.horizontalBar() to render the profile (see the Chart API).
API
volumeByPrice(items) // { price, buys, sells }[], sorted by price

volumeByPrice(candles)                 // whole loaded session
volumeByPrice(candles.slice(-40))      // last 40 candles
volumeByPrice(candles.filter((c) => {  // up-candles only
  return c.ohlc !== null && c.ohlc.close > c.ohlc.open
}))
volumeByPrice(tape.range(fromMs, toMs))          // from individual trades
volumeByPrice([...tape].filter((t) => t.size >= 50)) // big trades only