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.
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.vdsubscriptions per timeframe and a column for the sign of the last confirmed bar’s delta. - OI on the top timeframe. Subscribe to
data.oiwithtimeframe: tfHighand 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 withon: dash.price. See custom axes and charts. - Alert on alignment. Trigger an alert from
on upper.closewhen all three trends turn the same way.
Related
- HTF trend filter uses the same per-timeframe trend states as a signal filter.
- Market stats table
- Multiple timeframes,
surfacesreference