MMTMMT Docs

Market decision dashboard

Turn price, momentum, volatility, volume, open interest, and CVD into a readable table beside the chart.

A table is most useful when it condenses several related inputs into decisions. This example builds a compact market dashboard beside the chart and uses cell styling to make the important states easy to scan.

V5 terminal with a BTC chart and a styled market decision table using trend, RSI, ATR, volume, OI, and CVD
script "Market Decision Dashboard"

data chart = subscribe(data.ohlcv)
data oi = subscribe(data.oi, timeframe: tf("15m"))
data cvd = subscribe(data.cvd, timeframe: tf("15m"))

window dashboard = window(title: "Market Dashboard") {
  signals: table.new([
    table.col(title: "Factor"),
    table.col(title: "Reading", align: align.right),
    table.col(title: "State", align: align.right),
  ], title: "Decision Board", capacity: 8),
}

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

on chart.close {
  let close = chart.close ?? 0.0
  let average = ta.ema(chart.close, 50) ?? close
  let trendPct = average != 0.0 ? (close / average - 1.0) * 100.0 : 0.0
  let bullish = close >= average

  let rsi = ta.rsi(chart.close, 14) ?? 50.0
  let momentumState = rsi >= 60.0 ? "STRONG" : (rsi <= 40.0 ? "WEAK" : "NEUTRAL")
  let momentumColor = rsi >= 60.0 ? color.green : (rsi <= 40.0 ? color.red : color.gray)

  let atrPct = close != 0.0 ? (chart.atr(14) ?? 0.0) / close * 100.0 : 0.0
  let volumeAverage = ta.sma(chart.volume, 20) ?? 1.0
  let relativeVolume = volumeAverage != 0.0 ? (chart.volume ?? 0.0) / volumeAverage : 0.0
  let oiChange = ta.change(oi.close, 4) ?? 0.0
  let cvdChange = ta.change(cvd.close, 4) ?? 0.0

  trend.plot(average)

  dashboard.signals.row("1", "Trend vs EMA 50",
    table.cell(str.format("{0:+.2}%", trendPct), fg: bullish ? color.green : color.red),
    table.cell(bullish ? "BULLISH" : "BEARISH", fg: bullish ? color.green : color.red))
  dashboard.signals.row("2", "RSI 14", str.format("{0:.1}", rsi),
    table.cell(momentumState, fg: momentumColor))
  dashboard.signals.row("3", "ATR 14", str.format("{0:.2}%", atrPct),
    table.cell(atrPct >= 0.65 ? "EXPANDED" : "NORMAL", fg: atrPct >= 0.65 ? color.orange : color.gray))
  dashboard.signals.row("4", "Relative volume", str.format("{0:.2}x", relativeVolume),
    table.cell(relativeVolume >= 1.2 ? "ACTIVE" : "QUIET", fg: relativeVolume >= 1.2 ? color.teal : color.gray))
  dashboard.signals.row("5", "Open interest · 1h", str.format("{0:+.0}", oiChange),
    table.cell(oiChange >= 0.0 ? "EXPANDING" : "CONTRACTING", fg: oiChange >= 0.0 ? color.teal : color.orange))
  dashboard.signals.row("6", "CVD · 1h", str.format("{0:+.0}", cvdChange),
    table.cell(cvdChange >= 0.0 ? "BUYING" : "SELLING", fg: cvdChange >= 0.0 ? color.green : color.red))
}

Fix the table's schema during setup

table.new declares the columns, alignment, and capacity once. Every row then follows the same three-cell shape: factor, numeric reading, and interpreted state.

Update rows by stable key

Keys such as "1" and "2" determine row identity and ordering. Calling row again replaces that row, so the dashboard always represents the latest closed candle without growing over time.

Pin the horizons you label

data.oi and data.cvd default their timeframe to the host chart's period, and ta.change counts bars on its source's clock — so an unpinned ta.change(oi.close, 4) would mean four chart bars, not one hour. Subscribing both at tf("15m") keeps the · 1h rows honest on any chart.

Style meaning, not decoration

The reading remains neutral unless its sign is important. Color is concentrated in the state column, where it helps distinguish bullish, bearish, active, and contracting conditions.

On this page