Market HUD
One card at the bottom of the price pane that reads the market in one glance and follows every tick, in the glass look: a dark translucent card with round corners and rounded type, light on a light chart.
The trend word leads as the headline: Bullish when the close is above the EMA and RSI is above 50, Bearish when it is below both, else Mixed, in green, red or slate after a small dot. Under it, three activity rings read RSI, the taker buy share of the last 20 bars and this bar's volume so far against its 20-bar average, each value printed beside its ring; then ATR as a percent of price, and the open-interest change over the last 20 bars, counted in contracts, as a shaded sparkline. The EMA is drawn on price, and the legend names the lengths in use.
The card is one render.hud declaration of four tiles with look: "glass" (HUD cards, Looks, Blocks and tiles): a headline tile.pill coloured by a ladder output, a tile.rings of three outputs, a tile.value and a tile.spark, each reading data-only outputs written on every bar. It reads side-split trades.volume, an oi.close input that reads NaN where the market has none and a funding.rate_close input that reads NaN off perpetuals (Data sources), with the Ema, Rsi, Atr and Roc helpers (TA library). This is also the market-hud template: the Market HUD card under HUDs in the editor's starter list, and it compiles as written.
The wrun indicator
// Market HUD: one card in a corner of the price pane that reads the market in one glance, drawn by the chart in the
// glass look. The trader sees the trend word first, large (Bullish when the close is above the EMA and RSI is above
// its midline, Bearish when it is below both, else Mixed, in the bull, bear or neutral colour), then three activity
// rings with their readings beside them (RSI, the taker buy share over the flow window, and this bar's volume so far
// against its average over the same window), then ATR as a percent of price and the open-interest change over the
// flow window as a filled line, counted in contracts: coins on a perpetual (the dollar reading over the bar's close, so
// a price move alone never moves it), the reading as served on a prediction market (already a count of YES and NO
// pairs). The EMA itself is drawn on price. A tile whose market has no data prints a dash:
// open interest on spot, stocks and FX; the taker share where the venue has no sided trades. Every number on the card
// is a data-only output written on every bar, so the line has history and the Console or a watch reads the same
// values the card shows (the close's distance from the EMA too).
// Settings: every length is a whole number with bounds; the flow window also spans the open-interest change. The
// trend word's RSI midline is a setting too, and the last section holds the EMA line's colour.
section("Lengths");
param.int("ema_len", 50, { min: 2, max: 400, label: "EMA length, bars", description: "EMA length in bars, the trend reference" });
param.int("rsi_len", 14, { min: 2, max: 200, label: "RSI length, bars", description: "RSI length in bars (Wilder smoothing)" });
param.int("atr_len", 14, { min: 2, max: 200, label: "ATR length, bars", description: "ATR length in bars (Wilder smoothing)" });
param.int("flow_len", 20, { min: 2, max: 200, label: "Flow window, bars", description: "Bars in the flow window and in the open-interest change" });
section("Trend word");
param.number("rsi_mid", 50, { min: 1, max: 99, step: 1, label: "RSI midline", description: "RSI midline: Bullish needs RSI above it and the close above the EMA, Bearish needs both below" });
section("Look");
param.color("ema_color", "#38bdf8", { label: "EMA line" });
// The legend reads the lengths after the name ("Market HUD 50 14 14 20"): a tile label is a fixed string, so the
// card's labels name what they read in words and the settings in use ride the legend.
legend({ title: "{{ema_len}} {{rsi_len}} {{atr_len}} {{flow_len}}" });
// Inputs: the chart's own candles set the grid; flow reads sided trades (a bar with no prints reads 0); open
// interest reads NaN on a bar without a reading and where the market has none; funding is served on perpetuals only
// (NaN elsewhere), which tells the file its open interest is dollars to turn into coins.
input("close", ohlcv.close);
input("buy", trades.volume, { side: "BUY", missing: "zero" });
input("sell", trades.volume, { side: "SELL", missing: "zero" });
input("oi", oi.close, { missing: "nan" });
input("funding", funding.rate_close, { missing: "nan" });
// Outputs: the EMA is drawn on price; the rest are data-only, one value per bar behind the card's tiles. The first
// five keep their names and order from the earlier card; the trend rung, the EMA distance and the buy share are new.
// A static colour on a data-only output is the colour its dial, sparkline or meter draws in.
const emaLine = output("ema", line, overlay, { color: "@ema_color", width: 1, label: "EMA", format: "price", description: "EMA of the close over ema_len bars" }); // the colour setting paints it; the handle names it for the hover card
output("rsi", none, overlay, { color: "#a78bfa", format: "0.0", description: "RSI of the close over rsi_len bars" });
output("atr_pct", none, overlay, { format: "%", description: "ATR over atr_len bars as a percent of the close" });
output("flow", none, overlay, { description: "Buy minus sell volume over flow_len bars, as a percent of their sum" });
output("oi_change", none, overlay, { color: "#2dd4bf", format: "%", description: "Open-interest change over flow_len bars in contracts (coins on a perpetual, as served on a prediction market), percent" });
output("trend", none, overlay, { description: "The trend word as a ladder rung: 0 bearish, 1 mixed, 2 bullish" });
output("ema_dist", none, overlay, { format: "%", description: "Close minus the EMA, as a percent of the EMA" });
output("buy_share", none, overlay, { color: "#94a3b8", format: "%", description: "Taker buy volume over flow_len bars, as a percent of all taker volume" });
output("volume_ratio", none, overlay, { format: "%", description: "Bar volume as a percent of its average over flow_len bars; on the live bar, its volume so far" });
hover(emaLine, [block.value("EMA", "ema", { format: "price" }), block.rows([["RSI", "rsi", "0.0"], ["ATR %", "atr_pct", "0.00"], ["Flow %", "flow", "0.0"], ["OI change in contracts %", "oi_change", "0.00"]])]); // the card the EMA line opens under the cursor: its value and the card's numbers
// The trend word (or the bars the card still needs), written on the live bar into a bounded string slot the first
// tile reads.
string("trend_word", { max_bytes: 16 });
// The card, in the glass look (rounded type, a large headline, rings): the trend word leads as the headline across
// both columns, its colour following the trend rung (bear, neutral, bull); the rings span the card under it, RSI on
// the outer ring; ATR and the open-interest line pair up below. The look word takes any shipped look name.
render.hud("market", { position: "bottom_center", look: "glass", title: "Market", columns: 2, tiles: [
tile.pill("Trend", "trend_word", { headline: true, color_by: "trend", colors: ["#ff003c", "#94a3b8", "#10b981"] }),
tile.rings("Momentum, flow and volume", [
["RSI", "rsi", { min: 0, max: 100, color: "#fa114f" }],
["Buy share", "buy_share", { min: 0, max: 100, color: "#a6ff00" }],
["Volume so far", "volume_ratio", { min: 0, max: 200, color: "#00f0ff" }],
]),
tile.value("ATR, % of price", "atr_pct", { format: "%" }),
tile.spark("OI in contracts", "oi_change", { bars: 64 }),
] });
// The flow window is a ring of the last bars' buy and sell volume, sized for the largest window at load; onStart()
// reads the window in use.
const MAX_WINDOW = 200;
const buys = new StaticArray<f64>(MAX_WINDOW);
const sells = new StaticArray<f64>(MAX_WINDOW);
const vols = new StaticArray<f64>(MAX_WINDOW);
let volBars = 0;
let flowLen = 20;
let rsiMid = 50.0;
let warmBars = 50; // the bars the EMA, RSI and ATR need before the card answers
let ema = new Ema(50);
let rsi = new Rsi(14);
let atr = new Atr(14);
let rawChange = new Roc(20); // the open-interest change as served, and in coins: the funding feed picks one
let coinChange = new Roc(20);
let oiRaw: f64 = NaN; // the last open-interest reading and its coins; a new bar keeps them until its own reading lands
let oiCoins: f64 = NaN;
let perp = false; // funding seen: a perpetual, whose dollar open interest moves with price
let head = 0;
// onStart() runs once before the first bar: read the settings and size the helpers to them.
function onStart(): void {
flowLen = i32(p_flow_len());
rsiMid = p_rsi_mid();
warmBars = i32(Math.max(p_ema_len(), Math.max(p_rsi_len() + 1.0, p_atr_len())));
ema = new Ema(i32(p_ema_len()));
rsi = new Rsi(i32(p_rsi_len()));
atr = new Atr(i32(p_atr_len()));
rawChange = new Roc(flowLen);
coinChange = new Roc(flowLen);
}
// onBar() runs once per bar: fold the bar into every helper and the flow ring, then write the outputs; the trend
// word is written on the live bar only.
function onBar(): void {
const close = bar.close();
const emaValue = ema.update(close);
const rsiValue = rsi.update(close);
const atrPct = (atr.update(bar.high(), bar.low(), close) / close) * 100.0;
// The ring's sums are recounted from its slots: a running sum that adds and subtracts keeps a rounding crumb
// after a quiet stretch, and a crumb over a crumb would read as a share.
buys[head] = in_buy();
sells[head] = in_sell();
vols[head] = bar.volume();
head = (head + 1) % flowLen;
if (volBars < flowLen) volBars += 1;
let buySum = 0.0;
let sellSum = 0.0;
let volSum = 0.0;
for (let i = 0; i < flowLen; i += 1) {
buySum += buys[i];
sellSum += sells[i];
volSum += vols[i];
}
const total = buySum + sellSum;
const flowPct = total > 0.0 ? ((buySum - sellSum) / total) * 100.0 : NaN; // NaN where the venue has no sided trades
const buyShare = total > 0.0 ? (buySum / total) * 100.0 : NaN;
const oiNow = in_oi();
if (!isNaN(oiNow)) {
oiRaw = oiNow;
oiCoins = oiNow / close; // dollars over the close: the coins, whatever price did
}
if (!isNaN(in_funding())) perp = true;
const rawPct = rawChange.update(oiRaw);
const coinPct = coinChange.update(oiCoins);
const oiPct = perp ? coinPct : rawPct; // NaN on markets without open interest, and until the window is full
const volAvg = volSum / f64(volBars);
if (isNaN(emaValue) || isNaN(rsiValue) || isNaN(atrPct)) { // warm-up rows write nothing; the live bar says why
if (bar.isLast()) { sb_clear(); sb_text("Needs "); sb_int(warmBars); sb_text(" bars"); str_trend_word_sb(); }
return;
}
const bullish = close > emaValue && rsiValue > rsiMid;
const bearish = close < emaValue && rsiValue < rsiMid;
out_ema(emaValue);
out_rsi(rsiValue);
out_atr_pct(atrPct);
out_flow(flowPct);
out_oi_change(oiPct);
out_trend(bearish ? 0.0 : bullish ? 2.0 : 1.0);
out_ema_dist(((close - emaValue) / emaValue) * 100.0);
out_buy_share(buyShare);
out_volume_ratio(volAvg > 0.0 ? (bar.volume() / volAvg) * 100.0 : NaN); // on the live bar, its volume so far
if (bar.isLast()) {
sb_clear();
sb_text(bullish ? "Bullish" : bearish ? "Bearish" : "Mixed");
str_trend_word_sb();
}
}How it works
One declaration is the card. render.hud("market", { position: "bottom_center", look: "glass", title: "Market", columns: 2, tiles: [...] }) names the anchor, the look, the title and four tiles; the chart draws the card, keeps it at its anchor while the chart pans, and opens any tile as a hover card under the pointer. headline: true gives the trend pill the whole first row, where the answer reads before anything else. The rings are a wide tile that spans the card under it, and the ATR value and the open-interest sparkline share the last row.
The look draws the tiles. look: "glass" sets the surface (dark and slightly translucent, a 22 px corner radius, a soft shadow, light on a light chart), the type (the rounded font, the headline at 34 px, the title as written) and each tile kind's drawing: the pill as its word after a small dot in its colour, the spark as a line over a faint fill with a dot at its end. A word declared beside look wins over the look's own (Looks).
Three rings in one tile. tile.rings takes up to three [label, output, { min, max, color }] entries, outermost first: RSI from 0 to 100, the buy share from 0 to 100 and volume against its average from 0 to 200. Each ring fills the share of its own range its output holds, so a ring closes at RSI 100, at a window of nothing but taker buys, or on a bar with twice its average volume. Each ring takes its own color, and the legend beside the rings prints each value in its output's format.
Every number is an output. trend, rsi, buy_share, volume_ratio, atr_pct and oi_change are data-only outputs (none), one value per bar. A tile reads the newest row and the sparkline the last 64. Each output's format word prints it (% adds a percent sign to a value already in percent units, so 0.09 reads 0.09%), and the static color on oi_change is the colour its sparkline draws in. flow (buy minus sell volume as a percent of their sum) rides the hover card the EMA line opens, and ema_dist (the close's distance from the EMA, percent) is written beside them for anything that reads it.
The trend word is a slot and a ladder. The word is written into a bounded string slot on the live bar only. The pill's colour follows the trend output: 0 bearish, 1 mixed and 2 bullish pick red, slate and green from its colors. Until the EMA, RSI and ATR have the bars they need, the slot says so instead ("Needs 50 bars", the longest of the three lengths).
Open interest is counted in contracts. On a perpetual the oi source serves open interest in dollars, which rise and fall with price even when no position opens or closes, so each reading is divided by the bar's close and the sparkline and its tile ("OI in contracts") move only when positions do: price up 2% with fewer coins open reads as a fall. The funding input tells a perpetual apart, since only a perpetual serves funding. A prediction market's open interest is its collateral, one dollar per YES and NO pair, so it is already a count of contracts and is read as served. A new bar keeps the last reading until its own lands, so the tile does not blink to a dash at each bar's open.
The windows are fixed arrays. Buy, sell and total volume of the last flow_len bars sit in three arrays sized at load, each bar writing over the slot of the bar that leaves the window, and their sums are recounted from the slots on every bar, so a quiet stretch reads as no trades rather than as a rounding crumb. The buy share is buy volume over all taker volume in the window. Volume against its average is a bar's volume over the mean of the window, that bar included; on the live bar that is its volume so far, as the ring's label says ("Volume so far"), so the ring starts low at each bar's open and fills as the bar trades. Until the window fills, the mean counts only the bars seen so far.
The lengths ride the legend. A tile's label is a fixed string, so legend({ title: "{{ema_len}} {{rsi_len}} {{atr_len}} {{flow_len}}" }) prints the lengths in use after the name ("Market HUD 50 14 14 20") and the labels say in words what each tile reads.
Where it runs
Every market with candles: crypto perpetuals and spot, Hyperliquid, a prediction market's single outcome, US stocks and FX. Open interest needs a perpetual (counted in coins) or a prediction market (its collateral, read as served), the taker share needs sided trades and the volume ring needs volume, so the open-interest sparkline prints a dash on spot, stocks and FX, the buy share ring on stocks and FX, and the volume ring on FX, whose candles carry no volume. The card sits at the bottom centre of the price pane, away from the legend, the High and Low tags, the price axis, the newest candles at the right edge and the round button at the bottom left; on a chart whose candles dip into the bottom middle it covers them, and position moves it.
When data is missing
A tile whose output has no number prints a dash, and a ring without a reading stays empty with a dash beside it: open interest where the market has none, the taker share where the venue serves no sided trades, volume against its average where the candles carry no volume. The trend word, ATR and RSI need only candles, so the card always answers. The first ema_len bars are warm-up rows that write nothing, so the sparkline starts after them; on a chart with fewer bars than the lengths need (an ema_len of 400 on a short history), the headline reads "Needs 400 bars" instead of leaving the card blank.
Customize it
- Another window.
flow_len(20) sets the taker window, the volume average and the open-interest change together; the legend shows the new length. - Another corner. Set
positionto any of the nine anchors,top_lefttobottom_right;columns: 1stacks every tile in a narrow card. - Another tile. Declare a data-only output, write it in
onBar(), and add a tile that reads it, such astile.value("Close vs EMA", "ema_dist", { format: "%" })for the distance already computed. - Change the look.
look:takes any of the twelve shipped looks,defaulttoclassic(Looks), and a word declared beside it still wins; the Look row on the indicator's Style page switches it without code (The Style page).
Run it
- In the editor's Explorer, press the Templates icon ("Browse starter templates") and pick Market HUD under HUDs.
- Press Run: the EMA draws on price and the glass card appears at the bottom of the price pane. Rest the pointer on a tile to open it as a hover card.
- At the editor's Console prompt, type
outputsto read the newest value of every output the card shows.
Concepts used
- HUD cards for
render.hud, its anchors and its columns - Looks for
look: "glass"and what it draws for each tile kind - Blocks and tiles for the headline pill,
tile.rings, their options and the format words - The Style page for the Look row that switches the look without code
- Hover cards for the card the EMA line and each tile open
- Legend for
legend({ title })and its{{setting}}placeholders - Data sources for the
tradesandoisources and theirmissingpolicies - TA library for
Ema,Rsi,AtrandRoc