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.timefeeds straight intochart.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
30sbase. - Prices are real prices (e.g.
5000.25), like everywhere else in the API - never scale them.
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:
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].pricecandle.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.
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
startMsis the same number ascandle.time. On a tick interval (133t)timecounts 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
timestampcan go straight into a drawing on any interval - no conversion, no flooring.
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
volumeByPriceof 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-
nonetrades 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).
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