Volume Profile

Description

SXVNT Volume Profile + Session Profile Two professional-grade profile tools in one script. Built native for SXVNT. Stop running three indicators to do what one should. This is one script, two independent profiles, fully click-to-place, with bid/ask delta pulled straight from your footprint data. --- ## What's Inside ### 🎯 Rolling Volume Profile A continuously updating profile across your last N bars. Set it once, watch it breathe with the market. - **POC, VAH, VAL** with the standard 70% expanding-window value area calculation - **HVN / LVN classification** — high-volume nodes show up bold, low-volume nodes (the gaps price rips through) fade out so you can spot them at a glance - **Lookback from 20 to 2000 bars** — micro scalper profiles or full-day context, your call ### 📍 Session Profile A profile bound to a specific range. Two ways to define it: **Preset sessions — one click, done:** - Asia (20:00 – 00:00 ET) - London (02:00 – 05:00 ET) - NY AM (07:00 – 10:00 ET) - NY Lunch (12:00 – 13:00 ET) - NY PM (13:30 – 16:00 ET) - RTH (CME) — 09:30 – 16:00 ET - ETH (CME) — full extended session - Globex — overnight session **Click Range mode — total control:** - Click two points anywhere on the chart - Drag the anchor handles to fine-tune the range in real time - Anchors stay locked to (timestamp, price), so they don't drift as new bars print - Profile rebuilds instantly with each adjustment --- ## 🔥 Key Features ### Standard + Delta Modes Toggle between two analysis modes per profile: - **Standard** — total volume per price row, classified by HVN/LVN intensity - **Delta (Bid/Ask)** — splits buying vs selling at every price level using your footprint data. Two-tone bars show you instantly where buyers absorbed sellers, where supply overwhelmed demand, and where the auction was actually balanced ### Three Anchor Layouts Place the histogram wherever it fits your workflow: - **Right edge → extend left** — overlays the candles, classic look - **Right edge → extend right** — protrudes past the last bar into open space, candles untouched - **Left edge → extend right** — anchors to the start of the lookback, grows over the candles Each profile has its own anchor setting, so you can run the rolling profile on one side and the session profile on the other without overlap. ### POC, VAH, VAL Lines Auto-extend across the range with optional right-side extension into live price action. Color flips based on whether price is above or below the POC, so you always know which side of value you're trading. ### SXVNT Palette Electric cyan, neon teal. Designed to sit on top of your charts without fighting them. --- ## 🧠 Why This Matters The auction is the only thing that actually happens on a chart. Every other tool is a derivative of *where contracts changed hands*. Volume Profile is the source. The POC is the price the market agreed on most — a magnet. Value Area High and Low are your fair-value boundaries. Low-volume nodes are where the auction broke down — price rips through them, then often returns to test them. Session Profile lets you isolate a specific session's auction, then watch how price reacts to *that session's* POC the next day, the next week, the next month. Click two points to profile a specific impulse leg. Drop a session profile on the overnight range to see where Globex agreed before RTH opens. Stack a rolling profile on the right margin while a click-anchored profile sits over a recent reaction zone. The flexibility is the point. --- ## ⚙️ Built Right - **Sandbox-safe** — no `Date`, no `window`, no async, no random, no side effects. Same inputs always produce the same output. - **Pure render plan** — never repaints, never lags. Click anchors persist as `(timestamp, price)` and resolve to bar indices at execution time, so they stay stable even as new bars print or old ones scroll off. - **Native footprint integration** — Delta mode reads directly from `ctx.footprint.bar(i)` for true bid/ask split, not estimated. - **Configurable everything** — 25+ inputs covering lookback, row count, value area %, HVN/LVN thresholds, profile width, colors for every element, label visibility, range shading. --- ## 🚀 Quick Start 1. Add the script to your chart 2. Pick a session preset, or switch to **Click Range** and tap two points 3. Toggle between Standard and Delta mode based on what you're hunting 4. Adjust the anchor side until the layout feels right 5. Trade

Categories & Tags

Volume#sxvnt#volume

Comments (0)

0/2000

Loading comments…

Source code

// =============================================================================
// SXVNT Volume Profile + Session Profile
// =============================================================================
// Two profiles in one script:
//
//   1) VOLUME PROFILE — rolling lookback profile across the last N bars.
//        Standard mode → POC + VAH/VAL + HVN/LVN classification
//        Delta mode    → bid/ask split per row using ctx.footprint
//
//   2) SESSION PROFILE — profile bound to a specific range. Range source:
//        Preset      → ICT killzones / RTH / ETH / Globex (uses ctx.session)
//        Click Range → user clicks two points on the chart to anchor it
//                      (ctx.input.points with count: 2). Anchors are
//                      draggable on-chart to fine-tune the profile range.
//
// Anchor Side:
//        Right (extend left)  → histogram sits at right edge, bars grow leftward
//        Right (extend right) → histogram protrudes past right edge into empty space
//        Left  (extend right) → histogram sits at left edge, bars grow rightward
//
// Sandbox-safe: no Date, no window, no async, no random, no setTimeout.
// Pure: same (bars, inputs) → same render plan.
// =============================================================================

function calculate(bars, ctx) {

  // ---------------------------------------------------------------------------
  // INPUTS
  // ---------------------------------------------------------------------------

  const PROFILE_MODES = [
    { label: 'Standard',         value: 'Standard' },
    { label: 'Delta (Bid/Ask)',  value: 'Delta' },
  ];

  const RANGE_SOURCES = [
    { label: 'Preset',      value: 'Preset' },
    { label: 'Click Range', value: 'Click' },
  ];

  const PRESETS = [
    { label: 'Asia Killzone',        value: 'asia' },
    { label: 'London Killzone',      value: 'london' },
    { label: 'NY AM Killzone',       value: 'ny_am' },
    { label: 'NY Lunch Killzone',    value: 'ny_lunch' },
    { label: 'NY PM Killzone',       value: 'ny_pm' },
    { label: 'RTH (CME)',            value: 'rth' },
    { label: 'ETH (CME)',            value: 'eth' },
    { label: 'Globex',               value: 'globex' },
  ];

  // Three anchor layouts:
  //   right_left   — anchor at right edge, bars extend leftward (over candles)
  //   right_right  — anchor at right edge, bars extend rightward (into empty space)
  //   left_right   — anchor at left edge, bars extend rightward (over candles)
  const ANCHOR_SIDES = [
    { label: 'Right edge → extend left',   value: 'right_left' },
    { label: 'Right edge → extend right',  value: 'right_right' },
    { label: 'Left edge → extend right',   value: 'left_right' },
  ];

  // ---- Volume Profile (rolling) ----
  const vpEnabled      = ctx.input('Volume Profile: Enabled', true);
  const vpLookback     = ctx.input('VP: Lookback Bars', 200, { min: 20, max: 2000, step: 10 });
  const vpRows         = ctx.input('VP: Price Rows', 60, { min: 10, max: 200, step: 5 });
  const vpValueAreaPct = ctx.input('VP: Value Area %', 70, { min: 50, max: 95, step: 1 });
  const vpMode         = ctx.input('VP: Mode', 'Standard', { options: PROFILE_MODES });
  const vpAnchor       = ctx.input('VP: Anchor Side', 'right_left', { options: ANCHOR_SIDES });
  const vpWidthPct     = ctx.input('VP: Width % of Lookback', 25, { min: 5, max: 60, step: 1 });
  const vpHvnThreshold = ctx.input('VP: HVN Threshold % of POC', 70, { min: 40, max: 95, step: 1 });
  const vpLvnThreshold = ctx.input('VP: LVN Threshold % of POC', 20, { min: 5, max: 50, step: 1 });

  // ---- Session Profile ----
  const spEnabled      = ctx.input('Session Profile: Enabled', true);
  const spRangeSource  = ctx.input('SP: Range Source', 'Preset', { options: RANGE_SOURCES });
  const spPreset       = ctx.input('SP: Preset', 'ny_am', { options: PRESETS });
  const spClickAnchors = ctx.input.points('SP: Click Two Points (Start, End)', { count: 2 });

  const spMode         = ctx.input('SP: Mode', 'Standard', { options: PROFILE_MODES });
  const spAnchor       = ctx.input('SP: Anchor Side', 'right_left', { options: ANCHOR_SIDES });
  const spRows         = ctx.input('SP: Price Rows', 50, { min: 10, max: 200, step: 5 });
  const spValueAreaPct = ctx.input('SP: Value Area %', 70, { min: 50, max: 95, step: 1 });
  const spWidthPct     = ctx.input('SP: Width % of Range', 35, { min: 5, max: 80, step: 1 });
  const spExtendRight  = ctx.input('SP: Extend POC/VA Right', true);
  const spShadeRange   = ctx.input('SP: Shade Session Range', true);

  // ---- Visuals (SXVNT palette) ----
  const colPocAbove    = ctx.input('POC Above Price', '#00E5FF');
  const colPocBelow    = ctx.input('POC Below Price', '#26F0B6');
  const colValueArea   = ctx.input('Value Area Fill', 'rgba(0,229,255,0.18)');
  const colHvn         = ctx.input('HVN Bars', 'rgba(0,229,255,0.55)');
  const colLvn         = ctx.input('LVN Bars', 'rgba(38,240,182,0.30)');
  const colMid         = ctx.input('Mid Vol Bars', 'rgba(0,229,255,0.35)');
  const colDeltaUp     = ctx.input('Delta Buy Bars',  'rgba(38,240,182,0.65)');
  const colDeltaDn     = ctx.input('Delta Sell Bars', 'rgba(255,72,108,0.65)');
  const colSessionBg   = ctx.input('Session Range Background', 'rgba(0,229,255,0.06)');
  const colSessionEdge = ctx.input('Session Range Border', 'rgba(0,229,255,0.35)');

  // ---------------------------------------------------------------------------
  // GUARDS
  // ---------------------------------------------------------------------------

  const close  = ctx.price.close;
  const high   = ctx.price.high;
  const low    = ctx.price.low;
  const volume = ctx.price.volume;

  const lastIndex = close.length - 1;
  if (lastIndex < 1) return;

  // ---------------------------------------------------------------------------
  // ANCHOR LAYOUT — convert (anchorSide, rangeStart, rangeEnd, widthBars)
  // into a concrete (anchorBar, direction) the renderer uses.
  // ---------------------------------------------------------------------------

  function resolveAnchor(anchorSide, rangeStart, rangeEnd) {
    // anchorBar = where bars grow FROM. direction = which way they grow.
    if (anchorSide === 'right_right') {
      return { anchorBar: rangeEnd,   direction: 'right' };
    }
    if (anchorSide === 'left_right') {
      return { anchorBar: rangeStart, direction: 'right' };
    }
    // Default: 'right_left'
    return   { anchorBar: rangeEnd,   direction: 'left' };
  }

  // Where to place labels (POC/VAH/VAL text) so they stay readable
  // regardless of which side the histogram is on.
  function labelBar(anchorSide, rangeStart, rangeEnd, widthBars) {
    if (anchorSide === 'right_right') return rangeEnd + widthBars;
    if (anchorSide === 'left_right')  return rangeStart + widthBars;
    return rangeEnd; // right_left
  }

  // ---------------------------------------------------------------------------
  // PROFILE BUILDER
  // ---------------------------------------------------------------------------

  function buildProfile(startBar, endBar, rowCount, mode) {
    if (startBar > endBar) { const t = startBar; startBar = endBar; endBar = t; }
    startBar = Math.max(0, startBar);
    endBar   = Math.min(lastIndex, endBar);
    if (endBar - startBar < 1) return null;

    let lo = Infinity, hi = -Infinity;
    for (let i = startBar; i <= endBar; i++) {
      if (low[i]  < lo) lo = low[i];
      if (high[i] > hi) hi = high[i];
    }
    if (!isFinite(lo) || !isFinite(hi) || hi === lo) return null;

    const binSize = (hi - lo) / rowCount;
    const rows = new Array(rowCount);
    for (let k = 0; k < rowCount; k++) {
      rows[k] = {
        price:   lo + (k + 0.5) * binSize,
        vol:     0,
        buyVol:  0,
        sellVol: 0,
        delta:   0,
      };
    }

    const useFootprint = mode === 'Delta' && ctx.footprint && ctx.footprint.bar;

    for (let i = startBar; i <= endBar; i++) {
      if (useFootprint) {
        const fp = ctx.footprint.bar(i);
        if (fp && fp.levels && fp.levels.length) {
          for (let j = 0; j < fp.levels.length; j++) {
            const lvl = fp.levels[j];
            const k = Math.min(rowCount - 1, Math.max(0, Math.floor((lvl.price - lo) / binSize)));
            const bid = lvl.bidVolume   != null ? lvl.bidVolume   : 0;
            const ask = lvl.askVolume   != null ? lvl.askVolume   : 0;
            const tot = lvl.totalVolume != null ? lvl.totalVolume : (bid + ask);
            rows[k].sellVol += bid;
            rows[k].buyVol  += ask;
            rows[k].vol     += tot;
            rows[k].delta   += (ask - bid);
          }
          continue;
        }
      }
      const v  = volume[i] || 0;
      const bH = high[i];
      const bL = low[i];
      const startK = Math.max(0,            Math.floor((bL - lo) / binSize));
      const endK   = Math.min(rowCount - 1, Math.floor((bH - lo) / binSize));
      const span   = Math.max(1, endK - startK + 1);
      const slice  = v / span;
      for (let k = startK; k <= endK; k++) rows[k].vol += slice;
    }

    let totalVol = 0;
    for (let k = 0; k < rows.length; k++) totalVol += rows[k].vol;

    return { rows: rows, binSize: binSize, lo: lo, hi: hi, totalVol: totalVol };
  }

  // ---------------------------------------------------------------------------
  // VALUE AREA — standard expanding-window method
  // ---------------------------------------------------------------------------

  function computeValueArea(profile, valueAreaPct) {
    const rows = profile.rows;
    const totalVol = profile.totalVol;
    const binSize = profile.binSize;
    if (!totalVol) return null;

    let pocIdx = 0;
    for (let k = 1; k < rows.length; k++) {
      if (rows[k].vol > rows[pocIdx].vol) pocIdx = k;
    }

    const target = totalVol * (valueAreaPct / 100);
    let acc = rows[pocIdx].vol;
    let lo = pocIdx, hi = pocIdx;

    while (acc < target && (lo > 0 || hi < rows.length - 1)) {
      const above = hi + 1 <= rows.length - 1 ? rows[hi + 1].vol : -1;
      const below = lo - 1 >= 0                ? rows[lo - 1].vol : -1;
      if (above >= below && hi + 1 <= rows.length - 1) {
        hi += 1; acc += rows[hi].vol;
      } else if (lo - 1 >= 0) {
        lo -= 1; acc += rows[lo].vol;
      } else break;
    }

    return {
      pocPrice: rows[pocIdx].price,
      pocVol:   rows[pocIdx].vol,
      vahPrice: rows[hi].price + binSize / 2,
      valPrice: rows[lo].price - binSize / 2,
    };
  }

  // ---------------------------------------------------------------------------
  // RENDER — horizontal histogram bars via ctx.box(x1,y1,x2,y2,opts)
  // ---------------------------------------------------------------------------

  function renderProfile(profile, va, anchorBar, widthBars, direction, mode) {
    const rows = profile.rows;
    const binSize = profile.binSize;
    let maxVol = 0;
    for (let k = 0; k < rows.length; k++) if (rows[k].vol > maxVol) maxVol = rows[k].vol;
    if (!maxVol) return;

    const sign = direction === 'left' ? -1 : 1;
    const pocVol = va ? va.pocVol : maxVol;
    const hvnCutoff = pocVol * (vpHvnThreshold / 100);
    const lvnCutoff = pocVol * (vpLvnThreshold / 100);

    for (let k = 0; k < rows.length; k++) {
      const r = rows[k];
      if (r.vol <= 0) continue;

      const barW = (r.vol / maxVol) * widthBars;
      const x1 = anchorBar;
      const x2 = anchorBar + sign * barW;
      const y1 = r.price - binSize / 2;
      const y2 = r.price + binSize / 2;

      let color;
      if (mode === 'Delta') {
        color = r.delta >= 0 ? colDeltaUp : colDeltaDn;
      } else if (r.vol >= hvnCutoff) {
        color = colHvn;
      } else if (r.vol <= lvnCutoff) {
        color = colLvn;
      } else {
        color = colMid;
      }

      ctx.box(x1, y1, x2, y2, {
        fillColor:   color,
        borderColor: 'transparent',
      });
    }
  }

  // ---------------------------------------------------------------------------
  // SESSION RANGE RESOLUTION
  // ---------------------------------------------------------------------------

  function resolveSessionRange() {
    if (spRangeSource === 'Click') {
      if (!spClickAnchors || spClickAnchors.length < 2) return null;
      const a = spClickAnchors[0];
      const b = spClickAnchors[1];
      if (a == null || b == null) return null;
      const startBar = Math.min(a.barIndex, b.barIndex);
      const endBar   = Math.max(a.barIndex, b.barIndex);
      return { startBar: startBar, endBar: endBar };
    }

    if (!ctx.session) return null;

    if (typeof ctx.session.lastRange === 'function') {
      const rng = ctx.session.lastRange(spPreset);
      if (rng && rng.startBar != null && rng.endBar != null) return rng;
    }

    if (typeof ctx.session.inSession === 'function') {
      let endBar = -1;
      for (let i = lastIndex; i >= 0; i--) {
        if (ctx.session.inSession(i, spPreset)) { endBar = i; break; }
      }
      if (endBar < 0) return null;
      let startBar = endBar;
      for (let i = endBar - 1; i >= 0; i--) {
        if (ctx.session.inSession(i, spPreset)) startBar = i;
        else break;
      }
      return { startBar: startBar, endBar: endBar };
    }

    return null;
  }

  // ---------------------------------------------------------------------------
  // 1) VOLUME PROFILE (rolling lookback)
  // ---------------------------------------------------------------------------

  if (vpEnabled) {
    const vpStart = Math.max(0, lastIndex - vpLookback + 1);
    const vpEnd   = lastIndex;
    const profile = buildProfile(vpStart, vpEnd, vpRows, vpMode);

    if (profile) {
      const va = computeValueArea(profile, vpValueAreaPct);
      const widthBars = Math.max(2, Math.round(vpLookback * (vpWidthPct / 100)));
      const anc = resolveAnchor(vpAnchor, vpStart, vpEnd);
      renderProfile(profile, va, anc.anchorBar, widthBars, anc.direction, vpMode);

      if (va) {
        const pocColor = close[lastIndex] >= va.pocPrice ? colPocBelow : colPocAbove;

        // Value area shading + POC line span the lookback range itself,
        // independent of which side the histogram sits on.
        ctx.box(vpStart, va.valPrice, vpEnd, va.vahPrice, {
          fillColor: colValueArea,
          borderColor: 'transparent',
        });

        ctx.line(vpStart, va.pocPrice, vpEnd, va.pocPrice, {
          color: pocColor, lineWidth: 2,
        });

        const lblBar = labelBar(vpAnchor, vpStart, vpEnd, widthBars);
        ctx.label(lblBar, va.pocPrice, ' POC ' + va.pocPrice.toFixed(2) + ' ', {
          color: pocColor, textColor: '#000',
        });
      }
    }
  }

  // ---------------------------------------------------------------------------
  // 2) SESSION PROFILE
  // ---------------------------------------------------------------------------

  if (spEnabled) {
    const range = resolveSessionRange();
    if (range) {
      const startBar = range.startBar;
      const endBar   = range.endBar;
      const profile  = buildProfile(startBar, endBar, spRows, spMode);

      if (profile) {
        const va = computeValueArea(profile, spValueAreaPct);
        const widthBars = Math.max(2, Math.round((endBar - startBar + 1) * (spWidthPct / 100)));
        const anc = resolveAnchor(spAnchor, startBar, endBar);

        renderProfile(profile, va, anc.anchorBar, widthBars, anc.direction, spMode);

        if (spShadeRange) {
          ctx.box(startBar, profile.lo, endBar, profile.hi, {
            fillColor:   colSessionBg,
            borderColor: colSessionEdge,
            borderStyle: 'dashed',
          });
        }

        if (va) {
          const xRight = spExtendRight ? lastIndex : endBar;
          const pocColor = close[lastIndex] >= va.pocPrice ? colPocBelow : colPocAbove;

          ctx.line(startBar, va.pocPrice, xRight, va.pocPrice, {
            color: pocColor, lineWidth: 2,
            extend: spExtendRight ? 'right' : undefined,
          });
          ctx.line(startBar, va.vahPrice, xRight, va.vahPrice, {
            color: pocColor, lineWidth: 1, lineStyle: 'dashed',
          });
          ctx.line(startBar, va.valPrice, xRight, va.valPrice, {
            color: pocColor, lineWidth: 1, lineStyle: 'dashed',
          });

          const lblBar = labelBar(spAnchor, startBar, endBar, widthBars);
          ctx.label(lblBar, va.pocPrice, ' Session POC ' + va.pocPrice.toFixed(2) + ' ', {
            color: pocColor, textColor: '#000',
          });
          ctx.label(lblBar, va.vahPrice, ' VAH ' + va.vahPrice.toFixed(2) + ' ', {
            color: pocColor, textColor: '#000',
          });
          ctx.label(lblBar, va.valPrice, ' VAL ' + va.valPrice.toFixed(2) + ' ', {
            color: pocColor, textColor: '#000',
          });
        }
      }
    }
  }
}