Key takeaways
- Visualize exactly where volume accumulated during each swing leg, with isolated profiles anchored to market structure instead of arbitrary time windows
- Identify high-probability support and resistance zones using the Point of Control from completed swings, ready to feed into automated limit order strategies
- Monitor buying vs selling pressure per swing via delta percentage, useful as a trend quality filter before committing to breakout entries
- Run it locally with PineTS on Node.js for backtesting or real-time signal generation through the Binance data provider, no TradingView needed
Loading chart…
Volume profiles are everywhere, but most of them anchor to fixed time sessions, the New York open, the Asian session, or whatever range you manually drag onto the chart. BigBeluga’s Swing Profile takes a different approach: it builds a complete volume distribution for each individual swing leg, anchored precisely between confirmed swing highs and lows. Every bullish and bearish move gets its own isolated profile, its own Point of Control, and its own buy/sell delta breakdown.
I ran this on weekly BTCUSDC and the profiles immediately showed me something useful: where the actual volume piled up during each leg, not just where price went. Instead of guessing which session or time window matters, the indicator lets the market structure itself define the boundaries. It also rebuilds the current swing’s profile in real time, so you can watch for volume skew before a swing completes.
Running Swing Profile with PineTS
You can run this indicator locally on Node.js using PineTS. No TradingView subscription needed, just your own data provider. Here’s how to get it set up.
Installing PineTS
npm install pinets
Loading the Pine Script Indicator
PineTS compiles and executes Pine Script v6 natively in JavaScript. Point it at the raw .pine file and a data provider (Binance here), and it runs everything, all 400+ lines of drawing logic, volume binning, and real-time profile updates, locally on your machine.
Method 1: Batch Execution with pineTS.run()
The simplest approach. Load the script, run it against historical data, and get back all the computed plot values in one shot. Perfect for backtesting or one-off analysis.
// PineTS batch execution example template
import { PineTS, Provider } from 'pinets'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pineScript = readFileSync(join(__dirname, 'source.pine'), 'utf-8')
const pineTS = new PineTS(Provider.Binance, 'BTCUSDC', '1w', 500)
const ctx = await pineTS.run(pineScript)
console.log('Bars processed:', ctx.marketData.length)
console.log('Plots:', Object.keys(ctx.plots).join(', '))
for (const [name, plot] of Object.entries(ctx.plots)) {
const lastPoint = plot.data?.[plot.data.length - 1]
if (lastPoint) {
console.log(` ${name}: last value = ${lastPoint.value}`)
}
}
This fetches 500 weekly candles from Binance, runs the full Swing Profile calculation, and dumps every plot’s last value. You’ll see the MA plot plus the internal drawing objects (labels, lines, boxes, polylines) that PineTS tracks behind the scenes.
Method 2: Live Streaming with pineTS.stream()
If you want the indicator to update as new candles come in (or as the current candle ticks), the streaming API is what you want. It fires a callback on every data update with the latest computed values.
// PineTS live streaming example template
import { PineTS, Provider } from 'pinets'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const pineScript = readFileSync(join(__dirname, 'source.pine'), 'utf-8')
const pineTS = new PineTS(Provider.Binance, 'BTCUSDC', '1w', 500)
let count = 0
const stream = pineTS.stream(pineScript)
stream.on('data', (ctx) => {
count++
console.log(`[Update ${count}] Bars: ${ctx.marketData.length}, Plots: ${Object.keys(ctx.plots).length}`)
for (const [name, plot] of Object.entries(ctx.plots)) {
const lastPoint = plot.data?.[plot.data.length - 1]
if (lastPoint) {
console.log(` ${name}: ${lastPoint.value}`)
}
}
if (count >= 5) {
console.log('Received 5 updates, stopping stream.')
process.exit(0)
}
})
stream.on('error', (err) => {
console.error('Stream error:', err.message)
process.exit(1)
})
For live trading, this is the one you want. The stream re-executes the indicator on every data update, so you get fresh swing detection and volume profiles as the market moves. Combine this with your own order logic and you’ve got swing-aware volume profiling running in your bot.
Indicator Properties
Inputs
General
| Input | Type | Default | Description |
|---|---|---|---|
| Swing Length | int | 50 | Lookback period for detecting swing highs and lows (min: 10) |
| Swing Color Up | color | lime | Color for bullish swing legs |
| Swing Color Down | color | orange | Color for bearish swing legs |
| Data Size | string | Small | Label size on chart (Tiny, Small, Normal, Large, Huge) |
Swing Volume Profile
| Input | Type | Default | Description |
|---|---|---|---|
| Profile | bool | true | Show volume profile bars for each swing |
| HeatMap | bool | false | Fill the entire swing range with a volume heatmap gradient |
ZigZag
| Input | Type | Default | Description |
|---|---|---|---|
| ZigZag | bool | true | Draw zigzag lines connecting swing highs and lows |
| ZigZag Style | string | Dotted | Line style for zigzag connector (Dotted, Dashed, Solid) |
Point of Control
| Input | Type | Default | Description |
|---|---|---|---|
| PoC | bool | true | Highlight the Point of Control (highest volume price level) |
| PoC Width | int | 2 | Line width for the PoC marker |
| PoC Color | color | red | Color of the Point of Control line |
Moving Average
| Input | Type | Default | Description |
|---|---|---|---|
| Type | string | None | MA type (None, SMA, EMA, SMMA/RMA, WMA, VWMA) |
| Length | int | 25 | Moving average period |
| Source | source | close | Price source for the MA calculation |
Plots
| Plot Name | Type | Description |
|---|---|---|
| MA | Line (yellow) | Optional moving average overlay, only visible when MA Type is not “None” |
Beyond the MA plot, the indicator generates extensive drawing objects (boxes, polylines, labels, lines) for the volume profiles, zigzag connectors, PoC markers, and data labels. All of these come through Pine Script’s drawing API rather than plot() calls.
Alerts
There are no built-in alerts here. If you want to fire on swing completions or PoC breaks, you’ll need to add your own alertcondition() calls to the Pine source, or just watch for direction flips in your Node.js code.
Understanding the Indicator
Swing Detection: Simple but Effective
Alright so here’s the deal. BigBeluga’s swing detection is dead simple, and that’s a good thing. It uses ta.highest(swingLen) and ta.lowest(swingLen) to track the rolling highest high and lowest low over the lookback window (default 50 bars). When the current high matches the rolling highest, the indicator flips to “down mode.” When the current low matches the rolling lowest, it flips to “up mode.”
A swing point is confirmed when the previous bar was the extreme and the current bar has broken away from it. So if yesterday’s high was the 50-bar highest and today’s high is lower, that swing high is locked in. Clean, no repainting on historical swings. The real-time profile for the current swing does update on every bar, which is by design.
ATR-Adaptive Volume Binning
The bin-sizing logic is the clever part. Instead of using a fixed number of bins or a fixed dollar width, the bin size adapts to volatility via ta.atr(200) * 0.5. On a quiet market, the bins are tighter, giving you higher resolution. On a volatile market, they widen to keep the profile readable. The number of bins is then calculated as the swing range divided by the bin size.
For each bin, the script loops through every bar in the swing leg and checks if candleClose falls within that bin’s range. If it does, the bar’s volume gets added to the bin. Green candles (close > open) go into the buy bucket, red candles go into sell. That gives you the buy/sell split and delta for each price level.
One thing I noticed: the volume assignment uses a proximity check (math.abs(binMidPrice - candleClose) < binStep) rather than a strict "close is between binLow and binHigh" check. This means a single candle can contribute volume to adjacent bins if it's near the boundary. It's a form of smoothing that makes the profiles look cleaner, though it does mean volume totals per bin aren't perfectly exclusive.
Real-Time vs. Historical Profiles
Rendering splits into two distinct paths. When a swing completes (direction flips), the profile is drawn once and locked in place as permanent chart objects. While a swing is still forming, the profile gets completely rebuilt on every bar update, with old boxes deleted and new ones created.
Smart move here: the real-time profile only runs on barstate.islast, so it doesn't waste computation on historical bars. The zigzag connector for the current swing renders as a dashed preview line, and the data label (showing total, buy, sell, and delta volumes) updates continuously. Once the swing locks, everything solidifies into the final colored version.
Using It for Algorithmic Signal Generation
If you're building anything that trades around volume structure, this indicator gives you a lot to work with.
The PoC (Point of Control) from completed swings is a natural support/resistance level. If price retraces back to a previous swing's PoC, that's a high-probability reaction zone because it's where the most volume traded during that move. Extract PoC levels from the plot data and set limit orders there.
Delta percentage adds another layer. It tells you whether buyers or sellers dominated a swing. A bullish swing with, say, +35% delta is healthy. A bullish swing with -5% delta? That's distribution disguised as a rally, and you should be suspicious. I'd treat delta as a trend quality filter, something to check before committing to a breakout entry, especially on pairs where volume data tends to be noisy.
Beyond individual metrics, the shape of the real-time profile compared to completed ones reveals a lot. Volume clustered at the bottom of the range suggests accumulation, a meaningfully different signal than volume piling up at the top, which points to distribution. Build a simple skew metric from the volume bins to quantify this.
For the PineTS streaming setup, I'd monitor the isDownMove direction flag. When it flips, a swing just completed. Grab the delta and PoC from the now-finalized swing, compare them to your signal thresholds, and fire your orders. The 50-bar default swing length means you'll get roughly one signal per major move on the weekly chart.
Performance Notes
Some practical limits worth knowing about. The indicator uses calc_bars_count = 2000 and max_bars_back = 5000, which means it processes a substantial history. With the default 50-bar swing length, you need at least 50 bars before the first swing can be detected, and the 200-bar ATR needs even more warmup data.
On the weekly timeframe, that's about 4 years of data before the ATR is fully warmed up. On daily, it's less than a year. I'd stick to 4H or above for reliable volume classification, because on lower timeframes the buy/sell split (based purely on candle color) gets noisy. A green 1-minute candle doesn't reliably indicate buying pressure the way a green weekly candle does.
Because the algorithm nests bar iteration inside bin iteration, computation scales with swing length. With the default swing length of 50 and typical bin counts around 10-20, you're looking at 500-1000 iterations per swing calculation. Not a problem on modern hardware, but if you crank the swing length to 200+ on a 1-minute chart, expect some latency.
Important Notes
PineTS handles this indicator without issues: v6 syntax, the custom SwingData struct, array operations, and all the drawing objects (boxes, polylines, lines, labels) come through clean. I tested it on weekly BTCUSDC with 500 bars and got clean output across all 5 plot keys. You will need a data provider that supplies volume data, though. Binance works well for crypto pairs, but for equities or forex, make sure your provider's volume figures are reliable. Thin-volume pairs will produce sparse, unreliable profiles.
The indicator's buy/sell volume classification is based on candle color (close vs. open), not on actual order flow or tick data. This is a common approximation in Pine Script and works reasonably well on higher timeframes, but don't confuse it with real order flow analysis from Level 2 data.
// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
// https://creativecommons.org/licenses/by-nc-sa/4.0/
// © BigBeluga
//@version=6
indicator("Swing Profile [BigBeluga]", overlay = true, max_labels_count = 500, max_boxes_count = 500, calc_bars_count = 2000, max_bars_back = 5000)
// INPUTS ---------------------------------------------------------------------
swingLen = input.int(50, "Swing Length", minval = 10)
swingColorUp = input.color(color.lime, "â–²", inline = "colors")
swingColorDown = input.color(color.orange, "â–¼", inline = "colors")
showProfile = input.bool(true, "Profile", group = "Swing Volume Profile")
showHeatMap = input.bool(false, "HeatMap", group = "Swing Volume Profile")
showZigZag = input.bool(true, "ZigZag", inline = "Zz")
zigZagStyleInput = input.string("Dotted", "", ["Dotted", "Dashed", "Solid"], inline = "Zz")
showPOC = input.bool(true, "PoC", "", inline = "Poc", group = "Point of Control")
pocWidthInput = input.int(2, "", inline = "Poc", group = "Point of Control")
pocColor = input.color(color.red, "", inline = "Poc", group = "Point of Control")
transparentNA = color.new(color.black, 100)
string dataSizeInput = input.string("Small", title = "Data Size", options = ["Tiny", "Small", "Normal", "Large", "Huge"])
// Size switcher for labels
getLabelSize() =>
switch dataSizeInput
"Small" => size.small
"Normal" => size.normal
"Large" => size.large
"Huge" => size.huge
=> size.tiny
getZigZagLineStyle() =>
switch zigZagStyleInput
"Dotted" => line.style_dotted
"Dashed" => line.style_dashed
"Solid" => line.style_solid
// Persistent storage containers
var polyline profilePoly = na
var dataLabel = label(na)
var pocLineHandle = line(na)
var profileBoxes = array.new<box>()
// Structs for swing points
type SwingData
float price
int index
var highSwing = SwingData.new(na, na)
var lowSwing = SwingData.new(na, na)
// Direction flag: true = down move, false = up move
var isDownMove = false
// CALCULATIONS ---------------------------------------------------------------
// Detect swing highs and lows
highestSwingHigh = ta.highest(swingLen)
lowestSwingLow = ta.lowest(swingLen)
if high == highestSwingHigh
isDownMove := true
if low == lowestSwingLow
isDownMove := false
// Store completed swing high
if high[1] == highestSwingHigh[1] and high < highestSwingHigh
highSwing.index := bar_index[1]
highSwing.price := high[1]
// Store completed swing low
if low[1] == lowestSwingLow[1] and low > lowestSwingLow
lowSwing.index := bar_index[1]
lowSwing.price := low[1]
// Default bin size based on ATR
priceBinSize = ta.atr(200) * 0.5
// HISTORICAL SWING LEG -------------------------------------------------------
// When direction flips we finalize previous swing leg and build full profile
if isDownMove != isDownMove[1]
// Clear previous drawings
profilePoly.delete()
dataLabel.delete()
for b in profileBoxes
b.delete()
profileBoxes.clear()
swingMainColor = isDownMove ? swingColorDown : swingColorUp
// Draw zigzag connector
if showZigZag
line.new(highSwing.index, highSwing.price, lowSwing.index, lowSwing.price, color = swingMainColor, style = getZigZagLineStyle())
// Determine swing boundaries
swingBottom = math.min(highSwing.price, lowSwing.price)
swingTop = math.max(highSwing.price, lowSwing.price)
// Number of volume bins
binCount = int(math.abs(highSwing.price - lowSwing.price) / priceBinSize)
binStep = math.abs(highSwing.price - lowSwing.price) / binCount
lastSwingIndex = math.max(highSwing.index, lowSwing.index)
prevSwingIndex = math.min(highSwing.index, lowSwing.index)
// Arrays for volume accumulation
volumeBins = array.new<float>(binCount, 0.)
profilePoints = array.new<chart.point>()
buyVolumeBins = array.new<float>(binCount, 0.)
sellVolumeBins = array.new<float>(binCount, 0.)
barOffset = bar_index - lastSwingIndex
// Accumulate volume per price bin
for i = barOffset to (lastSwingIndex - prevSwingIndex) + barOffset
candleClose = close[i]
candleOpen = open[i]
candleVol = volume[i]
for k = 0 to binCount - 1
binMidPrice = swingBottom + (k * binStep) + (binStep / 2)
if math.abs(binMidPrice - candleClose) < binStep
volumeBins.set(k, volumeBins.get(k) + candleVol)
if candleClose > candleOpen
buyVolumeBins.set(k, buyVolumeBins.get(k) + candleVol)
else
sellVolumeBins.set(k, sellVolumeBins.get(k) + candleVol)
// Draw boxes and build profile outline
for k = 0 to binCount - 1
binLow = swingBottom + (k * binStep)
binHigh = binLow + binStep
volumeRatio = volumeBins.get(k) / volumeBins.max()
profileWidth = int(volumeRatio * ((lastSwingIndex - prevSwingIndex) / 2))
// Draw POC line
if volumeRatio == 1 and showPOC
line.new(showProfile ? prevSwingIndex + profileWidth : prevSwingIndex, math.avg(binLow, binHigh), lastSwingIndex, math.avg(binLow, binHigh), color = pocColor, width = pocWidthInput)
// Create volume box
box.new(
showProfile ? prevSwingIndex + profileWidth : prevSwingIndex,
binHigh,
showHeatMap ? lastSwingIndex : prevSwingIndex,
binLow,
transparentNA,
0,
bgcolor = showProfile or showHeatMap ? color.from_gradient(volumeRatio, 0, 1, color.new(swingMainColor, 90), color.new(volumeRatio == 1 and showPOC ? pocColor : swingMainColor, 50)) : transparentNA,
text = showHeatMap ? "" : volumeRatio == 1 ? str.tostring(volumeBins.max(), format.volume) : ""
)
// Build polygon outline
if k == 0
profilePoints.push(chart.point.from_index(prevSwingIndex, binLow))
profilePoints.push(chart.point.from_index(prevSwingIndex + profileWidth, binLow))
profilePoints.push(chart.point.from_index(prevSwingIndex + profileWidth, binHigh))
if k == binCount - 1
profilePoints.push(chart.point.from_index(prevSwingIndex, binHigh))
// Draw outer polyline
if showProfile
polyline.new(profilePoints, false, true, line_color = showHeatMap ? color.new(swingMainColor, 50) : swingMainColor)
// Summary label for historical swing
swingArrowText = isDownMove ? "â–¼" : "â–²"
swingTooltip =
"Total Volume: " + str.tostring(volumeBins.sum(), format.volume)
+ "\nBuy Volume: " + str.tostring(buyVolumeBins.sum(), format.volume)
+ "\nSell Volume: " + str.tostring(sellVolumeBins.sum(), format.volume)
+ "\nDelta Volume: " + str.tostring((buyVolumeBins.sum() - sellVolumeBins.sum()) / volumeBins.sum() * 100, format.percent)
label.new(prevSwingIndex, isDownMove ? swingTop : swingBottom, swingArrowText,
style = isDownMove ? label.style_label_down : label.style_label_up,
color = transparentNA,
textcolor = swingMainColor,
size = getLabelSize(),
tooltip = swingTooltip)
// REAL-TIME SWING LEG --------------------------------------------------------
// Real-time profile rebuilding while swing is still forming
if not isDownMove
// Clear boxes on every update
for b in profileBoxes
b.delete()
profileBoxes.clear()
if barstate.islast
swingMainColor = chart.fg_color
// Remove previous zigzag preview line
line.delete(line.new(highSwing.index, highSwing.price, lowSwing.index, lowSwing.price, color = swingMainColor, style = line.style_dashed)[1])
swingBottom = math.min(highSwing.price, lowSwing.price)
swingTop = math.max(highSwing.price, lowSwing.price)
binCount = int(math.abs(highSwing.price - lowSwing.price) / priceBinSize)
binStep = math.abs(highSwing.price - lowSwing.price) / binCount
lastSwingIndex = math.max(highSwing.index, lowSwing.index)
prevSwingIndex = math.min(highSwing.index, lowSwing.index)
volumeBins = array.new<float>(binCount, 0.)
profilePoints = array.new<chart.point>()
buyVolumeBins = array.new<float>(binCount, 0.)
sellVolumeBins = array.new<float>(binCount, 0.)
barOffset = bar_index - lastSwingIndex
// Accumulate volume in real time
for i = barOffset to (lastSwingIndex - prevSwingIndex) + barOffset
candleClose = close[i]
candleOpen = open[i]
candleVol = volume[i]
for k = 0 to binCount - 1
binMidPrice = swingBottom + (k * binStep) + (binStep / 2)
if math.abs(binMidPrice - candleClose) < binStep
volumeBins.set(k, volumeBins.get(k) + candleVol)
if candleClose > candleOpen
buyVolumeBins.set(k, buyVolumeBins.get(k) + candleVol)
else
sellVolumeBins.set(k, sellVolumeBins.get(k) + candleVol)
// Draw real-time boxes
for k = 0 to binCount - 1
binLow = swingBottom + (k * binStep)
binHigh = binLow + binStep
volumeRatio = volumeBins.get(k) / volumeBins.max()
profileWidth = int(volumeRatio * ((lastSwingIndex - prevSwingIndex) / 2))
if volumeRatio == 1 and showPOC
pocLineHandle := line.new(showProfile ? prevSwingIndex + profileWidth : prevSwingIndex, math.avg(binLow, binHigh), lastSwingIndex, math.avg(binLow, binHigh), color = pocColor, width = pocWidthInput)
line.delete(pocLineHandle[1])
profileBoxes.push(box.new(
showProfile ? prevSwingIndex + profileWidth : prevSwingIndex,
binHigh,
showHeatMap ? lastSwingIndex : prevSwingIndex,
binLow,
transparentNA,
0,
bgcolor = showProfile or showHeatMap ? color.from_gradient(volumeRatio, 0, 1, color.new(swingMainColor, 90), color.new(volumeRatio == 1 and showPOC ? pocColor : swingMainColor, 50)) : transparentNA,
text = showHeatMap ? "" : volumeRatio == 1 ? str.tostring(volumeBins.max(), format.volume) : ""
))
if k == 0
profilePoints.push(chart.point.from_index(prevSwingIndex, binLow))
profilePoints.push(chart.point.from_index(prevSwingIndex + profileWidth, binLow))
profilePoints.push(chart.point.from_index(prevSwingIndex + profileWidth, binHigh))
if k == binCount - 1
profilePoints.push(chart.point.from_index(prevSwingIndex, binHigh))
// Update polyline in real time
if showProfile
polyline.delete(profilePoly[1])
profilePoly := polyline.new(profilePoints, false, true, line_color = showHeatMap ? color.new(swingMainColor, 50) : swingMainColor)
// Label with summary data
swingTooltip = "T - Total Volume\nB - Buy Volume\nS - Sell Volume\nD - Delta Volume"
swingDataText =
"T: " + str.tostring(volumeBins.sum(), format.volume)
+ "\nB: " + str.tostring(buyVolumeBins.sum(), format.volume)
+ "\nS: " + str.tostring(sellVolumeBins.sum(), format.volume)
+ "\nD: " + str.tostring((buyVolumeBins.sum() - sellVolumeBins.sum()) / volumeBins.sum() * 100, format.percent)
dataLabel := label.new(prevSwingIndex, not isDownMove ? swingTop : swingBottom, swingDataText,
style = not isDownMove ? label.style_label_down : label.style_label_up,
color = transparentNA,
textcolor = swingMainColor,
size = getLabelSize(),
tooltip = swingTooltip,
textalign = text.align_left)
label.delete(dataLabel[1])
// MIRROR OF ABOVE FOR isDownMove == true ------------------------------------------
// Mirror logic for bullish direction
else
for b in profileBoxes
b.delete()
profileBoxes.clear()
if barstate.islast
swingMainColor = chart.fg_color
line.delete(line.new(highSwing.index, highSwing.price, lowSwing.index, lowSwing.price, color = swingMainColor, style = line.style_dashed)[1])
swingBottom = math.min(highSwing.price, lowSwing.price)
swingTop = math.max(highSwing.price, lowSwing.price)
binCount = int(math.abs(highSwing.price - lowSwing.price) / priceBinSize)
binStep = math.abs(highSwing.price - lowSwing.price) / binCount
lastSwingIndex = math.max(highSwing.index, lowSwing.index)
prevSwingIndex = math.min(highSwing.index, lowSwing.index)
volumeBins = array.new<float>(binCount, 0.)
profilePoints = array.new<chart.point>()
buyVolumeBins = array.new<float>(binCount, 0.)
sellVolumeBins = array.new<float>(binCount, 0.)
barOffset = bar_index - lastSwingIndex
for i = barOffset to (lastSwingIndex - prevSwingIndex) + barOffset
candleClose = close[i]
candleOpen = open[i]
candleVol = volume[i]
for k = 0 to binCount - 1
binMidPrice = swingBottom + (k * binStep) + (binStep / 2)
if math.abs(binMidPrice - candleClose) < binStep
volumeBins.set(k, volumeBins.get(k) + candleVol)
if candleClose > candleOpen
buyVolumeBins.set(k, buyVolumeBins.get(k) + candleVol)
else
sellVolumeBins.set(k, sellVolumeBins.get(k) + candleVol)
for k = 0 to binCount - 1
binLow = swingBottom + (k * binStep)
binHigh = binLow + binStep
volumeRatio = volumeBins.get(k) / volumeBins.max()
profileWidth = int(volumeRatio * ((lastSwingIndex - prevSwingIndex) / 2))
if volumeRatio == 1 and showPOC
pocLineHandle := line.new(showProfile ? prevSwingIndex + profileWidth : prevSwingIndex, math.avg(binLow, binHigh), lastSwingIndex, math.avg(binLow, binHigh), color = pocColor, width = pocWidthInput)
line.delete(pocLineHandle[1])
profileBoxes.push(box.new(
showProfile ? prevSwingIndex + profileWidth : prevSwingIndex,
binHigh,
showHeatMap ? lastSwingIndex : prevSwingIndex,
binLow,
transparentNA,
0,
bgcolor = showProfile or showHeatMap ? color.from_gradient(volumeRatio, 0, 1, color.new(swingMainColor, 90), color.new(volumeRatio == 1 and showPOC ? pocColor : swingMainColor, 50)) : transparentNA,
text = showHeatMap ? "" : volumeRatio == 1 ? str.tostring(volumeBins.max(), format.volume) : ""
))
if k == 0
profilePoints.push(chart.point.from_index(prevSwingIndex, binLow))
profilePoints.push(chart.point.from_index(prevSwingIndex + profileWidth, binLow))
profilePoints.push(chart.point.from_index(prevSwingIndex + profileWidth, binHigh))
if k == binCount - 1
profilePoints.push(chart.point.from_index(prevSwingIndex, binHigh))
if showProfile
polyline.delete(profilePoly[1])
profilePoly := polyline.new(profilePoints, false, true, line_color = showHeatMap ? color.new(swingMainColor, 50) : swingMainColor)
swingTooltip = "T - Total Volume\nB - Buy Volume\nS - Sell Volume\nD - Delta Volume"
swingDataText =
"T: " + str.tostring(volumeBins.sum(), format.volume)
+ "\nB: " + str.tostring(buyVolumeBins.sum(), format.volume)
+ "\nS: " + str.tostring(sellVolumeBins.sum(), format.volume)
+ "\nD: " + str.tostring((buyVolumeBins.sum() - sellVolumeBins.sum()) / volumeBins.sum() * 100, format.percent)
dataLabel := label.new(prevSwingIndex, not isDownMove ? swingTop : swingBottom, swingDataText,
style = not isDownMove ? label.style_label_down : label.style_label_up,
color = transparentNA,
textcolor = swingMainColor,
size = getLabelSize(),
tooltip = swingTooltip,
textalign = text.align_left)
label.delete(dataLabel[1])
// Smoothing MA inputs
GRP = "Moving Average"
maTypeInput = input.string("None", "Type", options = ["None", "SMA", "EMA", "SMMA (RMA)", "WMA", "VWMA"], group = GRP, display = display.none)
maLengthInput = input.int(25, "Length", group = GRP, display = display.none, active = maTypeInput != "None")
src = input(close, title="Source", group = GRP, active = maTypeInput != "None")
// Smoothing MA Calculation
ma(source, length, MAtype) =>
switch MAtype
"SMA" => ta.sma(source, length)
"EMA" => ta.ema(source, length)
"SMMA (RMA)" => ta.rma(source, length)
"WMA" => ta.wma(source, length)
"VWMA" => ta.vwma(source, length)
plot(ma(src, maLengthInput, maTypeInput), color=color.yellow, title="MA")Disclaimer
QuantForge publishes technical material about indicators: what they do, how they are built, and how you can experiment with them in code and on charts. All of this is offered for education and illustration only. It is not financial, investment, or trading advice, and it should not be read as a recommendation to buy, sell, or hold any asset. Past or simulated performance does not guarantee future results. You are responsible for your own decisions; seek independent professional advice where appropriate.

![Swing Profile [BigBeluga] – Structure-Aware Volume Profiling Per Swing](https://quantforge.org/wp-content/uploads/2026/03/screenshot-4.webp)