FlowscopeDocs

CVD divergence

Compare confirmed price swings against cumulative volume delta and mark the places where price makes a new extreme that delta does not confirm.

A divergence between price and cumulative volume delta is one of the most common order-flow reads. Price prints a higher high, but aggressive buying at that high was weaker than at the previous one. Or price makes a lower low while sellers hit the bid less than before.

This script finds confirmed swing highs and lows on price, reads the CVD value at each swing, and compares it with the previous swing of the same kind. When price and CVD disagree, it connects the two swings on the chart, labels the divergence, and puts a marker in the CVD pane. Nothing is drawn until a swing is confirmed, so the marks do not move once they appear.

cvd-divergence.fs
script "CVD divergence"

input (
  left = input.int(5, title: "Swing bars left", min: 1, max: 30)
  right = input.int(3, title: "Swing bars right", min: 1, max: 20)
  maxGap = input.int(80, title: "Max bars between swings", min: 5, max: 400)
  minMovePct = input.float(0.05, title: "Min price difference (%)", min: 0.0, max: 5.0)
)

data (
  chart = subscribe(data.ohlcv)
  cvd = subscribe(data.cvd)
)

type Swing {
  index: int
  at: time
  price: float
  delta: float
}

type Divergence {
  kind: string
  price: float
}

state (
  lastHigh: Swing? = null
  lastLow: Swing? = null
  count = 0
  lines = entities.linePool(max: 200)
  labels = entities.labelPool(max: 200)
)

pane cvdPane = pane(title: "CVD", height: 0.25)

plot (
  cvdLine = plot.line(title: "CVD", color: color.cyan, width: 2, on: cvdPane)
  bearMark = plot.marker(title: "Bearish divergence", color: color.red, shape: shape.down, on: cvdPane)
  bullMark = plot.marker(title: "Bullish divergence", color: color.green, shape: shape.up, on: cvdPane)
)

alert divergenceAlert = alert(title: "CVD divergence", onClose: true, payload: Divergence)

on chart.close {
  let ph = ta.pivotHigh(chart.high, left, right)
  let pl = ta.pivotLow(chart.low, left, right)
  let d = cvd.close
  cvdLine.plot(d)

  // Values of the bar that just became a swing, `right` bars ago.
  let swingTime = chart.time[right]
  let swingDelta = cvd.close[right]
  let swingIndex = chart.index - right
  let up = 1.0 + minMovePct / 100.0
  let down = 1.0 - minMovePct / 100.0

  // Copy state to locals so null checks narrow them.
  let prevHigh = lastHigh
  let prevLow = lastLow

  let isBear = ph != null && prevHigh != null && swingDelta != null &&
    swingIndex - prevHigh.index <= maxGap &&
    ph > prevHigh.price * up &&
    swingDelta < prevHigh.delta
  let isBull = pl != null && prevLow != null && swingDelta != null &&
    swingIndex - prevLow.index <= maxGap &&
    pl < prevLow.price * down &&
    swingDelta > prevLow.delta

  if isBear && ph != null && prevHigh != null && swingTime != null {
    count += 1
    lines.get(count).set(prevHigh.at, prevHigh.price, swingTime, ph, width: 2.0, color: color.red)
    labels.get(count).set(swingTime, ph, "Bear div", color: color.red)
    divergenceAlert.trigger(Divergence { kind: "bearish", price: ph })
  }
  if isBull && pl != null && prevLow != null && swingTime != null {
    count += 1
    lines.get(count).set(prevLow.at, prevLow.price, swingTime, pl, width: 2.0, color: color.green)
    labels.get(count).set(swingTime, pl, "Bull div", color: color.green)
    divergenceAlert.trigger(Divergence { kind: "bullish", price: pl })
  }

  bearMark.plot(isBear ? d : null)
  bullMark.plot(isBull ? d : null)

  // Every new swing replaces the previous one of its kind.
  if ph != null && swingTime != null && swingDelta != null {
    lastHigh = Swing { index: swingIndex, at: swingTime, price: ph, delta: swingDelta }
  }
  if pl != null && swingTime != null && swingDelta != null {
    lastLow = Swing { index: swingIndex, at: swingTime, price: pl, delta: swingDelta }
  }
}

How it works

Swings are confirmed late on purpose

ta.pivotHigh(chart.high, left, right) returns a value only when the bar right bars ago is higher than the left bars before it and the right bars after it. The swing is therefore known right bars after it happened. Every value that belongs to the swing is read with the same offset:

Flowscope Script
let swingTime = chart.time[right]
let swingDelta = cvd.close[right]
let swingIndex = chart.index - right

This delay is what keeps the script from repainting. A larger right gives you fewer, more reliable swings and later signals. For more on the trade-off, see repainting.

Reading CVD from the chart’s handler

The script runs in on chart.close. The CVD subscription uses the chart’s timeframe, so when a chart bar closes, cvd.close is the CVD of that same confirmed period, and cvd.close[right] is the CVD at the swing bar. Both ta.pivotHigh and ta.pivotLow are called on every bar, outside any if, so their history stays in step with the chart.

Remembering the previous swing

The last swing high and swing low live in state as nullable records. A null check does not narrow a state value, so the handler copies both to locals first. Inside a chain of &&, every check narrows the locals for the rest of the chain. Each continued line follows a trailing &&.

At the end of the handler, the new swing always replaces the old one, whether or not it formed a divergence. Swings further apart than maxGap bars are not compared, which avoids linking a swing to a stale one from a different market phase.

The divergence rule

A bearish divergence is a higher price swing (by at least minMovePct) with a lower CVD reading than the previous swing high. A bullish divergence is the mirror image on swing lows. The script draws a line between the two price swings and a label on the newer one. Each divergence gets its own key in the line and label pools from a counter; the pools hold 200 entries each, so on a long history the oldest drawings are recycled.

Markers and alerts

The markers in the CVD pane appear on the bar where the divergence was confirmed, not on the swing bar. The alert uses onClose: true and a typed payload, so it fires once on the confirmed bar and tells you which kind of divergence occurred.

Adapting it

  • Hidden divergences. Swap the comparisons: a higher swing low on price with a lower CVD low signals continuation instead of reversal.
  • Large traders only. Replace the CVD with a running sum of the delta of large prints from a trades subscription, so the divergence measures whale flow. See size-band delta.
  • Session-anchored delta. Sum data.vd with ta.cum(vd.close, reset: ...) restarting each day, as in plots and panes, and keep maxGap below a day’s bars.