Clusters Volume Profile [LuxAlgo] – K-Means Clustered Liquidity Zones

Clusters Volume Profile [LuxAlgo] – K-Means Clustered Liquidity Zones

Key takeaways

  1. Identify high-conviction support and resistance zones by clustering price action into behavioral regimes, each with its own volume profile and Point of Control
  2. Detect market regime shifts automatically, since overlapping clusters signal accumulation while separated clusters reveal trending conditions
  3. Run it locally with PineTS on Node.js to build automated strategies that react when price approaches or breaks through cluster POC levels
  4. Compare volume conviction across clusters to prioritize which levels are institutional walls versus low-participation noise

Loading chart…

Indicator Credits : Author : LuxAlgo

Most volume profiles treat price history as one big block. Every bar gets the same weight regardless of whether the market was trending, consolidating, or in full-blown panic mode. LuxAlgo’s Clusters Volume Profile takes a completely different approach: it runs K-Means clustering on your price data first, groups bars into distinct behavioral regimes, then builds a separate volume profile for each cluster.

The result is something I honestly didn’t expect to find useful at first glance. Instead of one monolithic profile, you get up to 10 color-coded profiles, each representing a different price regime. The POC (Point of Control) for each cluster acts as its own gravity level, and because these clusters are formed by price proximity rather than time, they often reveal institutional accumulation zones that a standard VP would just smear across its histogram.

Running with PineTS

PineTS runs Pine Script outside of TradingView in Node.js. If you want to backtest cluster-based S/R levels or pipe the output straight into your own system, that’s your path.

Installing PineTS

Grab PineTS from npm:

npm install pinets

Loading the Pine Script Indicator

PineTS reads the Pine Script source file, compiles it internally, and executes it against live market data from your chosen provider. For this indicator, the Binance provider with BTCUSDC weekly data works well since you get a solid 200+ bars of history for the clustering algorithm to chew through.

Method 1: Batch Execution with pineTS.run()

Run it once against the full dataset and you get all computed outputs back together. Good for backtesting or anything where you don’t need live updates.

// 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 loads the indicator source, runs it against 500 weekly candles, and prints the computed plots. Because this indicator draws boxes, labels, and lines rather than traditional plot lines, the output contains the visual element data that PineTS captures from the rendering engine.

Method 2: Live Streaming with pineTS.stream()

For live updates as new candles come in, use the streaming API. It re-executes the indicator on each new data update and emits the fresh context through an event emitter.

Inputs

Clustering Settings

InputTypeDefaultDescription
Lookback Periodint200Number of recent bars to cluster and analyze (min: 10)
Number of Clustersint5How many distinct price groups to detect (2 to 10)
K-Means Iterationsint50Number of times the algorithm refines cluster centers (5 to 50)

Volume Profile Settings

InputTypeDefaultDescription
Rows per Cluster VPint20Number of histogram bins for each cluster’s volume profile (min: 2)
Max VP Width (Bars)int40Maximum horizontal length of the volume profile histograms (min: 5)
VP Offsetint10Horizontal spacing between current bar and the start of the profiles
Highlight Price DotsbooltrueShow colored dots on price to identify cluster assignments
Dot SizestringsmallSize of cluster assignment dots (tiny, small, normal, large, huge)

Plots

No plot() calls here. The whole thing renders as boxes, labels, and lines on the chart:

Element TypeDescription
Boxes (up to 500)Volume profile histogram bars for each cluster, color-coded by cluster assignment. POC bins are fully opaque while other bins are 75% transparent.
Labels (up to 500)Colored dots on price bars showing cluster membership, plus POC volume and total cluster volume labels at each POC level.
Lines (up to 500)Dashed horizontal lines extending from the lookback start to the volume profile, marking each cluster’s Point of Control.

Alerts

No alert conditions defined, which makes sense since the whole thing runs on the last bar only. If you want cluster-level alerts, you’d build that logic yourself on top of the PineTS output.


Understanding the Indicator

K-Means Clustering on Price Data

Alright, so here’s what makes this indicator tick. The core of it is a proper K-Means implementation written entirely in Pine Script, which is honestly impressive given the language’s limitations. The `f_kmeans()` function takes your lookback period, desired number of clusters, and iteration count, then goes to work.

First it collects `hl2` (the average of high and low) prices and volume for each bar in the lookback window. Centroids get initialized by evenly spacing them across the price range. Simple, but it works. If your lookback spans $20k to $100k and you want 5 clusters, the starting centroids land at roughly $33k, $47k, $60k, $73k, and $87k.

Now for the clever part. During the centroid recalculation step, LuxAlgo uses volume-weighted averages instead of simple means. The `sum_pv` array accumulates `price * volume` for each cluster, and dividing by `sum_v` gives you a VWAP-style centroid. This means high-volume bars pull the centroid toward them more aggressively. A single 10x-volume bar matters more than ten quiet bars at similar prices. Smart.

Per-Cluster Volume Profiles

Once the clustering is done, the script builds an individual volume profile for each cluster. This is where it gets visually interesting. For each cluster, it collects all the highs and lows of assigned bars, determines the price range, and then divides that range into `rowsInput` bins (default 20).

The volume allocation logic looks at how much of each bar’s wick intersects a given bin. If a bar’s wick spans from $50,000 to $55,000 and a bin covers $52,000 to $53,000, then 20% of that bar’s volume (1,000 out of 5,000 range) gets allocated to that bin. This produces much smoother profiles than simply dumping each bar’s entire volume into whichever bin its close lands in.

The POC for each cluster is simply the bin with the highest accumulated volume. It gets drawn as a dashed line stretching across the lookback period, color-coded to match its cluster. When I tested this on BTCUSDC weekly, the POC lines lined up surprisingly well with levels that I’d identified manually as strong support/resistance.

Visual Design and TradingView Limits

One thing worth noting is how the script manages TradingView’s object limits. With a max of 500 boxes, 500 labels, and 500 lines, you’d think 10 clusters with 20 rows each would eat through the box budget fast (that’s 200 boxes). And it would, except the script is clever about it: it skips bins with zero volume and breaks out of the loop when the box limit is hit.

Labels are even trickier. The script reserves `kInput * 2` labels upfront for the volume metrics (one for POC volume, one for total cluster volume at each POC). Only after securing those does it use remaining label capacity for the colored price dots. So if you set 10 clusters, that’s 20 reserved labels, leaving 480 for dots across the 200-bar lookback. Prioritizing the volume numbers over the decorative dots is a solid design call.

Using It for Algorithmic Signal Generation

So the centroid math has real practical implications. If you’re running this through PineTS, the cluster POC levels become programmable support/resistance zones. I’ve been thinking about a setup where you track the current price relative to each cluster’s POC, and when price enters a high-volume cluster from below, you look for long setups near that cluster’s POC. The total volume label tells you how much conviction is behind that level.

A particularly useful pattern: when two clusters overlap vertically, their combined volume creates an extra-strong zone. You can detect this by comparing cluster price ranges. If cluster 2’s max is above cluster 3’s min, that overlap zone is where institutional positioning is densest. Price tends to get sticky in those areas.

For trend detection, look at how clusters are distributed vertically. If your 5 clusters are tightly stacked with significant overlap, you’re in a range-bound market. If they’re spread out with gaps between them, the market has been trending. When a new bar creates a price point that’s far from any existing centroid, that’s often the beginning of a breakout.

One pattern I’ve found reliable on 4H and above: when price breaks through a cluster’s POC and then retests it from the other side, the retest tends to hold if the cluster’s total volume is in the top 2 of all clusters. Low-volume cluster POCs are more like suggestions than walls.

Performance Notes

One quirk: the indicator only runs on `barstate.islast`, meaning the entire K-Means computation happens on the final bar. On TradingView, that’s fine because it only runs once. In PineTS, the same applies, so you won’t see intermediate cluster states during backtesting. The output reflects the clustering as of the most recent bar.

With the default settings (200-bar lookback, 5 clusters, 50 iterations), the algorithm performs roughly 200 * 5 * 50 = 50,000 distance calculations per execution. That’s nothing for modern hardware. Even bumping to 10 clusters and max iterations, you’re still well under a second. On weekly timeframes, that means negligible latency for live trading.

Be careful with the lookback on lower timeframes though. A 200-bar lookback on 1-minute charts gives you about 3.3 hours of data, which isn’t much for meaningful clustering. For intraday use, bump the lookback to 500+ or use the 15-minute timeframe at minimum. Without enough price variation across the lookback, the algorithm just carves noise into smaller noise buckets.


Important Notes

PineTS handles this one cleanly. The boxes, labels, and lines all render correctly, and the K-Means array work runs without issues on weekly and 4H timeframes.

For best results with PineTS, use the Binance data provider with at least 500 bars of history. The default 200-bar lookback needs a buffer of prior data for the chart to display properly, and having extra bars ensures the volume distribution calculations are stable.

Licensed under CC BY-NC-SA 4.0, so it’s off-limits for commercial systems. Personal algo work and research are fine.

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.