Key takeaways
- Spot trend exhaustion and reversal setups by detecting when price action and volume conviction start disagreeing
- Compare orderflow strength across any asset or timeframe without recalibrating, since the oscillator is always normalized to the same -50 to +50 scale
- Includes on-chart divergence triangles and sync lines so you can identify signals directly on the price chart without switching panes
- Run it locally with PineTS for backtesting or real-time signal generation through the Binance data provider
Loading chartโฆ
Alright, you know how most volume delta indicators have this annoying habit of drifting off into infinity? Cumulative values that keep climbing (or diving) until the chart is basically unreadable? Yeah. ChartPrime’s DeltaPulse Wave fixes that. It normalizes the net difference between buying and selling volume into a bounded oscillator that stays between -50 and +50, no matter what. So a reading of +30 on BTCUSDC means the same thing as +30 on ETHUSDC or SPY. If you’re building multi-asset bots, that’s a game changer right there.
But honestly, the normalization isn’t even the best part. The indicator ships with a full divergence engine that automatically detects when price and volume conviction start disagreeing, and it draws sync lines on both the price chart and the oscillator pane. I’ve been running this on weekly BTCUSDC through PineTS, and the divergence signals are surprisingly clean. Let me walk you through the code and show you how to get it running locally.
Running DeltaPulse Wave with PineTS
PineTS takes your .pine file and runs it in Node.js. Not a simulation, not an approximation. You get the actual calculated values, bar by bar, same as what TradingView would produce, which means your bot can read the oscillator output directly instead of scraping a chart.
Installing PineTS
One command, you’re done:
npm install pinets
Loading the Pine Script Indicator
PineTS reads the raw .pine file, compiles it, and runs it against market data from whatever provider you’re pointed at. Pick your symbol and timeframe and it handles the rest. Back comes an execution context with every plot value, bar by bar, which for DeltaPulse Wave means 13 plots total, overlay divergence shapes included.
Batch Execution with pineTS.run()
This is the simplest path: load the history, run everything at once, get your context back. Good enough for backtesting or a quick sanity check on current state:
// 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}`)
}
}
ctx.plots has every named plot from the indicator. For DeltaPulse Wave that’s the wave line, baseline, OB/OS thresholds, and the divergence shapes. Each one is an array indexed by bar. Grab lastPoint.value and you’ve got the current reading ready for your signal logic.
Live Streaming with pineTS.stream()
Now this is where it gets fun. If you want real-time signals (and let’s be honest, that’s why most of us are here), the streaming API re-evaluates the indicator every time new market data comes in. Zero-line crossover? You’ll know the moment it happens. Divergence signal? Same deal.
// 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)
})
The pineTS.stream(pineScript) call opens a persistent connection to Binance. Every time a candle closes (or updates mid-bar), your callback fires with a fresh execution context. You can check the wave value, look for zero-line crosses, or watch for divergence shapes appearing in the plots. The connection stays open, no polling loop, no manual reinit.
Indicator Properties
Inputs
The inputs split into four groups, and there are more of them than you probably need. The ones worth caring about: waveLen (default 20) sets how many bars the EMAs look back, and smoothLen (default 5) does the final noise reduction pass.
General
| Input | Type | Default | Description |
|---|---|---|---|
| Wave Lookback | int | 20 | EMA period for the volume delta calculation. Higher values = smoother, slower wave. I’ve found 20 works great on weekly, but you might want 14 on 4H. |
| Smoothing | int | 5 | Second EMA pass on the raw RDS. Lower = more reactive, higher = more lag. Default 5 is a good balance. |
Levels
| Input | Type | Default | Description |
|---|---|---|---|
| Upper Threshold (OB) | float | 25.0 | Overbought zone. When the wave sits above this, buyers are firmly in control. |
| Lower Threshold (OS) | float | -25.0 | Oversold zone. Sellers are dominant. Watch for snap-backs when it hits -40. |
Analysis
| Input | Type | Default | Description |
|---|---|---|---|
| Min Divergence Gap | int | 5 | Minimum bars between peaks before a divergence can fire. This keeps the engine from going trigger-happy on every tiny wiggle. |
| Show Divergence Lines | bool | true | Toggles the sync lines connecting price peaks to oscillator peaks. Pretty useful for visual confirmation. |
Style & Dashboard
| Input | Type | Default | Description |
|---|---|---|---|
| Aggressive Buying | color | #00ffbb | Bullish wave color. |
| Aggressive Selling | color | #ff0055 | Bearish wave color. |
| Neutral Line | color | #6b7b8f | Zero baseline and thresholds. |
| Show Dashboard | bool | true | Toggles the little status table showing current wave, trend, and last divergence. |
| Position | string | Top Right | Where the dashboard sits (any corner). |
| Size | string | Small | Dashboard text size (Tiny through Large). |
Plots
| Plot Name | Type | Description |
|---|---|---|
| DeltaPulse Wave | line (linewidth 2) | The main oscillator. Green above zero, red below. This is your primary signal. |
| Baseline | line | Zero line. Crossovers here = shift between net buying and selling. |
| OB Level | line | Upper threshold (+25 default). |
| OS Level | line | Lower threshold (-25 default). |
| Bull Div Circle | shape (circle) | Small dot at the bottom of the oscillator when bullish divergence fires. |
| Bear Div Circle | shape (circle) | Small dot at the top of the oscillator when bearish divergence fires. |
| Bullish Divergence | shape (triangleup, overlay) | Green “Bull” triangle below the price bar. Hard to miss. |
| Bearish Divergence | shape (triangledown, overlay) | Red “Bear” triangle above the price bar. |
A barcolor(waveColor) call also that tints your price bars green or red based on the wave’s direction, plus a gradient fill between the wave and zero that gets more intense as conviction increases. It looks great, and the fill intensity is actually a useful visual cue for how “hot” the volume delta is running.
Alerts
No built-in alertcondition() calls in the source. But honestly, you don’t need them if you’re running through PineTS. Just watch the bullDiv and bearDiv plotshape values in your streaming callback. If you want native TradingView alerts, drop your own alertcondition() lines into the script.
Understanding the Indicator
How the Normalization Actually Works
Okay, this part is actually really smart. ChartPrime’s approach starts with a dead-simple volume classification: if close > open, the entire bar’s volume counts as buying. If close < open, it's selling. Doji bars count for nothing. Yeah, it's a rough approximation, it's not tick-level order flow data. But on 4H+ timeframes, candle body direction correlates pretty well with net aggression, and I've found the signals hold up in practice.
Here's where it gets clever. Instead of just accumulating a running delta that drifts to infinity, the script runs three separate EMAs: one on buy volume, one on sell volume, and one on total volume (all using the same waveLen period of 20 bars). Then it computes (smoothedBuy - smoothedSell) / smoothedTot * 100.
That division by total volume is the key. It locks the output into a bounded percentage range. A reading of +40 means about 70% of recent smoothed volume landed on green candles. Flip to -40, and sellers have that same dominance. One final ta.ema(rdsRaw, smoothLen) pass with the default 5-bar period smooths out the noise, and you've got yourself a double-smoothed, normalized oscillator that stays between -50 and +50 no matter if the asset trades $1M or $1B per day.
I love that design choice, because it means you can genuinely compare readings across assets without recalibrating anything.
The Divergence Engine
DeltaPulse Wave's divergence engine is what separates it from a dozen similar oscillators. Peak detection uses a three-bar pattern: the middle bar has to be higher (or lower) than both its neighbors. But there's a threshold gate, a peak only counts if it's above obThreshold * 0.2 (so above +5 with defaults). Troughs need to be below -5. This kills the garbage signals from tiny wiggles around the zero line.
When a new peak gets detected, it's compared against the previous one. Price made a higher high but the wave made a lower high? That's a bearish divergence: price is climbing but volume conviction is fading. The mirror pattern (price lower low, wave higher low) triggers a bullish divergence. Both sides enforce the minGap requirement of 5 bars minimum between peaks, which prevents the engine from spamming signals on every minor swing.
Smart.
Confirmed divergences show up as sync lines across both panes (the price chart gets them via force_overlay), and triangles labeled "Bull" or "Bear" land right on the price bars themselves. You can monitor divergences from the main chart alone without ever glancing at the oscillator.
The Dashboard
On the last bar, a compact table pops up showing three things: the current wave value (two decimal places), the trend direction (bullish if above zero, bearish below), and the most recent divergence type. It's built with Pine Script's table functions and only fires on barstate.islast, so it doesn't slow down historical processing. You can drag it to any corner and resize it. Not essential for algo work, but nice to have when you're visually reviewing signals.
Using It for Algorithmic Signal Generation
Alright, here's the practical stuff. If I'm building a bot around DeltaPulse Wave, the zero-line crossover combined with threshold context is the most reliable entry signal. Go long when the wave crosses above zero and the previous reading was below -10 (so you know it's a real swing from oversold territory, not just noise bouncing around the baseline). Reverse or close when it drops below zero from above +10.
I wouldn't use the divergence signals as standalone entries though. They're much better as filters. When your trend-following system says "go long," check if a bearish divergence fired in the last 10-15 bars. If it did, maybe skip that trade or cut the size in half. Volume conviction is fading, and you'd rather not chase. On the flip side, a bullish divergence near a support level? That adds real confidence to a long entry.
The extreme readings are where mean-reversion setups live. Anything above +40 or below -40 tends to snap back toward zero. Combine that with a divergence signal and you've got a pretty solid reversal setup. I've seen this work well on 4H and weekly BTCUSDC. On 1-minute charts? Forget about it. The candle-body volume classification is way too noisy at that scale.
Performance Notes
Computation overhead is negligible. Two EMA passes on volume data, one on the result, a handful of comparisons for divergence detection. You won't hit any bottlenecks running this on large bar counts.
One thing to watch out for: divergence signals are confirmed one bar late. The three-bar peak pattern needs the bar after the peak to confirm the turnaround, and all plotshapes use offset = -1 so they visually appear on the correct bar. But in your streaming callback, the signal shows up after the peak bar has already closed. On a weekly chart, that's a one-week delay. Fine for position trading, but keep it in mind if you're on shorter timeframes.
The volume classification is purely candle-body-based, not tick data. Works great on daily and weekly charts. On 5-minute bars, not so much. Real high-frequency delta analysis needs actual tick data, and this indicator is not that tool.
Important Notes
DeltaPulse Wave drops into PineTS without any changes to the source. All 13 plots come through, overlays included. The dashboard table only fires on barstate.islast, so you'll see it on the final bar of the execution context but not in historical data.
One dependency to keep in mind: this indicator needs volume data. Binance pairs through PineTS have it, no worries there. Swap to a provider without volume data and everything quietly collapses to zero (since buyVol and sellVol both pull from the volume built-in). No error, no warning, just flat-lined output. Ask me how I found that out.
The force_overlay = true on the divergence triangles and sync lines means they draw on the price chart even though the indicator lives in a separate pane. In PineTS, these overlay elements show up as plot data alongside the oscillator plots in your execution context.
// This Pine Scriptยฎ code is subject to the terms of the Mozilla Public License 2.0 at https://mozilla.org/MPL/2.0/
// ยฉ ChartPrime
//@version=6
indicator("DeltaPulse Wave [ChartPrime]", "DeltaPulse Osc [ChartPrime]", overlay = false, precision = 2)
// --------------------------------------------------------------------------------------------------------------------}
// ๐ ๐๐๐๐ ๐๐๐๐๐๐
// --------------------------------------------------------------------------------------------------------------------{
int waveLen = input.int(20, "Wave Lookback", minval = 5, tooltip = "Period for calculating the relative delta strength.")
int smoothLen = input.int(5, "Smoothing", minval = 1, tooltip = "Smoothing period for the final wave.")
float obThreshold = input.float(25.0, "Upper Threshold (OB)", minval = 1.0, group = "Levels")
float osThreshold = input.float(-25.0, "Lower Threshold (OS)", maxval = -1.0, group = "Levels")
int minGap = input.int(5, "Min Divergence Gap", minval = 2, group = "Analysis", tooltip = "Minimum number of bars required between two peaks or troughs to confirm a divergence.")
bool showLines = input.bool(true, "Show Divergence Lines", group = "Analysis")
color bullColor = input.color(#00ffbb, "Aggressive Buying", group = "Style")
color bearColor = input.color(#ff0055, "Aggressive Selling", group = "Style")
color neutColor = input.color(#6b7b8f, "Neutral Line", group = "Style")
// Dashboard Settings
bool showDash = input.bool(true, "Show Dashboard", group = "Dashboard")
string dashPos = input.string("Top Right", "Position", options = ["Top Right", "Bottom Right", "Top Left", "Bottom Left"], group = "Dashboard")
string dashSize = input.string("Small", "Size", options = ["Tiny", "Small", "Normal", "Large"], group = "Dashboard")
// --------------------------------------------------------------------------------------------------------------------}
// ๐ ๐๐๐ฟ๐๐พ๐ผ๐๐๐ ๐พ๐ผ๐๐พ๐๐๐ผ๐๐๐๐๐
// --------------------------------------------------------------------------------------------------------------------{
// 1. Calculate Relative Delta Strength (RDS)
float buyVol = close > open ? volume : 0.0
float sellVol = close < open ? volume : 0.0
float smoothedBuy = ta.ema(buyVol, waveLen)
float smoothedSell = ta.ema(sellVol, waveLen)
float smoothedTot = ta.ema(volume > 0 ? volume : 1, waveLen)
float rdsRaw = (smoothedBuy - smoothedSell) / (smoothedTot > 0 ? smoothedTot : 1) * 100
float deltaWave = ta.ema(rdsRaw, smoothLen)
// 2. Divergence Logic (Detection of local peaks)
bool isWaveHigh = deltaWave < deltaWave[1] and deltaWave[1] > deltaWave[2] and deltaWave[1] > (obThreshold * 0.2)
bool isWaveLow = deltaWave > deltaWave[1] and deltaWave[1] < deltaWave[2] and deltaWave[1] < (osThreshold * 0.2)
// State Tracking for Divergences
var float lastPH = 0.0
var int lastPH_Idx = 0
var float lastWH = 0.0
var int lastWH_Idx = 0
var float lastPL = 0.0
var int lastPL_Idx = 0
var float lastWL = 0.0
var int lastWL_Idx = 0
bool bearDiv = false
bool bullDiv = false
// Bearish Divergence (Price HH, Wave LH with Min Gap)
if isWaveHigh
if high[1] > lastPH and deltaWave[1] < lastWH and (bar_index[1] - lastPH_Idx) >= minGap
bearDiv := true
if showLines
line.new(lastPH_Idx, lastPH, bar_index[1], high[1], xloc.bar_index, extend.none, bearColor, line.style_solid, 2, force_overlay = true)
line.new(lastWH_Idx, lastWH, bar_index[1], deltaWave[1], xloc.bar_index, extend.none, bearColor, line.style_solid, 2)
// Update anchors only if current peak is significant or enough gap has passed
lastPH := high[1]
lastPH_Idx := bar_index[1]
lastWH := deltaWave[1]
lastWH_Idx := bar_index[1]
// Bullish Divergence (Price LL, Wave HL with Min Gap)
if isWaveLow
if low[1] < lastPL and deltaWave[1] > lastWL and (bar_index[1] - lastPL_Idx) >= minGap
bullDiv := true
if showLines
line.new(lastPL_Idx, lastPL, bar_index[1], low[1], xloc.bar_index, extend.none, bullColor, line.style_solid, 2, force_overlay = true)
line.new(lastWL_Idx, lastWL, bar_index[1], deltaWave[1], xloc.bar_index, extend.none, bullColor, line.style_solid, 2)
lastPL := low[1]
lastPL_Idx := bar_index[1]
lastWL := deltaWave[1]
lastWL_Idx := bar_index[1]
// --------------------------------------------------------------------------------------------------------------------}
// ๐ ๐๐๐๐๐ผ๐๐๐๐ผ๐๐๐๐
// --------------------------------------------------------------------------------------------------------------------{
// Zero Line & Thresholds
plot(0, "Baseline", color = color.new(neutColor, 50), style = plot.style_linebr)
plot(obThreshold, "OB Level", color = color.new(neutColor, 80), style = plot.style_linebr)
plot(osThreshold, "OS Level", color = color.new(neutColor, 80), style = plot.style_linebr)
// Main Wave
waveColor = deltaWave > 0 ? bullColor : bearColor
wavePlot = plot(deltaWave, "DeltaPulse Wave", color = waveColor, linewidth = 2)
// Centered Fill
fill(wavePlot, plot(0, display = display.none, editable = false),
top_value = deltaWave > 0 ? deltaWave : 0,
bottom_value = deltaWave < 0 ? deltaWave : 0,
top_color = deltaWave > 0 ? color.new(bullColor, 40) : color.new(bullColor, 100),
bottom_color = deltaWave < 0 ? color.new(bearColor, 40) : color.new(bearColor, 100))
// Divergence Signal Shapes (Oscillator Pane)
plotshape(bullDiv, "Bull Div Circle", shape.circle, location.bottom, bullColor, size = size.tiny, offset = -1)
plotshape(bearDiv, "Bear Div Circle", shape.circle, location.top, bearColor, size = size.tiny, offset = -1)
// Divergence Signal Shapes (Main Chart)
plotshape(bullDiv, "Bullish Divergence", shape.triangleup, location.belowbar, bullColor, text = "Bull", textcolor = bullColor, force_overlay = true, offset = -1)
plotshape(bearDiv, "Bearish Divergence", shape.triangledown, location.abovebar, bearColor, text = "Bear", textcolor = bearColor, force_overlay = true, offset = -1)
barcolor(waveColor)
// --------------------------------------------------------------------------------------------------------------------}
// ๐ ๐ฟ๐ผ๐๐๐ฝ๐๐ผ๐๐ฟ
// --------------------------------------------------------------------------------------------------------------------{
var table dashTable = na
// Last Divergence
var string lastDivType = "None"
var color lastDivCol = chart.fg_color
if bullDiv
lastDivType := "Bullish"
lastDivCol := bullColor
if bearDiv
lastDivType := "Bearish"
lastDivCol := bearColor
if showDash and barstate.islast
string pos = switch dashPos
"Top Right" => position.top_right
"Bottom Right" => position.bottom_right
"Top Left" => position.top_left
"Bottom Left" => position.bottom_left
=> position.top_right
string tSize = switch dashSize
"Tiny" => size.tiny
"Small" => size.small
"Normal" => size.normal
"Large" => size.large
=> size.small
dashTable := table.new(pos, 2, 4, color.new(chart.bg_color, 10), chart.fg_color, 1, chart.fg_color, 1)
// Header
table.cell(dashTable, 0, 0, "DeltaPulse", text_color = chart.fg_color, text_size = tSize, bgcolor = color.new(neutColor, 80))
table.cell(dashTable, 1, 0, "Value", text_color = chart.fg_color, text_size = tSize, bgcolor = color.new(neutColor, 80))
// Wave Value
table.cell(dashTable, 0, 1, "Wave", text_color = chart.fg_color, text_halign = text.align_left, text_size = tSize)
table.cell(dashTable, 1, 1, str.format("{0,number,#.##}", deltaWave), text_color = waveColor, text_size = tSize)
// Trend
table.cell(dashTable, 0, 2, "Trend", text_color = chart.fg_color, text_halign = text.align_left, text_size = tSize)
table.cell(dashTable, 1, 2, deltaWave > 0 ? "Bullish" : "Bearish", text_color = waveColor, text_size = tSize)
table.cell(dashTable, 0, 3, "Last Div", text_color = chart.fg_color, text_halign = text.align_left, text_size = tSize)
table.cell(dashTable, 1, 3, lastDivType, text_color = lastDivCol, text_size = tSize)
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.

![DeltaPulse Wave [ChartPrime] – Standardized Volume Delta Oscillator](https://quantforge.org/wp-content/uploads/2026/03/screenshot-3.webp)