MMTMMT Docs

CVD divergence

Compare price swings against cumulative volume delta and mark divergences between the two.

This example pairs the chart with data.cvd and looks for the classic disagreement: price makes a new swing while cumulative volume delta refuses to confirm it. It covers a second data stream, reading a series at a historical offset, and anchored markers.

Price chart with divergence markers and a CVD pane
script "CVD Divergence"

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

input pivotLen = input.int(8, title: "Pivot Strength", min: 2, max: 30)

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

plot cvdPlot = plot.line(title: "CVD", color: color.cyan, width: 2, on: cvdPane)

state (
  markers = entities.markerPool(max: 24)
  slot: int = 0
  prevHighPrice: float? = null
  prevHighCvd:   float? = null
  prevLowPrice:  float? = null
  prevLowCvd:    float? = null
)

on chart.update {
  cvdPlot.plot(cvd.close)

  let now = (chart.time ?? time.fromUnixSeconds(0))
  let pivotTime = now - time.bars(pivotLen)

  // When a price pivot confirms, read the CVD value that printed on the
  // same bar and compare the pair against the previous swing.
  let pivotHigh = ta.pivotHigh(chart.high, pivotLen, pivotLen)
  if pivotHigh != null {
    let cvdAt = cvd.close[pivotLen]
    let ready = prevHighPrice != null && cvdAt != null
    let higherHigh = (pivotHigh ?? 0.0) > (prevHighPrice ?? 0.0)
    let weakerCvd = (cvdAt ?? 0.0) < (prevHighCvd ?? 0.0)
    if ready && higherHigh && weakerCvd {
      // Higher high in price, lower high in CVD: buyers are thinning out.
      slot = slot >= 23 ? 0 : slot + 1
      markers.get("bear", slot).set(
        x: pivotTime,
        y: (pivotHigh ?? 0.0),
        shape: shape.down,
        color: color.red,
        size: 11.0,
      )
    }
    prevHighPrice = pivotHigh
    prevHighCvd = cvdAt
  }

  let pivotLow = ta.pivotLow(chart.low, pivotLen, pivotLen)
  if pivotLow != null {
    let cvdAt = cvd.close[pivotLen]
    let ready = prevLowPrice != null && cvdAt != null
    let lowerLow = (pivotLow ?? 0.0) < (prevLowPrice ?? 0.0)
    let strongerCvd = (cvdAt ?? 0.0) > (prevLowCvd ?? 0.0)
    if ready && lowerLow && strongerCvd {
      // Lower low in price, higher low in CVD: sellers are thinning out.
      slot = slot >= 23 ? 0 : slot + 1
      markers.get("bull", slot).set(
        x: pivotTime,
        y: (pivotLow ?? 0.0),
        shape: shape.up,
        color: color.green,
        size: 11.0,
      )
    }
    prevLowPrice = pivotLow
    prevLowCvd = cvdAt
  }
}

Two streams, one clock

cvd.close inside the chart handler reads the CVD bar aligned with the current chart bar. Bracket indexing works across streams too: cvd.close[pivotLen] is the CVD value from the same bar as the price pivot being confirmed.

Compare swing pairs, not raw series

Divergence is a statement about consecutive swings. The script stores the previous pivot's price and CVD in state, and only signals when the new pair disagrees in direction: higher high in price with a lower high in delta, or the mirror image at lows.

Markers need entities here

plot.marker draws on the current bar, but a divergence confirms pivotLen bars after the swing it belongs to. A pooled marker with an explicit x: anchors the signal to the bar that earned it.

On this page