FlowscopeDocs

MTF dashboard

Summarise trend, momentum and trend efficiency on three timeframes in one table window, with an overall alignment row.

Before you take a trade on a short timeframe, you usually want to know what the larger ones are doing. Switching the chart back and forth costs time and breaks your focus. This script keeps three timeframes in one table: for each, the trend from an EMA pair, RSI, an efficiency ratio as a measure of trend strength, and how far price is from the slow EMA in ATRs. A final row tells you whether all three agree.

Every value comes from confirmed bars of its own timeframe, so the table never flickers with a forming higher-timeframe candle.

mtf-dashboard.fs
script "MTF dashboard"

input (
  tfLow = input.timeframe(15m, title: "Timeframe 1")
  tfMid = input.timeframe(1h, title: "Timeframe 2")
  tfHigh = input.timeframe(4h, title: "Timeframe 3")
  fastLen = input.int(21, title: "Fast EMA", min: 2, max: 200)
  slowLen = input.int(55, title: "Slow EMA", min: 3, max: 400)
  erLen = input.int(14, title: "Efficiency length", min: 2, max: 100)
  erTrend = input.float(0.3, title: "Trending above", min: 0.05, max: 1.0)
)

data (
  chart = subscribe(data.ohlcv)
  lower = subscribe(data.ohlcv, timeframe: tfLow)
  middle = subscribe(data.ohlcv, timeframe: tfMid)
  upper = subscribe(data.ohlcv, timeframe: tfHigh)
)

type TfState {
  trend: int
  rsi: float?
  efficiency: float?
  stretch: float?
}

window dash = window(title: "MTF dashboard") {
  table: table.new(columns: [table.col(title: "Timeframe"), table.col(title: "Trend"), table.col(title: "RSI", align: align.right), table.col(title: "Efficiency", align: align.right), table.col(title: "From slow EMA", align: align.right)]),
}

state (
  sLow = TfState { trend: 0, rsi: null, efficiency: null, stretch: null }
  sMid = TfState { trend: 0, rsi: null, efficiency: null, stretch: null }
  sHigh = TfState { trend: 0, rsi: null, efficiency: null, stretch: null }
)

fn summarize(c: float?, fast: float?, slow: float?, rsi: float?, efficiency: float?, atr: float?) -> TfState {
  let up = c != null && fast != null && slow != null && fast > slow && c > slow
  let down = c != null && fast != null && slow != null && fast < slow && c < slow
  let stretch: float? = c != null && slow != null && atr != null && atr > 0.0 ? (c - slow) / atr : null
  return TfState { trend: up ? 1 : down ? -1 : 0, rsi: rsi, efficiency: efficiency, stretch: stretch }
}

// Net move over the summed bar-to-bar moves: 1 is a straight line, 0 noise.
fn efficiencyOf(net: float?, path: float?) -> float? {
  if net == null || path == null || path <= 0.0 {
    return null
  }
  return math.abs(net) / path
}

fn trendText(t: int) -> string {
  return match t {
    1 => "Up"
    -1 => "Down"
    _ => "Flat"
  }
}

fn trendColor(t: int) -> color {
  return match t {
    1 => color.green
    -1 => color.red
    _ => color.gray
  }
}

fn writeRow(key: string, label: string, s: TfState) {
  let er = s.efficiency ?? 0.0
  dash.table.row(key, label,
    table.cell(trendText(s.trend), fg: color.white, bg: color.withAlpha(trendColor(s.trend), 170)),
    table.cell(str.format("{0:.0}", s.rsi ?? 50.0), fg: color.gradient(s.rsi ?? 50.0, 30.0, 70.0, color.red, color.green)),
    table.cell(str.format("{0:.2}", er), fg: er >= erTrend ? color.white : color.gray),
    str.format("{0:.2} ATR", s.stretch ?? 0.0))
}

on lower.close {
  let path = ta.sum(math.abs(ta.change(lower.close) ?? 0.0), erLen)
  sLow = summarize(lower.close, ta.ema(lower.close, fastLen), ta.ema(lower.close, slowLen), ta.rsi(lower.close, 14), efficiencyOf(ta.change(lower.close, erLen), path), lower.atr(14))
}

on middle.close {
  let path = ta.sum(math.abs(ta.change(middle.close) ?? 0.0), erLen)
  sMid = summarize(middle.close, ta.ema(middle.close, fastLen), ta.ema(middle.close, slowLen), ta.rsi(middle.close, 14), efficiencyOf(ta.change(middle.close, erLen), path), middle.atr(14))
}

on upper.close {
  let path = ta.sum(math.abs(ta.change(upper.close) ?? 0.0), erLen)
  sHigh = summarize(upper.close, ta.ema(upper.close, fastLen), ta.ema(upper.close, slowLen), ta.rsi(upper.close, 14), efficiencyOf(ta.change(upper.close, erLen), path), upper.atr(14))
}

on chart.update {
  if chart.isLast {
    writeRow("1", str.format("{0}", tfLow), sLow)
    writeRow("2", str.format("{0}", tfMid), sMid)
    writeRow("3", str.format("{0}", tfHigh), sHigh)

    let sum = sLow.trend + sMid.trend + sHigh.trend
    let verdict = match sum {
      3 => "All up"
      -3 => "All down"
      _ => "Mixed"
    }
    let verdictTone = sum == 3 ? color.green : sum == -3 ? color.red : color.gray
    dash.table.row("4", "Alignment", table.cell(verdict, fg: color.white, bg: color.withAlpha(verdictTone, 200)), "", "", "")
    script.setTitle(str.format("MTF dashboard: {0}", verdict))
  }
}

How it works

One subscription and one handler per timeframe

Each timeframe has its own OHLCV subscription and its own close handler. Inside on middle.close, every ta.* call sees one value per confirmed hourly bar, exactly as it would on an hourly chart. The handler condenses the results into a TfState record in state.

middle.atr(14) is a method of the subscription, because it needs high, low and close. On a chart, the three timeframes are built from the chart’s own bars, so each must be a whole multiple of the chart’s interval: with the defaults, use the dashboard on a chart of 15 minutes or less.

Efficiency instead of ADX

The efficiency ratio divides the net move over erLen bars by the sum of the bar-to-bar moves. A market that moves in a straight line scores 1; one that chops back and forth scores near 0. ta.sum of the absolute changes is the path; ta.change(close, erLen) the net move.

Pure helpers

summarize turns raw indicator values into a trend code and a stretch, without touching state or drawing. trendText and trendColor map the code to text and colour with exhaustive match expressions. writeRow writes one table row; a long call may continue on the next lines after a comma. Because the three timeframes share all of this, adding a fourth is a matter of one subscription, one handler, one state value and one writeRow call.

Rendering at the live edge

The chart’s update handler only renders when chart.isLast is true. It does not compute anything; it reads the three TfState values, which change only when their timeframe closes a bar. str.format("{0}", tfLow) writes a timeframe as its label, such as 15m.

Colour in cells

table.cell takes a text colour and a background. The trend column uses a solid background, RSI a gradient from red at 30 to green at 70, and the efficiency is bright only above the trending threshold, so the table reads from a distance.

Adapting it

  • Order-flow columns. Add data.vd subscriptions per timeframe and a column for the sign of the last confirmed bar’s delta.
  • OI on the top timeframe. Subscribe to data.oi with timeframe: tfHigh and show the last bar’s OI change next to the four-hour trend.
  • Chart in the same window. Add price: chart.panel(title: "4h") to the window and plot the slow EMA there with on: dash.price. See custom axes and charts.
  • Alert on alignment. Trigger an alert from on upper.close when all three trends turn the same way.