MMTMMT Docs

Confirmed market structure

Build higher-timeframe supply and demand zones, then react to breakouts on the chart timeframe.

This script keeps confirmed structure work on the one-hour clock and responsive plotting on the chart clock. It is a practical pattern for avoiding half-formed higher-timeframe signals without making the chart feel delayed.

BTC chart with subtle confirmed supply and demand zones, an EMA, and breakout markers
script "Confirmed Market Structure"

type PriceZone {
  top: float
  bottom: float

  fn midpoint() -> float {
    return (self.top + self.bottom) / 2.0
  }
}

fn supplyZone(level: float, width: float) -> PriceZone {
  return PriceZone { top: level + width, bottom: level - width }
}

fn demandZone(level: float, width: float) -> PriceZone {
  return PriceZone { top: level + width, bottom: level - width }
}

data chart = subscribe(data.ohlcv)
data h1 = subscribe(data.ohlcv, timeframe: tf("1h"))

plot trend = plot.line(title: "EMA 50", color: color.withAlpha(color.teal, 190), width: 2)

state (
  zones = entities.boxPool(max: 4)
  breakouts = entities.markerPool(max: 24)
  supply = PriceZone { top: 0.0, bottom: 0.0 }
  demand = PriceZone { top: 0.0, bottom: 0.0 }
  hasSupply = false
  hasDemand = false
  previousClose = 0.0
)

on h1.close {
  let now = h1.time ?? time.fromUnixSeconds(0)
  let width = (h1.atr(14) ?? 0.0) * 0.18

  let high = ta.pivotHigh(h1.high, 2, 2)
  if high != null {
    let start = h1.time[2] ?? now
    supply = supplyZone(high ?? 0.0, width)
    hasSupply = true
    zones.get(start, "supply").set(
      x1: start,
      y1: supply.top,
      x2: now + 24h,
      y2: supply.bottom,
      color: color.withAlpha(color.red, 14),
      borderColor: color.withAlpha(color.red, 105),
      borderWidth: 1.0,
    )
  }

  let low = ta.pivotLow(h1.low, 2, 2)
  if low != null {
    let start = h1.time[2] ?? now
    demand = demandZone(low ?? 0.0, width)
    hasDemand = true
    zones.get(start, "demand").set(
      x1: start,
      y1: demand.top,
      x2: now + 24h,
      y2: demand.bottom,
      color: color.withAlpha(color.green, 13),
      borderColor: color.withAlpha(color.green, 105),
      borderWidth: 1.0,
    )
  }
}

on chart.update {
  trend.plot(ta.ema(chart.close, 50))

  let now = chart.time ?? time.fromUnixSeconds(0)
  let close = chart.close ?? 0.0
  if hasSupply && previousClose <= supply.top && close > supply.top {
    breakouts.get(now).set(
      x: now,
      y: close,
      shape: shape.up,
      color: color.teal,
      size: 4.0,
    )
    hasSupply = false
  }
  if hasDemand && previousClose >= demand.bottom && close < demand.bottom {
    breakouts.get(now).set(
      x: now,
      y: close,
      shape: shape.down,
      color: color.orange,
      size: 4.0,
    )
    hasDemand = false
  }
  previousClose = close
}

Split work by event clock

on h1.close only changes zones after a one-hour candle is confirmed. on chart.update can still update the EMA and detect a breakout immediately. Each handler has one clear job, and neither has to simulate the other's timing.

Keep domain logic readable

PriceZone gives the pair of boundaries a name and a home for related methods. The two constructor functions are deliberately small; in a larger script they are the natural place to add tick rounding or different supply and demand widths.

Reuse drawings by key

The box and marker pools own their capacity and reuse old entries when full. Asking for an entity with get(key) is enough—you do not need an array of drawing IDs or manual recycling code. Supply and demand zones pass a second key part, get(start, "supply") and get(start, "demand"), so a bar that confirms both a pivot high and a pivot low draws two boxes instead of one overwriting the other.

On this page