Viewport HUD
Pin a compact market card to a corner of the price pane that stays in place while you pan and zoom.
Some information belongs on the chart but not on a price level: today’s change, current volatility, how far price is from VWAP, where funding stands. Labels attached to a bar scroll away as soon as you pan. A heads-up display (HUD) does not: it is anchored to the viewport, so it stays in the same corner whatever part of the chart you look at.
This script builds a small card with the market, price and daily change, ATR as a percentage of price, distance from the daily VWAP, funding, open-interest change and the active trading session. You choose the corner in the settings.
script "Market HUD"
input (
where = input.select("top right", title: "Corner", options: ["top left", "top right", "bottom left", "bottom right"])
oiBars = input.int(24, title: "OI change over (bars)", min: 1, max: 500)
perDay = input.int(3, title: "Funding periods per day", min: 1, max: 24)
showSession = input.bool(true, title: "Show session")
)
data (
chart = subscribe(data.ohlcv)
daily = subscribe(data.ohlcv, timeframe: 1D)
oi = subscribe(data.oi)
stats = subscribe(data.stat)
)
setup (
corner = match where {
"top left" => anchor.topLeft
"bottom left" => anchor.bottomLeft
"bottom right" => anchor.bottomRight
_ => anchor.topRight
}
asia = time.session("00:00", "08:00", "Asia")
london = time.session("07:00", "16:00", "London")
newYork = time.session("13:00", "22:00", "New York")
)
state (
card = entities.labelPool(max: 1, anchor: corner)
previousTime: time? = null
)
fn pct(x: float?, digits: int) -> string {
if x == null {
return "n/a"
}
let sign = x >= 0.0 ? "+" : ""
return digits == 1 ? str.format("{0}{1:.1}%", sign, x) : str.format("{0}{1:.2}%", sign, x)
}
on chart.update {
// Indicators run on every update so their history stays complete.
let t = chart.time ?? time.fromUnixSeconds(0)
let atr = chart.atr(14)
let vwap = chart.vwap(reset: time.isNewPeriod(t, previousTime, 1D))
if chart.isNew {
previousTime = t
}
if chart.isLast {
let c = chart.close
let dayOpen = daily.forming.open
let dayPct: float? = c != null && dayOpen != null && dayOpen != 0.0 ? (c - dayOpen) / dayOpen * 100.0 : null
let atrPct: float? = c != null && atr != null && c != 0.0 ? atr / c * 100.0 : null
let vwapPct: float? = c != null && vwap != null && vwap != 0.0 ? (c - vwap) / vwap * 100.0 : null
let oiNow = oi.close
let oiThen = oi.close[oiBars]
let oiPct: float? = oiNow != null && oiThen != null && oiThen != 0.0 ? (oiNow - oiThen) / oiThen * 100.0 : null
let rate = stats.forming.fundingRate
let annual: float? = rate != null ? rate * float(perDay) * 365.0 * 100.0 : null
let session = newYork.contains(t) ? "New York" : london.contains(t) ? "London" : asia.contains(t) ? "Asia" : "Off hours"
let lines = string[](8)
lines.push(str.format("{0} {1} {2}", context.base, context.exchange, context.timeframe))
lines.push(str.format("Price {0} {1} today", c ?? 0.0, pct(dayPct, 2)))
lines.push(str.format("ATR 14 {0:.2}% of price", atrPct ?? 0.0))
lines.push(str.format("VWAP {0} away", pct(vwapPct, 2)))
lines.push(str.format("Funding {0} p.a.", pct(annual, 1)))
lines.push(str.format("OI {0} bars {1}", oiBars, pct(oiPct, 2)))
if showSession {
lines.push(str.format("Session {0}", session))
}
let text = lines.reduce("", |acc, line| acc == "" ? line : str.concat(acc, "\n", line))
card.get("market").set(12.0, 12.0, text, font: font.mono)
}
}How it works
Anchored to the viewport
A drawing pool with an anchor: measures its coordinates in pixels from that point of the pane instead of in time and price. card.get("market").set(12.0, 12.0, text) places the label 12 pixels inward from the chosen corner, and the anchor also decides which corner of the label sits there. Panning and zooming do not move it. Setting the same key again replaces the text, which is how the card updates live.
A corner chosen in settings
The anchor constants cannot be offered directly in a dropdown, so the input is a plain text selection. A match expression in setup converts it into an anchor once, before the pool is made. The _ arm makes the expression exhaustive and gives a sensible default.
Indicators outside the live-edge check
The card is only drawn when chart.isLast is true, at the live edge. The two indicators are computed before that check, on every update during history and live. If chart.atr and chart.vwap were only called at the live edge, they would have no history and return null. This rule applies to every function that keeps history: call it unconditionally, use the result conditionally.
chart.vwap(reset:) restarts when reset is true, here on the first bar of each UTC day. The previous bar’s time is stored once per bar, when chart.isNew says a bar has just opened.
Building the text
string[](8) creates a local array with room for eight lines, and push adds each line. reduce joins them with line breaks into one multi-line label, set in the monospace font so the columns line up. The pct helper formats a signed percentage and returns "n/a" when the value is missing, so the card never shows a raw null.
daily.forming.open is today’s open, because the daily subscription’s forming bar is the day in progress. oi.close[oiBars] reads open interest oiBars confirmed periods back. Sessions are UTC windows from time.session.
Adapting it
- Two cards. Make a second pool with another anchor, for example an order-flow card with the last bar’s liquidations from
stats.sellLiqandstats.buyLiq, or the book imbalance fromdata.book. - Warning lines. Add a line only when something needs attention, such as funding above a threshold or price more than two ATRs from VWAP.
- Strategy status. In a strategy script, show
strategy.positionSize,strategy.openProfitandstrategy.netProfiton the card. See position and trades.
Related
- Market stats table shows the same kind of data in a separate window.
- Inputs and visuals
entitiesreference,tareference