---
title: "Order flow HUD"
description: "A card at the bottom centre of the price pane that says who is pushing price, read from the exchange's own taker side and kept current on the live bar. It is…"
order: 94
section: "cookbook"
---

<!-- source: docs/indicators/cookbook/order-flow-hud.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Order flow HUD

A card at the bottom centre of the price pane that says who is pushing price, read from the exchange's own taker side and kept current on the live bar. It is drawn in the chart desk look: a white page with square corners, a small red tag at its top left and the title "Tug of war". It answers in a word first, as the headline: Buyers pushing, Sellers pushing or Balanced, in green, red or slate. Under the word, each of the last 24 bars draws as a red bar from the 50 mark, rising when taker buys made up more than half of that bar's taker volume and hanging below when taker sells did; the taker buy share of the last 20 bars sits on a meter around 50, the net taker delta of those bars reads in dollars with this bar's own delta in the capsule beside it, and two rows count the longs and the shorts liquidated over the same 20 bars. Nothing is drawn on price.

The card is one `render.hud(...)` of five tiles in one column 300 px wide, with `look: "chart_desk"`: a headline pill, a spark, a meter, a value and rows ([HUD cards](../presentation/hud-and-hover-cards.md#hud-cards), [Looks](../presentation/hud-and-hover-cards.md#looks), [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles)). Its numbers are data-only outputs written on every bar and its first word a string slot written on the live bar ([Plotting](../presentation/plotting.md)). The inputs are side-split `trades.volume` and `liquidations.liquidations`, both read with `missing: "zero"` ([Data sources](../core-concepts/data-sources.md)), and the sums are the delta and cumulative delta of the [Order flow kit](../functions/order-flow-kit.md), written out by hand. This is also the `order-flow-hud` template: the **Order Flow HUD** card under **HUDs** in the editor's starter list, and it compiles as written.

## The wrun indicator

```typescript sample=order-flow-hud
// Order Flow HUD: who is pushing price, read from the exchange's own taker side, drawn by the chart in the chart
// desk look. One card answers in a word first (Buyers pushing, Sellers pushing or Balanced, in green, red or slate),
// then charts each bar's taker buy share as bars around the 50 mark (the tug of war), shows the window's buy share on
// a meter around 50, the window's net taker delta in dollars with this bar's own delta beside it, and the longs and
// shorts liquidated over the window. Nothing is drawn on price: the card is the whole indicator, and every number on
// it is also a data-only output the Console and a watch can read (the cumulative delta too). A market without sided
// trades (FX, stocks) says so in the first tile; a spot market has no liquidations, so those two rows stay empty.

// Settings: the window every sum covers, and how far from even the buy share must sit before the card names a side.
section("Flow window");
param.int("flow_len", 20, { min: 2, max: 200, label: "Flow window in bars", description: "Bars the net delta, the buy share and the liquidations are summed over" });
param.number("lean", 5.0, { min: 0.5, max: 25.0, step: 0.5, label: "Balanced band in points", description: "Points the taker buy share must sit above or below 50 before the card names buyers or sellers; inside the band it reads Balanced" });
legend({ title: "({{flow_len}})" }); // the window after the name in the legend

// Inputs: the chart's own candles set the grid. Taker buys and sells arrive in coins and read 0 on a bar without
// prints; liquidations arrive in dollars, longs as forced sells and shorts as forced buys, 0 on a quiet bar.
input("close", ohlcv.close);
input("buy", trades.volume, { side: "BUY", missing: "zero" });
input("sell", trades.volume, { side: "SELL", missing: "zero" });
input("long_liq", liquidations.liquidations, { side: "SELL", missing: "zero" });
input("short_liq", liquidations.liquidations, { side: "BUY", missing: "zero" });

// Outputs: all data-only, one value per bar; the card reads the newest row of each. A colour on an output paints the
// mark that reads it: the meter's fill, the sparkline, the dot beside a liquidation row.
output("side", none, overlay, { description: "0 sellers pushing, 1 balanced or no data, 2 buyers pushing: the first tile's colour" });
output("buy_share", none, overlay, { color: "#10b981", format: "%", description: "Taker buys as a percent of all taker volume over the window" });
output("bar_buy_share", none, overlay, { format: "%", description: "This bar's taker buys as a percent of its taker volume" });
output("net_delta", none, overlay, { format: "si", description: "Taker buys minus taker sells over the window, in dollars" });
output("bar_delta", none, overlay, { format: "si", description: "This bar's taker buys minus taker sells, in dollars" });
output("cvd", none, overlay, { color: "#38bdf8", format: "si", description: "Cumulative taker delta since the first loaded bar, in dollars" });
output("long_liqs", none, overlay, { color: "#ff003c", format: "si", description: "Longs liquidated over the window, in dollars" });
output("short_liqs", none, overlay, { color: "#10b981", format: "si", description: "Shorts liquidated over the window, in dollars" });
output("window_bars", none, overlay, { format: "int", description: "The flow window in bars, read by the card's title" });
string("who", { max_bytes: 40 }); // the first tile's words, written on the live bar

// The card, in the chart desk look (a red tag, a short title, bars that grow from the meter's 50 mark): one column
// 300 px wide, the deciding word first, then the tug-of-war bars, the meter, the delta and the liquidation rows.
render.hud("order_flow", { position: "bottom_center", look: "chart_desk", title: "Tug of war", columns: 1, width: 300, tiles: [
  tile.pill("Who is pushing", "who", { headline: true, color_by: "side", colors: ["#ff003c", "#94a3b8", "#10b981"] }),
  tile.spark("Each bar's buy share, %", "bar_buy_share", { bars: 24 }),
  tile.meter("Taker buy share over the window", "buy_share", { min: 0, max: 100, marks: [50] }),
  tile.value("Net taker delta, USD", "net_delta", { format: "si", delta: "bar_delta" }),
  tile.rows("Liquidations, USD", [["Longs", "long_liqs", "si"], ["Shorts", "short_liqs", "si"]]),
] });

const MAX_WINDOW = 200; // the flow_len ceiling: the rings are sized for it once, at load
const buys = new StaticArray<f64>(MAX_WINDOW); // each bar's taker buys in dollars, one slot per bar of the window
const sells = new StaticArray<f64>(MAX_WINDOW);
const longs = new StaticArray<f64>(MAX_WINDOW); // each bar's liquidations in dollars
const shorts = new StaticArray<f64>(MAX_WINDOW);

const SELLERS = 0.0; // the first tile's ladder rungs, in the colours' order: bear, neutral, bull
const BALANCED = 1.0;
const BUYERS = 2.0;

let flowLen = 20; // settings, read in onStart()
let lean = 5.0;
let head = 0; // the ring slot the next bar writes: it holds the bar that is leaving the window
let filled = 0; // bars in the window so far, up to flowLen
let buySum = 0.0; // the window's sums, kept by adding the new bar and taking out the one that left
let sellSum = 0.0;
let longSum = 0.0;
let shortSum = 0.0;
let cvd = 0.0; // the running delta since the first loaded bar
let sidedSeen = 0.0; // every sided dollar seen: 0 means the market serves no sided trades
let liquidationsSeen = 0.0; // every liquidated dollar seen: 0 means the market serves none

function onStart(): void {
  flowLen = i32(p_flow_len());
  lean = p_lean();
}

// onBar() runs once per bar: price the bar's taker volume in dollars at its close, roll the window, write every
// output, and on the live bar write the first tile's words.
function onBar(): void {
  const close = bar.close();
  const buyCoins = in_buy();
  const sellCoins = in_sell();
  const longRaw = in_long_liq();
  const shortRaw = in_short_liq();
  const buy = isFinite(buyCoins) && isFinite(close) ? buyCoins * close : 0.0;
  const sell = isFinite(sellCoins) && isFinite(close) ? sellCoins * close : 0.0;
  const longLiq = isFinite(longRaw) ? longRaw : 0.0;
  const shortLiq = isFinite(shortRaw) ? shortRaw : 0.0;
  sidedSeen += buy + sell;
  liquidationsSeen += longLiq + shortLiq;
  cvd += buy - sell;

  // Roll the window: take out the bar this slot held, put this bar in its place.
  buySum += buy - buys[head];
  sellSum += sell - sells[head];
  longSum += longLiq - longs[head];
  shortSum += shortLiq - shorts[head];
  buys[head] = buy;
  sells[head] = sell;
  longs[head] = longLiq;
  shorts[head] = shortLiq;
  head = (head + 1) % flowLen;
  if (filled < flowLen) filled += 1;

  // The readings: nothing until the window is full, nothing on a market that never served the feed.
  const hasTrades = sidedSeen > 0.0;
  const hasLiquidations = liquidationsSeen > 0.0;
  const warm = filled >= flowLen;
  const total = buySum + sellSum;
  const share = hasTrades && warm && total > 0.0 ? (buySum / total) * 100.0 : NaN;
  let side = BALANCED;
  if (share >= 50.0 + lean) side = BUYERS;
  else if (share <= 50.0 - lean) side = SELLERS; // a NaN share passes neither test and stays Balanced
  out_side(side);
  out_buy_share(share);
  out_bar_buy_share(hasTrades && buy + sell > 0.0 ? (buy / (buy + sell)) * 100.0 : NaN);
  out_net_delta(hasTrades && warm ? buySum - sellSum : NaN);
  out_bar_delta(hasTrades ? buy - sell : NaN);
  out_cvd(hasTrades ? cvd : NaN);
  out_long_liqs(hasLiquidations && warm ? Math.max(0.0, longSum) : NaN); // the floor clears rounding left by the rolling sum
  out_short_liqs(hasLiquidations && warm ? Math.max(0.0, shortSum) : NaN);
  out_window_bars(f64(flowLen));

  // The first tile's words, on the live bar only: the deciding word, or why there is none.
  if (bar.isLast()) {
    sb_clear();
    if (!hasTrades) sb_text("No sided trades on this market");
    else if (!warm) sb_text("Not enough bars yet");
    else if (isNaN(share)) sb_text("No trades in the window");
    else if (side == BUYERS) sb_text("Buyers pushing");
    else if (side == SELLERS) sb_text("Sellers pushing");
    else sb_text("Balanced");
    str_who_sb();
  }
}
```

## How it works

**Taker volume, in dollars.** `trades.volume` with `side: "BUY"` and `side: "SELL"` hands each bar the coins that takers bought and sold, the side that crossed the spread. Each bar's coins are priced at its close, so the deltas are in dollars like the liquidations, and read the same way on any coin.

**One window, kept as a ring.** The last `flow_len` (20) bars of taker buys, taker sells and liquidations sit in rings sized once for the largest setting. Each bar adds itself and takes out the bar that left, so the sums cost the same at 20 bars or 200. The buy share is buys over buys plus sells and the net delta is buys minus sells; both, and the liquidation sums, read a dash until the window is full.

**The deciding word.** A buy share at or above 50 plus `lean` (5) reads Buyers pushing, at or below 50 minus `lean` reads Sellers pushing, and anything between reads Balanced. A data-only `side` output (0, 1 or 2) picks the pill's colour from its ladder: red, slate, green.

**Each bar against the 50 mark.** `bar_buy_share` is one bar's taker buys as a percent of its taker volume. The chart desk look draws a spark as bars 96 px tall that grow from the card's first meter mark, so with the meter marked at 50 the spark shows, bar by bar, which side took the larger part. `bars: 24` keeps the last 24, and the number beside the spark's label is the newest bar's share.

**This bar in the capsule.** The net delta's capsule is the live bar's own buys minus sells, with an up or down arrow, so the card shows the window and the bar at once.

**The cumulative delta stays an output.** `cvd` is a running sum of each bar's dollar delta, written on every bar for the Console, though no tile draws it. It counts from the first loaded bar, so its number moves when more history loads: read its slope, not its level.

**Liquidations.** Longs are liquidated by forced sells and shorts by forced buys, so `side: "SELL"` reads the longs and `side: "BUY"` the shorts, already in dollars. The two rows sum the window; the red and green dots beside them are the two outputs' own colours.

**The look draws the tiles.** `look: "chart_desk"` sets the white page with dark text, the red tag at the top left and the title as written, prints the headline as its word alone in its colour and draws the spark as bars from the meter's mark in the look's red. It keeps its paper on a dark and a light chart alike ([Looks](../presentation/hud-and-hover-cards.md#looks)).

**Live.** Every tile reads the newest row of its output, and the chart folds live prints into the live bar as they arrive, so the card refreshes about once a second while the bar is open.

## Where it runs

Every market with sided trades: crypto perpetuals and spot on the venues the chart carries (Binance, Hyperliquid and the rest) and prediction markets. The liquidation rows fill on perpetuals with a liquidations feed; a spot market has none, so they stay empty there. FX and stocks have no sided trades: the first tile reads "No sided trades on this market".

## When data is missing

- A market with no sided trades at all: the first tile says so and every number reads a dash, never a fake zero.
- A market with sided trades but none inside the window: the first tile reads "No trades in the window".
- No liquidation seen on any loaded bar (spot, a prediction market): both rows read a dash rather than 0. A perpetual that served liquidations earlier but none inside the window reads 0.
- A chart with fewer bars than `flow_len`: the first tile reads "Not enough bars yet", and the share, the net delta and the liquidation sums read a dash. The capsule and each bar's own share start on the first bar.

## Customize it

- **Another window.** `flow_len` sets the window of the share, the net delta and the liquidations; the legend shows it after the name.
- **A wider middle.** Raise `lean` so the card names a side only on a clear lean.
- **More or fewer bars.** `bars: 24` on the spark sets how many bars stand around the 50 mark, up to 64.
- **The cumulative delta on the card.** `tile.spark("Cumulative delta", "cvd", { bars: 64 })` draws the running delta the file already writes.
- **Another corner.** Change `position: "bottom_center"` to any of the nine anchors, `top_left` to `bottom_right`.
- **Coins instead of dollars.** Drop `* close` from the two volume lines; the liquidation rows stay in dollars.
- **Change the look.** `look:` takes any of the twelve shipped looks, `default` to `classic` ([Looks](../presentation/hud-and-hover-cards.md#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](../settings/style-page.md#the-look-row)).

## Run it

1. In the editor's Explorer, press the **Templates** icon ("Browse starter templates") and pick **Order Flow HUD** under **HUDs**.
2. Press **Run** on a crypto chart: the chart desk card appears at the bottom centre and follows the live bar.
3. At the editor's Console prompt, type `outputs` to read the newest value of every output: the side, the buy share, this bar's share, both deltas, the cumulative delta and the two liquidation sums.

## Concepts used

- [HUD cards](../presentation/hud-and-hover-cards.md#hud-cards) and [Blocks and tiles](../presentation/hud-and-hover-cards.md#blocks-and-tiles) for `render.hud`, the five tiles, the headline, the pill's colour ladder and the capsule
- [Looks](../presentation/hud-and-hover-cards.md#looks) for `look: "chart_desk"` and what it draws for each tile kind
- [The Style page](../settings/style-page.md#the-look-row) for the Look row that switches the look without code
- [Data sources](../core-concepts/data-sources.md) for side-split `trades` and `liquidations` and the `missing` policy
- [Order flow kit](../functions/order-flow-kit.md) for delta and cumulative delta
- [Plotting](../presentation/plotting.md) for data-only outputs and string slots
