Indicator · Pine Script v6 · On the chart

Delta Footprint Bubble

by piecioshka

Calculates volume delta and market profile metrics directly on the price chart using TradingView footprint data.

View on GitHub

GitHub stars2on the repository
Pine versionv6current
Lines of code221
Settings19inputs
LicenceMIT
Last update10 days ago18 Oct 2026

Add It to Your TradingView Chart

  1. 1Copy the scriptPress Copy script above, or the copy button on the code below.
  2. 2Open the Pine EditorOpen a TradingView chart and choose Pine Editor at the bottom of the screen.
  3. 3Paste over the templateCreate a new indicator, select all of the template text and paste. Press Save and give it a name.
  4. 4Add it to the chartPress Add to chart, then adjust the settings with the gear icon next to its name.

Source Code

221 lines of Pine Script v6 from indicators-premium/volume/delta-footprint-bubble/delta-footprint-bubble.pine, published under MIT. Keep the author's credit and licence lines when you reuse it.

delta-footprint-bubble.pine
// This Pine Script® code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// © Piotr Kowalski "piecioshka"
// Docs (EN): https://github.com/piecioshka/tradingview-pine-scripts/blob/main/indicators-premium/volume/delta-footprint-bubble/delta-footprint-bubble.md
// Docs (PL): https://github.com/piecioshka/tradingview-pine-scripts/blob/main/indicators-premium/volume/delta-footprint-bubble/delta-footprint-bubble.pl.md

//@version=6
indicator('Delta Footprint Bubble', overlay=true, max_labels_count=500, max_polylines_count=100)

// Volume delta from real footprint data (request.footprint, since January 2026).
// The footprint splits a candle into price levels and knows the aggressor volume on the buy (ask)
// and sell (bid) side. Delta = buy volume − sell volume:
//   Delta > 0  → aggressive buyers dominate (buying at the ask).
//   Delta < 0  → aggressive sellers dominate (selling at the bid).
//
// REQUIREMENTS / LIMITATIONS (imposed by TradingView):
//   • Premium or Ultimate account — without it the script does NOT compile.
//   • request.footprint() works only for the CURRENT chart timeframe (no TF parameter).
//   • Only ONE request.footprint() call per script.
//   • Returns na when there is no footprint data for a candle — always check not na(fp).

// --- Color palette (default TradingView candle colors) ---
cBuy     = color.rgb(38, 166, 154)  // buy dominance (positive delta) — default candle green #26A69A
cSell    = color.rgb(239, 83, 80)   // sell dominance (negative delta) — default candle red #EF5350
cNeutral = color.gray               // no data / zero delta
cVA      = color.rgb(150, 130, 220)  // Value Area — violet

// --- Inputs ---
gCalc = 'Footprint'
ticksPerRow = input.int(10, 'Ticks per row', minval=1, group=gCalc, tooltip='Height of a single footprint row in ticks. Affects POC/Value Area resolution (does not change the delta total). Fewer = more precise, but more rows.')
vaPercent   = input.int(70, 'Value Area %',  minval=1, maxval=100, group=gCalc, tooltip='Percent of volume defining the Value Area (classically 70%).')

gView = 'Display'
showLabels  = input.bool(true,  'Show delta numbers', group=gView, tooltip='Delta number printed next to each candle (green = buy dominance, red = sell).')
numMode     = input.string('Absolute', 'Number format', options=['Absolute', 'Percent of volume'], group=gView, tooltip='"Absolute" = delta in contracts/units (K/M/B abbreviated). "Percent of volume" = delta as % of the candle volume (buy + sell), e.g. +37% means buyers outweigh by 37% of the total.')
fontSize    = input.string('Small', 'Number size', options=['Tiny', 'Small', 'Normal', 'Large'], group=gView)
numPos      = input.string('Below POC', 'Number position', options=['Below POC', 'Above POC', 'Below candle', 'Above candle', 'By sign (POC)', 'By sign (candle)'], group=gView, tooltip='Where to print the delta number: "Below/Above POC" = below/above the POC bubble, "Below/Above candle" = below the candle (low) / above the candle (high). "By sign" = position follows the delta sign: green (positive) numbers above, red (negative) below - anchored to the POC bubble or the candle. No POC data → POC positions fall back to the candle.')
numGap      = input.float(0.5, 'Number gap (× ATR)', minval=0, maxval=5, step=0.1, group=gView, tooltip='Gap between the number and the candle / POC bubble, as a multiple of ATR(14). 0 = right next to it.')
showBubble  = input.bool(true, 'Show POC bubbles', group=gView, tooltip='Bubbles (dots) at the POC level, scaled by volume. Turn off to keep only delta numbers / Value Area / POC trail.')
pocColor    = input.color(color.new(color.orange, 20), 'POC bubble color', group=gView)
pocByDelta  = input.bool(false, 'Color POC bubbles by delta sign', group=gView, tooltip='Bubble green/red by the candle delta sign (who won the fight at the POC) instead of the fixed color above. The POC trail line keeps the fixed color.')
pocLink     = input.bool(false, 'Link POC bubbles with a line', group=gView, tooltip='Line connecting the centers of consecutive POC bubbles (POC trail).')
pocLinkMode = input.string('Segmented', 'POC line style', options=['Segmented', 'Curved'], group=gView, tooltip='"Segmented" = straight segments connecting consecutive POCs, drawn as the "POC trail" plot - its thickness and line type (line, step line, circles...) are editable in the Style tab. "Curved" = smooth, interpolated curve through the bubble centers (a drawing - not stylable from the Style tab).')
normPow     = input.float(0.5, 'Bubble size normalization', minval=0.1, maxval=1, step=0.05, group=gView, tooltip='Power scaling the bubble size (like in the histogram): 1 = linear (no normalization), 0.5 = square root, lower = stronger compression of spikes. Reference: max of ~500 candles.')
showVA      = input.bool(false,  'Value Area (VAH/VAL)', group=gView, tooltip='Value Area (VAH = Value Area High = upper bound, VAL = Value Area Low = lower) — the price range holding the given % of the candle volume (the "Value Area %" field).')
vaFill      = input.color(color.new(cVA, 90), 'Value Area fill', group=gView, tooltip='Color and transparency of the Value Area fill (set opacity with the palette slider). VAH/VAL lines inherit this hue.')

gImb = 'Stacked imbalance'
showImb  = input.bool(true, 'Detect stacked imbalance', group=gImb, tooltip='Diagonal imbalance from the footprint rows: aggressive buys at a level vs aggressive sells one level LOWER (and vice versa). Several imbalanced levels in a row = stacked imbalance, marked with a triangle under/over the candle.')
imbRatio = input.float(3.0, 'Imbalance ratio', minval=1.5, step=0.5, group=gImb, tooltip='Dominance threshold for one diagonal pair: 3 = one side has at least 3x (300%) the opposite side. A side facing zero opposite volume also counts.')
imbStack = input.int(3, 'Stacked rows (min)', minval=2, group=gImb, tooltip='How many CONSECUTIVE imbalanced levels are needed to flag a stacked imbalance.')

gCvd = 'Cumulative delta (CVD)'
resetDaily = input.bool(true, 'Reset CVD on new daily session', group=gCvd, tooltip='Cumulative delta (CVD) zeroed at the start of each day. Turn off to accumulate from the beginning of history.')

// --- Fetch the current candle's footprint ---
fp = request.footprint(ticksPerRow, vaPercent)
hasData = not na(fp)

// --- Read metrics (only when data exists — never call methods on na) ---
float delta    = na
float buyVol   = na
float sellVol  = na
float pocPrice = na
float pocVol   = na
float vahPrice = na
float valPrice = na
if hasData
    delta   := fp.delta()
    buyVol  := fp.buy_volume()
    sellVol := fp.sell_volume()
    pocRow = fp.poc()
    if not na(pocRow)
        pocPrice := (pocRow.up_price() + pocRow.down_price()) / 2
        pocVol   := pocRow.total_volume()
    vahRow = fp.vah()
    if not na(vahRow)
        vahPrice := vahRow.up_price()
    valRow = fp.val()
    if not na(valRow)
        valPrice := valRow.down_price()

totalVol = hasData ? buyVol + sellVol : na
// "Have delta" only when the footprint returned real trades. Otherwise (no footprint / zero volume,
// e.g. some crypto markets) → clear the metrics to na so NOTHING is shown instead of misleading "0"s.
hasDelta = hasData and not na(totalVol) and totalVol > 0
if not hasDelta
    delta    := na
    buyVol   := na
    sellVol  := na
    pocPrice := na
    pocVol   := na
    vahPrice := na
    valPrice := na

// --- Cumulative delta (CVD) ---
newDay = ta.change(time('D')) != 0
var float cvd = 0.0
if resetDaily and newDay
    cvd := 0.0
if hasDelta
    cvd += delta

// --- Number color by delta sign (label/table text, NOT the candle) ---
deltaColor = not hasDelta ? cNeutral : delta > 0 ? cBuy : delta < 0 ? cSell : cNeutral

// --- Divergence: candle direction contradicts the delta (something odd) ---
// Candle rises (close > open) with negative delta  → buyers "paint" the rise without aggression.
// Candle falls (close < open) with positive delta → sellers push price down despite buy dominance.
diverg = hasDelta and ((close > open and delta < 0) or (close < open and delta > 0))

// --- Stacked imbalance: diagonal buy/sell dominance on consecutive footprint rows ---
// Diagonal pair = buys at a level vs sells one level LOWER (aggressive buyers lift offers above
// the price where aggressive sellers hit bids). One side >= ratio x the other = imbalanced level;
// imbStack such levels in a row = stacked imbalance (absorption/initiative worth attention).
bool imbBuy  = false
bool imbSell = false
if showImb and hasDelta
    rows = fp.rows()
    n = na(rows) ? 0 : array.size(rows)
    if n >= imbStack + 1
        // Row order is not guaranteed - walk the pairs in ascending price order either way.
        asc = array.get(rows, 0).down_price() < array.get(rows, n - 1).down_price()
        int runBuy  = 0
        int runSell = 0
        for k = 0 to n - 2
            lowRow = array.get(rows, asc ? k : n - 1 - k)      // the lower-priced row of the pair
            upRow  = array.get(rows, asc ? k + 1 : n - 2 - k)  // the row one level higher
            buyUp   = nz(upRow.buy_volume())
            sellLow = nz(lowRow.sell_volume())
            bImb = buyUp > 0 and buyUp >= imbRatio * sellLow
            sImb = sellLow > 0 and sellLow >= imbRatio * buyUp
            runBuy  := bImb ? runBuy + 1 : 0
            runSell := sImb ? runSell + 1 : 0
            if runBuy >= imbStack
                imbBuy := true
            if runSell >= imbStack
                imbSell := true

// Markers via plotshape (does not consume the label budget shared by numbers and bubbles)
plotshape(imbBuy, 'Stacked buy imbalance', style=shape.triangleup, location=location.belowbar, color=cBuy, size=size.tiny)
plotshape(imbSell, 'Stacked sell imbalance', style=shape.triangledown, location=location.abovebar, color=cSell, size=size.tiny)

// --- Bookmap-style POC trail: bubbles scaled by volume ---
// Size ∝ volume at POC: gradation of 3 glyphs (• ● ⬤) + tiny/small/normal sizes.
// For TEXT large == huge (same pixels), so at the top of the scale the glyph change, not the size, makes the difference.
pocVolN   = math.pow(nz(pocVol), normPow)                // same normalization (power) as in the histogram
pocVolRef = ta.highest(pocVolN, 500)                     // reference from ~500 candles (no separate window)
pocNorm   = pocVolRef > 0 ? pocVolN / pocVolRef : 0.0    // 0..1 → bubble size grade
if showBubble and hasDelta and not na(pocPrice)
    lv      = math.min(6, math.floor(pocNorm * 7))
    pocChar = lv <= 1 ? '•' : lv <= 4 ? '●' : '⬤'
    pocSize = lv == 0 ? size.small : lv == 1 ? size.large : lv == 2 ? size.small : lv == 3 ? size.normal : lv == 4 or lv == 5 ? size.large : size.huge
    bubbleColor = pocByDelta ? deltaColor : pocColor   // fixed color or green/red by the delta sign
    label.new(bar_index, pocPrice, text=pocChar, yloc=yloc.price, style=label.style_none, textcolor=bubbleColor, size=pocSize)

// POC trail through the bubble centers, per the "POC line style" field:
// "Segmented" = the "POC trail" plot below (thickness and plot type editable in the Style tab),
// "Curved" = a smooth polyline (drawings cannot be styled from the Style tab).
// Polyline WINDOW: the last pocTrailMax points (polyline limit = 10000 points).
pocCurved = pocLinkMode == 'Curved'
var array<chart.point> pocPts = array.new<chart.point>()
var polyline pocPoly = na
pocTrailMax = 500   // the trail covers the last N bubbles (spans the visible ones, fits the polyline limit)
if pocLink and pocCurved and hasDelta and not na(pocPrice) and barstate.isconfirmed
    array.push(pocPts, chart.point.from_index(bar_index, pocPrice))
    if array.size(pocPts) > pocTrailMax
        array.shift(pocPts)   // keep the window: drop the oldest point
if pocLink and pocCurved and barstate.islast and array.size(pocPts) >= 2
    if not na(pocPoly)
        polyline.delete(pocPoly)
    pocPoly := polyline.new(pocPts, curved = true, line_color = color.new(pocColor, 30), line_width = 2)
plot(pocLink and not pocCurved ? pocPrice : na, 'POC trail', color=color.new(pocColor, 30), linewidth=2)

// --- Value Area (VAH/VAL): price range with the given % of candle volume (driven by "Value Area %") ---
pVah = plot(showVA ? vahPrice : na, 'VAH', color=color.new(vaFill, 25), style=plot.style_stepline, linewidth=1)
pVal = plot(showVA ? valPrice : na, 'VAL', color=color.new(vaFill, 25), style=plot.style_stepline, linewidth=1)
fill(pVah, pVal, color=showVA ? vaFill : na, title='Value Area zone')

// --- Values in the legend / data window (delta at the cursor — like the number shown for the Volume indicator) ---
plot(delta,               'Delta',      color=deltaColor, display=display.status_line + display.data_window, format=format.volume)
plot(buyVol,              'Buy (ask)',  color=cBuy,       display=display.data_window, format=format.volume)
plot(sellVol,             'Sell (bid)', color=cSell,      display=display.data_window, format=format.volume)
plot(hasDelta ? cvd : na, 'CVD (session)', color=cNeutral, display=display.status_line + display.data_window, format=format.volume)

// --- K/M/B abbreviation; below 1000 an integer (no ".00") ---
f_abbr(float x) =>
    a = math.abs(x)
    a >= 1e9 ? str.tostring(x / 1e9, '#.##') + 'B' : a >= 1e6 ? str.tostring(x / 1e6, '#.##') + 'M' : a >= 1e3 ? str.tostring(x / 1e3, '#.#') + 'K' : str.tostring(x, '#')

// Bold the number with Unicode characters (Pine has no native bold for labels)
f_bold(string s) =>
    string out = ''
    for i = 0 to str.length(s) - 1
        ch = str.substring(s, i, i + 1)
        b = ch == '0' ? '𝟬' : ch == '1' ? '𝟭' : ch == '2' ? '𝟮' : ch == '3' ? '𝟯' : ch == '4' ? '𝟰' : ch == '5' ? '𝟱' : ch == '6' ? '𝟲' : ch == '7' ? '𝟳' : ch == '8' ? '𝟴' : ch == '9' ? '𝟵' : ch == 'K' ? '𝗞' : ch == 'M' ? '𝗠' : ch == 'B' ? '𝗕' : ch
        out := out + b
    out

// --- Delta labels (below/above the candle or the POC bubble; divergence = bold number) ---
// "By sign" modes place the number by the delta sign: positive (green) above, negative (red) below.
szLbl   = fontSize == 'Tiny' ? size.tiny : fontSize == 'Small' ? size.small : fontSize == 'Normal' ? size.normal : size.large
atr     = nz(ta.atr(14))
bySign  = numPos == 'By sign (POC)' or numPos == 'By sign (candle)'
usePoc  = (numPos == 'Below POC' or numPos == 'Above POC' or numPos == 'By sign (POC)') and not na(pocPrice)   // no POC → falls back to the candle
above   = numPos == 'Above POC' or numPos == 'Above candle' or (bySign and delta > 0)
baseY   = usePoc ? pocPrice : (above ? high : low)
lblY    = above ? baseY + numGap * atr : baseY - numGap * atr   // gap = numGap × ATR; above or below the POC bubble / candle
if showLabels and hasDelta
    // "Percent of volume" = delta as % of the candle volume (signed); otherwise the absolute value (K/M/B)
    dRaw = numMode == 'Percent of volume' ? str.tostring(delta / totalVol * 100, '#') + '%' : f_abbr(delta)
    dTxt = diverg ? f_bold(dRaw) : dRaw   // divergence → bold number
    label.new(bar_index, lblY, text=dTxt, yloc=yloc.price, style=label.style_none, textcolor=deltaColor, size=szLbl)

// --- Alerts ---
alertcondition(ta.crossover(delta, 0),  'Delta: flip to buy',  'Delta turned positive (buyers dominate)')
alertcondition(ta.crossunder(delta, 0), 'Delta: flip to sell', 'Delta turned negative (sellers dominate)')
alertcondition(diverg, 'Delta divergence', 'Candle direction contradicts the delta (rise on negative delta or fall on positive)')
alertcondition(imbBuy,  'Stacked buy imbalance',  'Stacked buy imbalance: aggressive buyers dominate on consecutive footprint levels')
alertcondition(imbSell, 'Stacked sell imbalance', 'Stacked sell imbalance: aggressive sellers dominate on consecutive footprint levels')
alertcondition(ta.crossover(cvd, 0),  'CVD: flip to buy',  'Cumulative delta turned positive')
alertcondition(ta.crossunder(cvd, 0), 'CVD: flip to sell', 'Cumulative delta turned negative')

Overview

Delta Footprint Bubble identifies the Point of Control (POC), Value Area, and stacked buy or sell imbalances based on tick row settings. Visual outputs include POC bubbles, configurable delta labels, optional connecting lines, and Value Area shading. Custom inputs allow tuning tick height, Value Area percentage, imbalance ratios, and label placement. Requires a TradingView plan with footprint data support.

Details

Published by

piecioshka on GitHub

Type

Indicator, Pine Script v6

Draws

On the price chart

Category

Volume, Support & resistance

Markets

Any symbol

Timeframes

M1-M5, M15-H1

Risk Notes

No repainting, lookahead or stacked entries were found in its code or README. That says nothing about how well it works: test it first.

Topics

pinescriptpinescript-indicatorspinescript-strategiestradingtradingview

Settings

The inputs you can change from the gear icon on the chart. Defaults are the author's.

SettingTypeDefaultVariable
Ticks per rowint10ticksPerRow
Value Area %int70vaPercent
Show delta numbersbooltrueshowLabels
Number formatstringAbsolutenumMode
Number sizestringSmallfontSize
Number positionstringBelow POCnumPos
Number gap (× ATR)float0.5numGap
Show POC bubblesbooltrueshowBubble
POC bubble colorcolorcolor.new(color.orange, 20)pocColor
Color POC bubbles by delta signboolfalsepocByDelta
Link POC bubbles with a lineboolfalsepocLink
POC line stylestringSegmentedpocLinkMode
Show all 19 settings
SettingTypeDefaultVariable
Bubble size normalizationfloat0.5normPow
Value Area (VAH/VAL)boolfalseshowVA
Value Area fillcolorcolor.new(cVA, 90)vaFill
Detect stacked imbalancebooltrueshowImb
Imbalance ratiofloat3.0imbRatio
Stacked rows (min)int3imbStack
Reset CVD on new daily sessionbooltrueresetDaily
Third-party code. ForexR did not write, test or endorse this script, and the summary above was generated automatically from its code and README. Indicators describe the past, and a strategy's backtest can look better than live trading, especially when a script repaints. Check it in the Strategy Tester and on a demo account before you rely on it.

Similar Pine Scripts

This overlay indicator draws a fixed-range volume profile on the price chart, splitting traded activity into up and down volume at each price level. It also plots a sentiment profile, a developing point of control, and value-area high and low around a configur

Indicator v5 VolumeSupport & resistance
Updated 2 months ago · MIT GitHub repository of Volume Profile and Indicator by DGT

MGIC-tracker

by Allen1212
303 stars

The MGIC-tracker is a TradingView indicator that overlays ARK Invest trade records onto stock price charts. It plots buy and sell labels at the exact timestamp of each transaction, showing share volume and ETF weight. Users select a specific ETF type via setti

Indicator v4 Support & resistanceVolume
Updated 2 years ago · None stated GitHub repository of MGIC-tracker
218 stars

Comprehensive Volume Profile is an overlay indicator that calculates and plots volume distribution across specified time ranges such as daily sessions, weekly, monthly, or custom bar lengths. It identifies key structural price levels including the Point of Con

Indicator v6 VolumeSupport & resistance
Updated 3 months ago · None stated GitHub repository of Comprehensive Volume Profile

Moon Dev GEX Arrows

by moondevonyt
92 stars

This overlay indicator plots option strike walls and an optional session VWAP on the price chart for SPY or ES. It spaces major strikes by the selected instrument, draws a set number of strikes on each side, and marks wall-trap and sweep events after a cooldow

Indicator v5 Support & resistanceSessions & time
Updated 8 weeks ago · Custom licence GitHub repository of Moon Dev GEX Arrows

More indicators

Trade Your Charts With a Regulated Broker

Compare brokers that connect to TradingView, with spreads, minimum deposits and leverage side by side.

Compare brokers