# 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.

```flowscope title="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
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](/docs/scripting/guides/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](/docs/scripting/examples/size-band-delta).
- **Session-anchored delta.** Sum `data.vd` with `ta.cum(vd.close, reset: ...)` restarting each day, as in [plots and panes](/docs/scripting/examples/plots-and-panes), and keep `maxGap` below a day's bars.

## Related

- [Swing levels](/docs/scripting/examples/swing-levels) uses the same pivot logic for support and resistance.
- [User-defined types](/docs/scripting/guides/user-defined-types), [missing values](/docs/scripting/guides/missing-values)
- [`ta` reference](/docs/scripting/reference/catalog/ta), [`entities` reference](/docs/scripting/reference/catalog/entities)
