VaultCharts
Trading Strategystockscryptofutures

Volume Profile Trading Strategy

Rolling volume profile (POC, 70% value area). Long when close crosses above VAL; short when close crosses below VAH.

What is Volume Profile Trading?

Rolling volume profile: POC (Point of Control), Value Area (70%). Long when price crosses above VAL (value area low – rotation bullish); short when crosses below VAH (value area high). Exits on opposite cross or value area flip. HVN/LVN concepts: trade bounces from value area and breakouts from low-volume zones.

Strategy Parameters

ParameterTypeDefaultDescription
lookbacknumber50Bars for volume profile
valueAreaPctnumber70Value area percentage
numRowsnumber100Price buckets for profile

Use Cases

  • POC and value area (VAH/VAL)
  • Rotation at value area
  • Breakouts from low volume
  • Stocks, crypto, futures

Strategy Script (JavaScript)

This strategy runs in VaultCharts using the built-in strategy engine. Below is the full script in a readable format. You can copy it or run it directly in VaultCharts.

strategy.jsVaultCharts built-in
module.exports = {
  meta: {
    name: "Volume Profile Trading",
    params: {
      lookback: { type: "number", default: 50 },
      valueAreaPct: { type: "number", default: 70 },
      numRows: { type: "number", default: 100 }
    }
  },
  compute: (data, params, utils) => {
    const cleanData = data.filter(d =>
      d && Number.isFinite(d.high) && Number.isFinite(d.low) && Number.isFinite(d.close) &&
      Number.isFinite(d.time) && (d.volume == null || Number.isFinite(d.volume)) &&
      d.high >= d.low && d.close > 0
    );
    const lookback = params?.lookback ?? 50;
    const valueAreaPct = (params?.valueAreaPct ?? 70) / 100;
    const numRows = params?.numRows ?? 100;
    if (!cleanData || cleanData.length < lookback + 5) return { signals: [] };

    function getPOCVAHVAL(slice) {
      if (slice.length === 0) return { poc: null, vah: null, val: null };
      let minP = slice[0].low, maxP = slice[0].high;
      for (let k = 0; k < slice.length; k++) {
        minP = Math.min(minP, slice[k].low);
        maxP = Math.max(maxP, slice[k].high);
      }
      if (minP === maxP) return { poc: minP, vah: minP, val: minP };
      const step = (maxP - minP) / numRows || 1e-9;
      const dist = new Array(numRows).fill(0);
      for (let k = 0; k < slice.length; k++) {
        const c = slice[k];
        const vol = c.volume || 1;
        const startB = Math.max(0, Math.floor((c.low - minP) / step));
        const endB = Math.min(numRows - 1, Math.floor((c.high - minP) / step));
        const buckets = endB - startB + 1;
        const vpb = vol / buckets;
        for (let b = startB; b <= endB; b++) dist[b] += vpb;
      }
      let pocIdx = 0;
      for (let b = 1; b < numRows; b++) if (dist[b] > dist[pocIdx]) pocIdx = b;
      const poc = (pocIdx + 0.5) * step + minP;
      const total = dist.reduce((a, b) => a + b, 0);
      const target = total * valueAreaPct;
      const sorted = dist.map((v, idx) => ({ v, idx })).sort((a, b) => b.v - a.v);
      let cum = 0;
      const indices = new Set();
      for (let s = 0; s < sorted.length && cum < target; s++) {
        cum += sorted[s].v;
        indices.add(sorted[s].idx);
      }
      const indArr = Array.from(indices);
      if (indArr.length === 0) return { poc, vah: poc, val: poc };
      const vah = (Math.max(...indArr) + 0.5) * step + minP;
      const val = (Math.min(...indArr) + 0.5) * step + minP;
      return { poc, vah, val };
    }

    const signals = [];
    let position = null;

    for (let i = lookback; i < cleanData.length; i++) {
      const candle = cleanData[i];
      const prev = cleanData[i - 1];
      const slice = cleanData.slice(i - lookback, i);
      const { poc, vah, val } = getPOCVAHVAL(slice);
      if (poc == null || vah == null || val == null) continue;

      const closeAboveVAL = candle.close > val && (prev.close <= val || prev.close === undefined);
      const closeBelowVAH = candle.close < vah && (prev.close >= vah || prev.close === undefined);
      const closeBelowVAL = candle.close < val;
      const closeAboveVAH = candle.close > vah;

      if (position === 'long') {
        if (closeBelowVAL) {
          signals.push({ type: "exit", direction: "long", time: candle.time, price: candle.close, index: i });
          position = null;
        }
        continue;
      }
      if (position === 'short') {
        if (closeAboveVAH) {
          signals.push({ type: "exit", direction: "short", time: candle.time, price: candle.close, index: i });
          position = null;
        }
        continue;
      }

      if (closeAboveVAL && candle.close > candle.open) {
        signals.push({ type: "entry", direction: "long", time: candle.time, price: candle.close, index: i });
        position = 'long';
      }
      if (closeBelowVAH && candle.close < candle.open) {
        signals.push({ type: "entry", direction: "short", time: candle.time, price: candle.close, index: i });
        position = 'short';
      }
    }
    return { signals };
  }
};

Run Volume Profile Trading in VaultCharts

VaultCharts includes this strategy as a built-in option. Backtest it, adjust parameters, and use it on your own data—all stored locally on your device.

Related Strategies

Explore More