Decision board
One card at the bottom of the price pane that grades the market factor by factor and says which side is ahead, so the chart keeps its full height.
It is printed in the broadsheet look: cream paper, black ink, serif type and a thick and a thin rule over the title in small capitals. A headline the script writes from the six factors leads: "Buyers Ahead, 3 to 1" when the bullish factors lead the bearish ones by two or more, "Sellers Ahead" when the bearish ones do, else "Signals Split", each followed by the two counts. Under it, the net score (bullish factors minus bearish ones) of the last 64 bars runs as a stippled line, then the six readings the score comes from, each joined to its value by a dotted leader: the close against the EMA, RSI, this bar's volume so far against its 20-bar average, taker flow, the open-interest change in contracts and the longs' share of liquidations. The EMA is drawn on price, and the legend names the lengths in use.
The card is one render.hud declaration of three tiles in one column 300 px wide, with look: "broadsheet" (HUD cards, Looks, Blocks and tiles): a headline tile.pill over a string slot, a tile.spark and a tile.rows list, each reading data-only outputs written on every bar. It reads side-split trades.volume, oi.close reading NaN where the market has none, funding.rate_close reading NaN off perpetuals, and liquidations split by side (Data sources), with the Ema, Rsi, Sma and Roc helpers (TA library). This is also the decision-board template: the Decision Board card under HUDs in the editor's starter list, and it compiles as written.
The wrun indicator
// Decision Board: one card in a corner of the price pane that grades the market factor by factor and says which
// way it leans, drawn by the chart in the broadsheet look. The trader reads a headline first, written by the script
// from the count ("Buyers Ahead, 4 to 1", "Sellers Ahead, 3 to 1" or "Signals Split, 2 to 2": the leading side's
// factors against the other side's), then the net score (bullish factors minus bearish ones, -6 to +6) over the last
// 64 bars as a stippled line, then the six readings behind it in dotted-leader rows: the close against the EMA, RSI,
// volume against its average (the live bar's volume so far), taker flow, the open-interest change in contracts (coins
// on a perpetual, as served on a prediction market) and the longs' share of liquidations. A factor whose market has no data prints a dash and leaves the count: open interest and
// liquidations on spot, stocks and FX; flow and volume on FX. The EMA is drawn on price, and every number on the
// card is a data-only output written on every bar, so the Console or a watch reads the same values the card shows.
// Settings: the lengths, one threshold per factor rule, the margin the lean needs, and the EMA line's colour.
section("Lengths");
param.int("ema_len", 50, { min: 2, max: 500, 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" });
param.int("window", 20, { min: 2, max: 200, label: "Window, bars", description: "Bars in the volume, flow, open-interest and liquidation windows" });
section("Factor thresholds");
param.number("rsi_bull", 55, { min: 50, max: 100, step: 1, label: "RSI bullish above", description: "RSI above this level reads bullish momentum" });
param.number("rsi_bear", 45, { min: 0, max: 50, step: 1, label: "RSI bearish below", description: "RSI below this level reads bearish momentum" });
param.number("volume_spike", 1.2, { min: 1, max: 10, step: 0.1, label: "Volume spike, times average", description: "Volume at this multiple of its average or more counts: bullish on an up bar, bearish on a down bar (the live bar counts its volume so far)" });
param.number("flow_edge", 5, { min: 0, max: 100, step: 0.5, label: "Flow edge, percent", description: "Taker flow above this percent reads bullish, below minus this percent bearish" });
param.number("oi_flat", 0.05, { min: 0, max: 10, step: 0.01, label: "Minimum OI rise, percent", description: "Open interest rising more than this percent counts in the direction price moved (new positions); flat or falling open interest reads neutral (positions closing). Counted in coins on a perpetual" });
param.number("liq_heavy", 65, { min: 50, max: 100, step: 1, label: "Heavy long share, percent", description: "Longs over this percent of liquidations read bearish; longs under 100 minus it read bullish" });
param.int("lean_margin", 2, { min: 1, max: 6, label: "Lean margin, factors", description: "Bullish factors over bearish ones needed to lean Long (and the reverse to lean Short)" });
section("Look");
param.color("ema_color", "#38bdf8", { label: "EMA line" });
// The legend reads the lengths after the name ("Decision Board 50 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}} {{window}}" });
// Inputs: the chart's own candles set the grid; flow and liquidations read 0 on a bar without prints; 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" });
input("liq_buy", liquidations.liquidations, { side: "BUY", missing: "zero" }); // shorts liquidated: forced buying
input("liq_sell", liquidations.liquidations, { side: "SELL", missing: "zero" }); // longs liquidated: forced selling
// Outputs: the EMA on price; the rest are data-only, one value per bar behind the card's tiles. The first three keep
// their names and order from the earlier board; the bearish count, the net score, the lean rung and the six
// readings are new. A static colour on a data-only output is the colour its 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("score", none, overlay, { format: "int", description: "Bullish factors on this bar" });
output("factors", none, overlay, { format: "int", description: "Factors with data on this bar" });
output("bearish", none, overlay, { format: "int", description: "Bearish factors on this bar" });
output("net", none, overlay, { format: "int", description: "Bullish minus bearish factors, -6 to +6" });
output("lean", none, overlay, { description: "The lean as a ladder rung: 0 short, 1 neutral, 2 long" });
output("ema_dist", none, overlay, { format: "%", description: "Close minus the EMA, as a percent of the EMA" });
output("rsi", none, overlay, { format: "0.0", description: "RSI of the close over rsi_len bars" });
output("volume_ratio", none, overlay, { format: "0.00", unit: "x", description: "Bar volume over its average across the window; on the live bar, its volume so far" });
output("flow", none, overlay, { format: "%", description: "Taker buy minus sell volume over the window, as a percent of their sum" });
output("oi_change", none, overlay, { format: "%", description: "Open-interest change over the window in contracts (coins on a perpetual, as served on a prediction market), percent" });
output("long_share", none, overlay, { format: "%", description: "Longs liquidated as a percent of all liquidations in the window" });
hover(emaLine, [block.value("EMA", "ema", { format: "price" }), block.rows([["Bullish factors", "score", "int"], ["Bearish factors", "bearish", "int"], ["Factors with data", "factors", "int"]])]); // the card the EMA line opens under the cursor
// The lean word, written on the live bar into a bounded string slot the first tile reads.
string("lean_word", { max_bytes: 8 });
string("headline_text", { max_bytes: 48 }); // the headline sentence, written on the live bar
// The card, in the broadsheet look (newsprint, serif type, small caps, dotted leaders): one column 300 px wide, the
// headline sentence first, then the stippled score line and the six readings. The lean word (Long, Neutral or
// Short) still rides its own slot for a hover card or a watch.
render.hud("decision", { position: "bottom_center", look: "broadsheet", title: "Decision board", columns: 1, width: 300, tiles: [
tile.pill("Lean", "headline_text", { headline: true }),
tile.spark("Net score, 64 bars", "net", { bars: 64 }),
tile.rows([["Close vs EMA", "ema_dist", "%"], ["RSI", "rsi", "0.0"], ["Volume so far", "volume_ratio", "0.00"], ["Taker flow", "flow", "%"], ["OI change, contracts", "oi_change", "%"], ["Long share of liquidations", "long_share", "%"]]),
] });
const FACTORS = 6;
const NA = 9; // a factor without data on this market
const MAX_WINDOW = 200; // the rings are sized for the largest window at load; onStart() reads the one in use
const buys = new StaticArray<f64>(MAX_WINDOW);
const sells = new StaticArray<f64>(MAX_WINDOW);
const liqBuys = new StaticArray<f64>(MAX_WINDOW);
const liqSells = new StaticArray<f64>(MAX_WINDOW);
const tones = new StaticArray<i32>(FACTORS); // per factor: 1 bullish, -1 bearish, 0 neutral, NA without data
let window = 20;
let rsiBull = 55.0;
let rsiBear = 45.0;
let volumeSpike = 1.2;
let flowEdge = 5.0;
let oiFlat = 0.05;
let liqHeavy = 65.0;
let leanMargin = 2;
let ema = new Ema(50);
let rsi = new Rsi(14);
let volumeMean = new Sma(20);
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 priceChange = new Roc(20);
let head = 0;
let tradesSeen = false; // the venue has served sided trades at least once
let liqSeen = false; // the venue has served liquidations at least once
let close: f64 = NaN;
let prev: f64 = NaN;
let emaV: f64 = NaN;
let rsiV: f64 = NaN;
let volumeRatio: f64 = NaN;
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 flowPct: f64 = NaN;
let oiPct: f64 = NaN;
let pricePct: f64 = NaN;
let longShare: f64 = NaN; // longs liquidated as a percent of all liquidations in the window
let bullish = 0;
let bearish = 0;
let withData = 0;
// Judge each factor from this bar's readings. A helper still warming up, or a market without the data, reads NA
// and stays out of both counts. Open interest rising confirms the price move (new positions behind it); flat or
// falling open interest is positions closing, which confirms nothing.
function judge(): void {
tones[0] = !isFinite(emaV) ? NA : close > emaV ? 1 : close < emaV ? -1 : 0;
tones[1] = !isFinite(rsiV) ? NA : rsiV > rsiBull ? 1 : rsiV < rsiBear ? -1 : 0;
tones[2] = !isFinite(volumeRatio) ? NA : volumeRatio >= volumeSpike && isFinite(prev) ? (close > prev ? 1 : close < prev ? -1 : 0) : 0;
tones[3] = !tradesSeen ? NA : !isFinite(flowPct) ? 0 : flowPct > flowEdge ? 1 : flowPct < -flowEdge ? -1 : 0;
tones[4] = !isFinite(oiPct) || !isFinite(pricePct) ? NA : oiPct <= oiFlat ? 0 : pricePct > 0.0 ? 1 : pricePct < 0.0 ? -1 : 0;
tones[5] = !liqSeen ? NA : !isFinite(longShare) ? 0 : longShare > liqHeavy ? -1 : longShare < 100.0 - liqHeavy ? 1 : 0;
bullish = 0;
bearish = 0;
withData = 0;
for (let i = 0; i < FACTORS; i += 1) {
if (tones[i] == NA) continue;
withData += 1;
if (tones[i] > 0) bullish += 1;
else if (tones[i] < 0) bearish += 1;
}
}
// onStart() runs once before the first bar: read the settings and size every helper to them.
function onStart(): void {
window = i32(p_window());
rsiBull = p_rsi_bull();
rsiBear = p_rsi_bear();
volumeSpike = p_volume_spike();
flowEdge = p_flow_edge();
oiFlat = p_oi_flat();
liqHeavy = p_liq_heavy();
leanMargin = i32(p_lean_margin());
ema = new Ema(i32(p_ema_len()));
rsi = new Rsi(i32(p_rsi_len()));
volumeMean = new Sma(window);
rawChange = new Roc(window);
coinChange = new Roc(window);
priceChange = new Roc(window);
}
// onBar() runs once per bar: read the bar, update every helper and window, judge the factors, then write the
// outputs; the lean word is written on the live bar only.
function onBar(): void {
prev = close;
close = bar.close();
emaV = ema.update(close);
rsiV = rsi.update(close);
const volume = bar.volume();
const volumeAvg = volumeMean.update(volume);
volumeRatio = volumeAvg > 0.0 ? volume / volumeAvg : NaN; // NaN on markets without volume (FX); the live bar's so far
// The windows are rings of the last bars, their sums recounted from the 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.
const buy = in_buy();
const sell = in_sell();
const liqBuy = in_liq_buy();
const liqSell = in_liq_sell();
buys[head] = buy;
sells[head] = sell;
liqBuys[head] = liqBuy;
liqSells[head] = liqSell;
head = (head + 1) % window;
if (buy + sell > 0.0) tradesSeen = true;
if (liqBuy + liqSell > 0.0) liqSeen = true;
let buySum = 0.0;
let sellSum = 0.0;
let liqBuySum = 0.0;
let liqSellSum = 0.0;
for (let i = 0; i < window; i += 1) {
buySum += buys[i];
sellSum += sells[i];
liqBuySum += liqBuys[i];
liqSellSum += liqSells[i];
}
const flowTotal = buySum + sellSum;
flowPct = flowTotal > 0.0 ? ((buySum - sellSum) / flowTotal) * 100.0 : NaN;
const liqTotal = liqBuySum + liqSellSum;
longShare = liqTotal > 0.0 ? (liqSellSum / liqTotal) * 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);
oiPct = perp ? coinPct : rawPct;
pricePct = priceChange.update(close);
judge();
if (isNaN(emaV)) return; // the first ema_len bars are warm-up rows: nothing written
const net = bullish - bearish;
out_ema(emaV);
out_score(f64(bullish));
out_factors(f64(withData));
out_bearish(f64(bearish));
out_net(f64(net));
out_lean(net >= leanMargin ? 2.0 : net <= -leanMargin ? 0.0 : 1.0);
out_ema_dist((close / emaV - 1.0) * 100.0);
out_rsi(rsiV);
out_volume_ratio(volumeRatio);
out_flow(flowPct);
out_oi_change(oiPct);
out_long_share(longShare);
if (bar.isLast()) {
sb_clear();
sb_text(net >= leanMargin ? "Long" : net <= -leanMargin ? "Short" : "Neutral");
str_lean_word_sb();
sb_clear();
// A headline, short enough for one line: the leading side and the signal count, bullish to bearish.
sb_text(net >= leanMargin ? "Buyers Ahead, " : net <= -leanMargin ? "Sellers Ahead, " : "Signals Split, ");
if (net <= -leanMargin) { sb_int(i64(bearish)); sb_text(" to "); sb_int(i64(bullish)); }
else { sb_int(i64(bullish)); sb_text(" to "); sb_int(i64(bearish)); }
str_headline_text_sb();
}
}How it works
Each factor is a rule. Trend is bullish with the close above the EMA (ema_len 50) and bearish below; RSI (rsi_len 14) is bullish above 55 and bearish below 45; volume is bullish at 1.2 times its window average on an up bar and bearish on a down bar (on the live bar its volume so far, so the factor can join the count as the bar trades); taker flow is bullish above +5% and bearish below -5%; open interest counted in contracts (on a perpetual the dollar reading over the close, so a price move alone never moves it; on a prediction market the collateral as served) that rose more than 0.05% over the window confirms the price move, bullish when price rose over the same window and bearish when it fell, while flat or falling open interest is positions closing and reads neutral; liquidations read longs over 65% of what was flushed as bearish and under 35% as bullish. Every threshold is a setting in the Factor thresholds section.
The score and the headline. The score is the bullish factors minus the bearish ones; the market leans long at +2 or more and short at -2 or less (lean_margin). A factor the market has no data for is left out of both counts. On the live bar the script writes the headline into the bounded headline_text slot from the same counts: "Buyers Ahead" and the bullish count first when the score reaches the margin, "Sellers Ahead" and the bearish count first when it reaches minus the margin, else "Signals Split" with the bullish count first.
One declaration is the card. render.hud("decision", { position: "bottom_center", look: "broadsheet", title: "Decision board", columns: 1, width: 300, tiles: [...] }) names the anchor, the look, the title and three tiles; the chart draws the card and keeps it at its anchor while the chart pans. headline: true sets the pill in the look's headline type, and width: 300 gives the one-column card room for the longest row (a one-column card is 180 px unless it says otherwise).
The look draws the tiles. look: "broadsheet" prints on cream paper in black ink with serif type and square corners, sets the title in small capitals under a thick and a thin rule, draws the spark as dots (a stippled line) and joins each row's label to its value with a dotted leader. It keeps its paper on a dark and a light chart alike (Looks).
Every number is an output. The six readings (ema_dist, rsi, volume_ratio, flow, oi_change, long_share), the counts (score for bullish, bearish, factors with data), net and the lean rung (0 short, 1 neutral, 2 long) are data-only outputs written on every bar. The spark reads the last 64 values of net, and the rows print each reading with its format word (% adds a percent sign to a value already in percent units; volume_ratio adds its unit, so it reads 1.35x). The lean is also written as a word, Long, Neutral or Short, into the lean_word slot, which no tile reads until you add one (below).
The windows are rings. Buy, sell and liquidation volume of the last window bars sit in fixed rings sized at load, their sums recounted from the slots on every bar, so a quiet stretch reads as no prints rather than as a rounding crumb. Open interest is kept in contracts, and a new bar keeps the last reading until its own lands, so the factor count does not drop at each bar's open. A perpetual is told apart by its funding input, which only a perpetual serves.
The lengths ride the legend. A tile's label is a fixed string, so legend({ title: "{{ema_len}} {{rsi_len}} {{window}}" }) prints the lengths in use after the name ("Decision Board 50 14 20") and the row labels say in words what each reading is.
Where it runs
Every market with candles, a prediction market's single outcome included. Crypto perpetuals have all six factors, their open interest counted in coins. Spot has no open interest or liquidations, and prediction markets no liquidations, their open interest read as served (one dollar of collateral per YES and NO pair, already a count of contracts); US stocks keep the trend, RSI and volume, and FX only the trend and RSI (its 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 factor without data prints a dash in its row and leaves both counts, so the score's reach shrinks with the market: four factors on spot, two on FX, where the headline reads at most 2 to 0. Flow and liquidations count only once the venue has served one print; after that a window without prints reads neutral (the row shows a dash because there is no share to print). The first ema_len bars are warm-up rows that write nothing, so the stippled line starts after them.
Customize it
- Other thresholds. Every cut-off is a setting under Factor thresholds; the lengths are under Lengths.
- A seventh factor. Add an input, a rule in
judge()(raiseFACTORS), an output and a row; the headline counts it with the others. - The lean as a word.
tile.pill("Lean", "lean_word", { color_by: "lean", colors: ["#ff003c", "#94a3b8", "#10b981"] })puts Long, Neutral or Short on the card in red, slate or green, read from the slot and the rung the file already writes. - 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 Decision Board under HUDs.
- Press Run: the EMA draws on price and the broadsheet 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
last 20 netto read the score of the last 20 bars.
Concepts used
- HUD cards for
render.hud, its anchors, its columns and its width - Looks for
look: "broadsheet"and what it draws for each tile kind - Blocks and tiles for the headline pill, the spark, the rows and the format words
- The Style page for the Look row that switches the look without code
- Plotting for data-only outputs and the string slots the headline and the lean word are written into
- Legend for
legend({ title })and its{{setting}}placeholders - Data sources for the
trades,oiandliquidationssources and theirmissingpolicies - TA library for
Ema,Rsi,SmaandRoc