Trading Activity Index [Zeiierman] – Dollar Volume Heatmap with Percentile Bands

Trading Activity Index [Zeiierman] – Dollar Volume Heatmap with Percentile Bands

Key takeaways

  1. Instantly see if market participation is high or low relative to its own history, perfect for filtering out fakeout breakouts
  2. Works across any asset without recalibrating since the percentile bands self-normalize, so below P20 is always quiet and above P80 is always loud
  3. Use it as a position sizing multiplier in your algo, scaling up during active markets and pulling back when liquidity dries out
  4. Run it locally with PineTS on Node.js for backtesting or real-time signal generation through the Binance data provider

Loading chart…

Indicator Credits : Author : Zeiierman

What Is the Trading Activity Index?

I think we all know what a volume bar looks like on a chart. But I bet most of us don’t realize that volume bars scale differently depending on the asset, timeframe and so on. This is exactly the issue that Zeiierman’s Trading Activity Index solves: it shows you how to look at raw volume data in a much more meaningful way.

Instead of showing just raw volume data it normalizes the raw volume to log scale and then calculates the percentiles of the volume across time (by default 20, 40, 60, 80). You get a single line that tells you where the current market activity is relative to its own history.

There are a few tricks in there: it computes close * volume, takes a simple moving average and then applies log scale. This helps compress those wild spikes into more readable form. It also calculates rolling percentile bands at 20th, 40th, 60th and 80th levels to give you context of the current market situation.


Running It with PineTS

We can execute our indicator outside of TradingView on our own machine by using PineTS. This means we can integrate this into our own codebase where we can use it in our own trading bots, backtesters or monitoring dashboards.

Installing PineTS

This is how you install the package:

npm install pinets

That’s it. One dependency and we’re good to go!

Loading the Pine Script Indicator

To load an indicator script into PineTS we first need to read a .pine file as a string and then compile it internally. It connects to a data provider (Binance) to fetch OHLCV bars and then runs the Pine Script bar by bar, exactly like TradingView does.

Once executed we will have access to all of the values that we can pass wherever needed in our codebase.

Method 1: Batch Execution with pineTS.run()

The first approach is the easiest way to run it. We can load the script and run it against some historical data:

// 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 example fetches 500 weekly BTCUSDC bars from Binance, runs the Trading Activity Index on them, and returns all of its values to us. We get back a main activity line and each of the four percentile bands (P20, P40, P60, P80). This is useful for running a one-time analysis or for any backtesting scenario where we only want a snapshot of the current market conditions.

Method 2: Live Streaming with pineTS.stream()

If you need to get live updates and new values as they come in then use stream:

// 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)
})

When new data comes in, it fires an event (callback) that triggers the update. The callback has all the values computed so far and a fresh context object is passed into the function.

This kind of approach makes sense for any live bot that needs to know the current market activity level before placing orders. For example when the activity drops below P20 we might want to widen our stops or reduce position size.


Indicator Properties

Inputs

ParameterTypeDefaultRangeDescription
Formation Window (bars)int20min 2, step 1Number of bars used to average dollar volume before the log transform
History Window (bars)int252min 50, step 5Lookback period for rolling percentile calculations and rank normalization

There are only two parameters. I like that this is simple: it’s very easy to understand and set up.

The formation window defines how much smoothing we want on our volume average. A higher value will make it more stable, lower values will make it more reactive. The history window defines how far back we look to calculate percentile bands. For our BTCUSDC weekly setup it’s 252 bars which gives us almost 5 years of context. On daily charts, that same 252-bar default covers about one trading year.

Plots

Plot NameTypeDescription
Trading ActivityLine (width 2)Main activity line, color-coded from light blue (low) to red/orange (high) via gradient
P20Line (width 1)20th percentile band, light blue gradient
P40Line (width 1)40th percentile band, medium blue gradient
P60Line (width 1)60th percentile band, deeper blue gradient
P80Line (width 1)80th percentile band, near-deep-blue gradient

The color gradient is really well done. The main Trading Activity line smoothly shifts from light blue to red/orange depending on the range of the rolling min/max values in that window.

Alerts

This indicator does not define any alerts. If you want to build your own alert system then this is easy: it’s just a matter of checking some conditions and triggering an alert when necessary. For example “alert me when Trading Activity crosses above P80” is trivial to implement on the PineTS side.


Understanding the Indicator

The Core Approach: Dollar Volume as the Real Signal

Most volume indicators look at number of shares traded or contracts. This is not ideal since a lot of shares traded on a $2 stock is not the same as a lot of shares traded on a $1000 stock.

Zeiierman’s approach starts with the computation of dollar volume (close * volume) and then averages that value before applying the log scale. This is important because it helps make more sense when comparing across assets. We have a real signal here: how much money is flowing through the market.

The ta.sma(dlrVol, len_form) call smooths out any bar-to-bar noise. You don’t want a single anomalous tick causing big spikes in the volume line. The default 20 bars gives a nice balance between responsiveness and stability.

The Log Transform: Taming Outlier Spikes

Here’s where it gets interesting. After averaging the dollar volume, the script takes the natural log: math.log(math.max(dlrVolAvg, 1e-10)). Why? Because a lot of assets have an extremely skewed distribution. There are very few events that will cause the volume to spike by orders of magnitude.

Without this transformation we would see some big spikes in the activity line that make everything else look flat. With the log scale those spikes get compressed and become more manageable. The math.max(..., 1e-10) guard just prevents us from getting a log(0) which is undefined.

Rolling Percentiles: Context, Not Just a Number

The raw log-dollar volume is useful but having percentile bands makes it much more actionable. Using ta.percentile_linear_interpolation() over the history window, the script computes the 20th, 40th, 60th and 80th percentile thresholds. These aren’t fixed levels. They adapt as market conditions change.

When the main activity line is below P20, we’re in the bottom quintile of historical volume. The market is quiet, liquidity is thin, and breakouts are more likely to be fakeouts. When it’s above P60, we’re in a solid participation regime where trends tend to run through. Above P80 means something big is happening. Large players are actively moving capital.

The normalized rank (rank01) is purely cosmetic. It remaps the current vscale value to a 0-1 range using the rolling min/max over the history window. It’s only used to drive the gradient color on the main line.

Using It for Algorithmic Signal Generation

This is where the Trading Activity Index really shines for algo traders. Instead of using it on its own, think of it as a filter layer for your existing strategies.

I tested this on a BTCUSDC weekly setup and the percentile bands provide natural regime detection. When the activity line is below P20, switch your bot into mean-reversion mode or just reduce position size. When above P60, let your trend-following signals run with full conviction. Above P80? That’s your “something big is happening” signal where you might want to tighten stops because high-activity regimes often precede major moves.

The beauty of having this in PineTS is that you can compute the activity level on every new bar and use it as a multiplier for position sizing. Something like positionSize = baseSize * rank01 gives you automatic scaling: small positions during dead markets, full positions during active ones.

One thing I really like is that it works across different assets without any recalibration. The percentile bands are self-normalizing. Below P20 is quiet, above P80 is loud. You can run the same logic on ETHUSDC, SOLUSDC or any pair and the readings always mean the same thing. No parameter tuning needed.

Performance Notes

This is an incredibly lightweight indicator. No heavy loops, no matrix operations, just a few ta.sma(), ta.percentile_linear_interpolation(), ta.lowest(), and ta.highest() calls per bar. On our 360-bar weekly dataset, PineTS processes it almost instantly.

The main thing to watch is the history window. At the default 252, the indicator needs 252 bars of data before the percentile bands stabilize. On weekly charts, that’s roughly 5 years. On daily charts, about one trading year. On 1-minute charts, you’d only need about 4 hours of data, but the readings would be very noisy. For algo trading, I’d recommend daily or higher timeframes where the dollar volume figures are stable enough to trust.

One thing that makes this indicator appealing for backtesting: there are no barstate.isrealtime checks nor any future peeking, so the indicator does not repaint. What you see on historical bars is what you would have seen in real-time. That’s exactly what you want for strategy validation.


Important Notes

This indicator is fully compatible with PineTS. All functions used (ta.sma, ta.percentile_linear_interpolation, ta.lowest, ta.highest, math.log, math.max, math.min, math.round, color.rgb) are supported. Both the batch run() and streaming stream() methods work without any issues.

The data comes from Binance via PineTS. If you need different data providers or want to switch between spot and futures pairs, PineTS supports multiple providers. Check the PineTS documentation for the full list.

One thing that might not be obvious: the gradient coloring that makes this indicator so visually striking on TradingView won’t render in PineTS plot data. You get the raw numerical values (the activity level and percentile thresholds), which is actually better for algo use since you’re working with numbers, not colors. If you want to recreate the visual, you can apply the same gradient formula in your own charting library.

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.