Code example
Periodic Volume Profile
Build a periodic volume profile as a chart indicator. This example:
- Splits the chart into 40-candle blocks.
- Builds a Volume Profile for each block using
footprint.profile(). - Finds the highest traded volume across all blocks.
- Draws each profile normalized to that value.
- Renders a box showing the price range for each block.
// Periodic range boxes: 40-candle blocks, each with a high/low box and
// a left-adjusted volume profile whose width is a % of the block.
const BLOCK_CANDLES = 40
const PROFILE_PCT = 0.4 // longest profile bar = 40% of the block width
const blockMs = footprint.interval * BLOCK_CANDLES
const bucketKeys = Object.keys(footprint.buckets)
if (bucketKeys.length > 0) {
let minTs = Infinity
let maxTs = -Infinity
for (const key of bucketKeys) {
const ts = Number(key)
if (ts < minTs) minTs = ts
if (ts > maxTs) maxTs = ts
}
// First pass: gather each block's profile and track the global max per-price
// total across every block, so bar widths are comparable between segments
// (a bar's length reflects volume relative to the busiest level anywhere).
type Block = {
blockStart: number
blockEnd: number
index: number
profile: Record<string, { buys: number; sells: number; total: number }>
priceKeys: string[]
hi: number
lo: number
}
const blocks: Block[] = []
let globalMaxTotal = 0
let blockStart = Math.floor(minTs / blockMs) * blockMs
let index = 0
while (blockStart <= maxTs) {
const blockEnd = blockStart + blockMs
const profile = footprint.profile(blockStart, blockEnd - 1)
const priceKeys = Object.keys(profile)
if (priceKeys.length > 0) {
let hi = -Infinity
let lo = Infinity
for (const pk of priceKeys) {
const price = Number(pk)
if (price > hi) hi = price
if (price < lo) lo = price
if (profile[pk].total > globalMaxTotal) globalMaxTotal = profile[pk].total
}
blocks.push({ blockStart, blockEnd, index, profile, priceKeys, hi, lo })
}
blockStart += blockMs
index += 1
}
// Second pass: draw boxes and profiles normalized to the global max total.
if (globalMaxTotal > 0) {
for (const block of blocks) {
if (block.hi > block.lo) {
const even = block.index % 2 === 0
chart.box({
time1: block.blockStart,
price1: block.hi,
time2: block.blockEnd - footprint.interval,
price2: block.lo,
color: even ? "rgba(59, 130, 246, 0.08)" : "rgba(148, 163, 184, 0.08)",
borderColor: "rgba(148, 163, 184, 0.3)",
})
}
// Left-adjusted profile: widthTime scales the bar to a % of the block,
// relative to the global max total so blocks are directly comparable.
for (const pk of block.priceKeys) {
const price = Number(pk)
const { buys, sells, total } = block.profile[pk]
// onPress makes the bar clickable: pressing it shows the buys/sells behind its width.
const onPress = { text: `Buys: ${buys}
Sells: ${sells}` }
chart.horizontalBar({
price,
widthTime: (total / globalMaxTotal) * PROFILE_PCT * blockMs,
time: block.blockStart,
anchor: "time",
align: "right",
color: "rgba(239, 68, 68, 0.65)",
onPress,
})
chart.horizontalBar({
price,
widthTime: (buys / globalMaxTotal) * PROFILE_PCT * blockMs,
time: block.blockStart,
anchor: "time",
align: "right",
color: "rgba(59, 130, 246, 0.9)",
onPress,
})
}
}
}
}Each price level draws two bars with chart.horizontalBar: a red bar for total volume and a blue bar for buys on top of it. The onPress text reveals the buys and sells behind a bar's width when it is clicked.
Result
