FlowscopeDocs

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.

viewport-hud.fs
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.sellLiq and stats.buyLiq, or the book imbalance from data.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.openProfit and strategy.netProfit on the card. See position and trades.