FlowscopeDocs

Market stats table

Summarise price, volume, flow, open interest, funding and liquidations for the chart's market in a live table window.

When you trade several coins, you want the essentials of each one in a single glance: how far price has moved today, whether buyers or sellers are in control, whether open interest is building, where funding stands and who has been liquidated. This script collects those numbers and shows them in a table window over the chart, with colour-coded values.

The table has three columns: the measure, its value, and a short reading such as “buyers” or “building”. Every number is formatted for reading, not for precision.

market-stats-table.fs
script "Market stats"

input (
  lookback = input.int(24, title: "Window (bars)", min: 2, max: 500)
  hotFunding = input.float(30.0, title: "Funding hot above (% p.a.)", min: 1.0, max: 200.0)
  perDay = input.int(3, title: "Funding periods per day", min: 1, max: 24)
)

data (
  chart = subscribe(data.ohlcv)
  daily = subscribe(data.ohlcv, timeframe: 1D)
  oi = subscribe(data.oi)
  stats = subscribe(data.stat)
  book = subscribe(data.book)
)

window dash = window(title: "Market stats") {
  table: table.new(columns: [table.col(title: "Measure", width: 1.4), table.col(title: "Value", align: align.right), table.col(title: "Reading")]),
}

state (
  buys = rolling<float>(lookback)
  sells = rolling<float>(lookback)
  longLiqs = rolling<float>(lookback)
  shortLiqs = rolling<float>(lookback)
)

// "+1.23%" for a value, "n/a" for none.
fn signed(x: float?, suffix: string) -> string {
  if x == null {
    return "n/a"
  }
  return str.format("{0}{1:.2}{2}", x >= 0.0 ? "+" : "", x, suffix)
}

fn tone(x: float?) -> color {
  return (x ?? 0.0) >= 0.0 ? color.green : color.red
}

on chart.close {
  buys.push(chart.buyVolume)
  sells.push(chart.sellVolume)
  longLiqs.push(stats.sellLiq)
  shortLiqs.push(stats.buyLiq)
}

on chart.update {
  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 b = buys.sum()
    let s = sells.sum()
    let buyShare = b + s > 0.0 ? b / (b + s) * 100.0 : 50.0

    let oiNow = oi.close
    let oiThen = oi.close[lookback]
    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 hot = math.abs(annual ?? 0.0) >= hotFunding

    let ll = longLiqs.sum()
    let sl = shortLiqs.sum()
    let pressure = book.imbalance(0.5)

    let t = dash.table
    t.row("price", str.format("{0} on {1}", context.base, context.exchange), str.format("{0}", c ?? 0.0), table.cell(signed(dayPct, "% today"), fg: tone(dayPct)))
    t.row("flow", str.format("Buy share, {0} bars", lookback), str.format("{0:.1}%", buyShare), table.cell(buyShare >= 50.0 ? "buyers" : "sellers", fg: buyShare >= 50.0 ? color.green : color.red))
    t.row("oi", str.format("OI change, {0} bars", lookback), table.cell(signed(oiPct, "%"), fg: tone(oiPct)), (oiPct ?? 0.0) >= 0.0 ? "building" : "unwinding")
    t.row("funding", "Funding", table.cell(signed(annual, "% p.a."), fg: tone(annual)), table.cell(hot ? "crowded" : "normal", bg: hot ? color.withAlpha(color.amber, 60) : color.transparent))
    t.row("liqs", str.format("Liquidations, {0} bars", lookback), str.format("L {0:,.0} / S {1:,.0}", ll, sl), table.cell(ll >= sl ? "longs flushed" : "shorts squeezed", fg: ll >= sl ? color.red : color.green))
    t.row("book", "Book within 0.5%", table.cell(signed(pressure == null ? null : (pressure ?? 0.0) * 100.0, "%"), fg: tone(pressure)), (pressure ?? 0.0) >= 0.0 ? "bids heavier" : "asks heavier")
  }
}

How it works

A window with a table

A window declares a floating window with named panels. Here it holds a single table made with table.new and three columns from table.col. The panel is addressed as dash.table, and the handler keeps a short local alias t.

row(key, cells...) writes a whole row, one cell per column. A row with the same key is replaced, so each update rewrites the six rows in place. A cell is a plain value or table.cell(value, fg:, bg:) for colours.

Collect on close, render at the live edge

Rolling sums of buy and sell volume and of liquidations are collected in on chart.close, one value per confirmed bar, in rolling<float> collections sized by the lookback input. The table itself is only useful for the present, so the update handler renders it only when chart.isLast is true. During history nothing is rendered, and at the live edge the table refreshes with every update.

Reading other subscriptions

All other sources are read from the chart’s update handler. Each one gives its last confirmed period, except where the script explicitly asks for the developing one: daily.forming.open is today’s open, while daily.open would be yesterday’s, and stats.forming.fundingRate the current rate. oi.close[lookback] reaches back lookback periods on the OI subscription. See data and update events for the full rules.

book.imbalance(0.5) compares the resting value on both sides within 0.5 % of the mid price, from the book the chart has recorded. Before the recording reaches back far enough it is null, and the row reads “n/a”.

Formatting helpers

Two small functions keep the cells consistent. signed adds a plus sign to positive numbers and returns "n/a" for missing values, so the table never shows a raw null. tone picks a colour by sign. {0:,.0} in a format string writes a number with thousands separators and no decimals.

Adapting it

  • Profile rows. Add profile = subscribe(data.volume) and show the point of control of the last lookback bars from profile.profile(lookback).poc().
  • Large-print share. Sum the notional of prints above a floor from a trades subscription, as in large prints, and show its share of the volume.
  • Heat colours. Replace tone with color.gradient so the colour intensity follows the size of the move.