// The session's volume profile from every trade since 00:00 UTC: where it developed its point of
// control (the price that traded most) and its value area (the 70% of volume around it), bar by
// bar, and each session's profile as a histogram from its first bar. Trades reach back to the
// start of the UTC day before yesterday, so the lines and histograms start there.
indicator('Session volume profile', { overlay: true, shortName: 'Session VP', maxBoxes: 200 });
const share = input.float('Value area (%)', 70, { min: 10, max: 100, step: 5 });
const showHistogram = input.bool('Histogram', true);
const histogramBars = input.int('Histogram width (bars)', 30, { min: 5, max: 200 });
const chart = bars();
const profile = volumeProfile();
const day = state.bar<number>(na);
const firstBar = state.bar<number>(0);
const high = plot.line('VAH', { color: color.study(12), style: 'dashed' });
const low = plot.line('VAL', { color: color.study(12), style: 'dashed' });
const poc = plot.line('POC', { color: color.study(0), width: 2 });
const band = plot.fill('Value area', high, low, { color: color.withAlpha(color.study(12), 0.06) });
/** 1, 2, 2.5 or 5 times a power of ten, at or above `raw`: rows that read as round prices. */
function roundUp(raw: number): number {
const power = Math.pow(10, Math.floor(Math.log10(raw)));
for (const step of [1, 2, 2.5, 5, 10]) if (step * power >= raw) return step * power;
return 10 * power;
}
/** A session's profile as boxes from its first bar: 48 rows, each as long as its volume allows. */
function histogram(session: Profile) {
const coarse = roundUp(Math.max((session.high - session.low) / 48, chart.close * 1e-6));
const rows = session.rows({ rowSize: coarse });
const area = session.valueArea(share, { rowSize: coarse });
if (rows === null || area === null) return;
let most = 0;
for (const row of rows) most = Math.max(most, row.total);
for (const row of rows) {
if (row.total === 0) continue;
const inside = row.low >= area.low && row.high <= area.high;
const isPoc = row.low <= area.poc && area.poc < row.high;
const length = Math.max(1, Math.round((row.total / most) * histogramBars));
draw.box({
from: { bar: firstBar.value, price: row.low },
to: { bar: firstBar.value + length, price: row.high },
fill: color.withAlpha(isPoc ? color.study(0) : color.study(12), isPoc ? 0.6 : inside ? 0.42 : 0.2),
border: null,
});
}
}
chart.onBar(() => {
const start = clock.startOf(chart.time, 'day');
// A new session breaks the lines: its first bar draws nothing, so one session never joins the next.
if (start !== day.value) {
day.value = start;
firstBar.value = chart.index;
return;
}
const session = profile.since(start);
if (!session.available || session.total === 0) return;
// About 200 rows over the session's range, so the lines move in small steps.
const fine = roundUp(Math.max((session.high - session.low) / 200, chart.close * 1e-6));
const area = session.valueArea(share, { rowSize: fine });
if (area === null) return;
high.set(area.high);
low.set(area.low);
poc.set(area.poc);
// Today's histogram grows with the session; a finished session's is drawn once, on its last bar.
if (showHistogram && chart.isLast) histogram(session);
});
chart.onClose(() => {
if (!showHistogram || chart.isLast) return;
const start = clock.startOf(chart.time, 'day');
if (clock.startOf(chart.time + chart.interval.ms, 'day') === start) return;
const session = profile.since(start);
if (session.available && session.total > 0) histogram(session);
});