Data sources

View as MarkdownOpen the editor

The market data a wrun indicator can read on the chart: the sources the chart serves, the fields and knobs each one takes, what each value means, and the celled sources that hand the module a whole block of rows per bar.

A source is one feed (candles, funding, a book snapshot, a footprint) declared as an input, or read straight off the chart's own candle through bar; the module itself has no network, and every number it sees arrives through one of these, fetched by the chart over the bars it has loaded.

The same candles twice. Pine reads the bars and their volume. wrun also reads the order book's resting orders, the buyers and sellers at every price, the call and put walls from the options chain, and the long and short liquidations under the bars that forced them.Pinethe candlesand their volumewruncall wallput wallresting orders in the bookbuyers and sellersat every priceliquidations,longs and shorts
  1. Pine reads the candles and their volume.
  2. wrun reads the same candles and what moved them: the resting orders in the order book and the buyers and sellers at every price (celled sources), the call and put walls from the options chain, and long and short liquidations (feed sources).

Loading a source

One declaration per field you read beyond the chart's own candle. The source is a namespace word, the field is a member, and the options object carries the knobs:

text
input("close", ohlcv.close);                                        // the chart's own candles
input("funding", funding.rate_close);                               // another feed, same market
input("buy", trades.volume, { side: "BUY" });                       // a required knob
input("iv", implied_volatility.implied_volatility, { tenor: "ONE_M" });
input("btc", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" }); // another market's candles
input("daily", ohlcv.close, { interval: "1d" });                    // a coarser interval, as of its close
input("liqs", liquidations.liquidations, { side: "SELL", missing: "zero" });
input("us10y", economic.value, { publisher: "FRED", series: "DGS10" }); // a series named by its own knobs
input("bar_t", time.bar_open_sec);                                  // the bar's open, epoch seconds
input("profile", volume_profile.cells, { max_cells: 8192 });        // a celled source
input("d", candles.cells, { interval: "1d", bars: 400 });           // closed daily candles as a stream

The editor derives the sheet from these declarations as it compiles: each one becomes an inputSources entry keyed by input name ({ "source": "trades", "field": "volume", "side": "BUY" }). Every value is read in onBar() through in_<name>(), a celled block through in_<name>_cells() and in_<name>_view() (Celled sources).

The bar grid and the bar's own fields

The chart's own candle needs no declaration: bar.open(), bar.high(), bar.low(), bar.close(), bar.volume() and bar.time() read it inside onBar(), and reading a field adds its input to the sheet, on the chart's own market and interval, with no options:

MemberInput it addsSource field
bar.open()openohlcv.open
bar.high()highohlcv.high
bar.low()lowohlcv.low
bar.close()closeohlcv.close
bar.volume()volumeohlcv.volume
bar.time()bar_ttime.bar_open_sec (the bar's open, epoch seconds UTC)

Input 0 of the sheet is the bar grid: the market and interval every row is a candle of, which every other input aligns to. The rules:

  • A file with no input line runs on the chart's own candles. It gets close as input 0 whether or not it reads bar.close(), then the other fields it reads (bar.time() alone gives close, bar_t).
  • The first input line is the grid. Once a file has an input line, the first one is input 0, never a bar field. A file whose first input line names another candle field (a lone input("high", ohlcv.high);) keeps that line, so the grid stays on high instead of moving to the close; bar.high() reads through it, and the other bar fields work either way. A typed scalar feed may come first (input("fund", funding.rate_close) then bar.close() gives fund, close); a celled or time input first is refused when the sheet is checked (celled source class 'volume_profile' cannot be the primary input (index 0): the primary must be a fetched scalar series that defines the request grid; a time source cannot be the primary input (index 0); the primary defines the request grid, declare a feed or metric source first; for intrabar, wrun_intrabar_primary: input 'm1' reads intrabar cells at index 0; the primary input must be a fetched scalar series that defines the request grid the finer bars are sliced by (declare ohlcv first, intrabar as a secondary input); for candles, wrun_candles_primary: input 'd' reads candles at index 0; the primary input must be a fetched scalar series that defines the request grid the candles are delivered on (declare ohlcv first, candles as a secondary input)), so a file with a celled input keeps a scalar line in front of it (input("close", ohlcv.close);).
  • Declared inputs keep their order and indexes. The fields the file reads through bar that no declared input serves are appended after them, in the fixed order open, high, low, close, volume, bar_t.
  • A declared input serves its field. When an input is exactly that feed (a scalar input with that source and field and no option other than description), bar.close() reads through it, under its own name and index: input("price", ohlcv.close) serves bar.close(), and in_price() and bar.close() are then the same value. A symbol, exchange, interval, views, missing or any other option, a param.source pick or a derived view makes it another feed, and the field is added as an input of its own.
  • The bar's names are reserved. An input named open, high, low, close, volume or bar_t with another feed, in a file that reads that field through bar, is refused on the line of the first read: bar.close() reads the chart's own ohlcv.close through an input named 'close', and input 'close' is already declared with another feed; rename the declared input (open, high, low, close, volume and bar_t are the bar's own names). The same name is fine when the file never reads that field through bar.

Symbol and exchange are literals

Every option is a literal, read from the text without running it. An input without a pin follows the chart's market. A pinned input names symbol and exchange together, written in the chart's own ids (BTCUSDT on BINANCE_FUTURES): symbols are venue-native, so the editor refuses half a pin, which would name a market that does not exist. Leaving the pin off is how an input follows the chart. A pin may name a stock or ETF (POLYGON), a forex pair, gold or silver (FX_OTC: EUR/USD, XAU/USD, XAG/USD) beside the crypto venues (Stocks, forex and gold); a secondary input pinned to the CME Group family (CME, CBOT, NYMEX, COMEX, GLOBEX) is refused by name, because that market data cannot be read from another market's script (the first input is the package's own market, and whether a host serves a CME market is the host's call). Which inputs may pin a market or an interval is on Multi-source and Multi-timeframe; the venue ids are on Symbol format.

Feed sources

A feed source serves one number per bar. Every one needs its field, and each reads the chart's own market or its coin: only a secondary ohlcv input may name another market, an odds input names its own Polymarket market, and a few series are named by a knob of their own (a fund, a publisher and a series id, a token). The tables group them the way a chart does: the market itself, the options of its coin, and the series from beyond the venue.

The chart's market

SourceFieldsKnobsWhat the chart serves
ohlcvopen, high, low, close, volumenoneThe chart's own candles, history plus the live bar. Close prices are ohlcv + close; there is no "market" or "price" source. A secondary input may pin another market or a coarser interval.
tradesvolumeside: BUY or SELL (required); currency: USD or Coin (optional)One side's traded volume per bar, in the base asset (coins), history plus live. currency: "USD" reads it in dollars instead (each trade's notional), "Coin" in coins; two inputs differing only in currency are two lanes, so a package with a units setting declares both and picks one. Any other field is refused by name.
oiopen, high, low, closenoneOpen interest in USD, refreshed by polling. May pin a coarser interval.
liquidationsliquidationsside (optional): BUY or SELLLiquidation volume in USD, history plus live. With side the value is that side's; without it, the bar's total over both sides. Rows exist only where liquidations happened, so declare missing: "zero" when a quiet bar should read 0.
fundingrate_closenoneThe funding rate in percent, normalized to a one-hour interval, history plus live. The other funding fields are refused by name. May pin a coarser interval.
long_short_ratiototal_account, top_trader_account, top_trader_positionnoneThe market's long/short ratios, plain ratios of longs over shorts: every account, the venue's top traders counted by account, and the top traders counted by position size. Refreshed by polling. A value at or below 0 reads NaN, and the chart must be 5 minutes or coarser.
oddsopen, high, low, close (default), volumesymbol: the market's condition id (required); outcome: YES (default) or NOA Polymarket market's YES probability, 0 to 1, refreshed by polling. Details below.
timebar_open_sec, trade_date, sessionnoneThe bar's open time in epoch seconds, its exchange trade date, and its session phase (below).

Options, by the chart's coin

SourceFieldsKnobsWhat the chart serves
implied_volatilityimplied_volatilitytenor: ONE_W, ONE_M, TWO_M, THREE_M or SIX_M (required)Deribit's implied volatility for the chart's coin at that tenor, refreshed by polling. ONE_D, THREE_D and ONE_Y are refused by name: the data carries none of them.
skewskewtenor, as above (required); delta: 5, 15, 25 (the default) or 35Deribit's skew for the chart's coin at that tenor and delta, refreshed by polling.
volatility_indexopen, high, low, closenoneDeribit's volatility index (DVOL) for the chart's coin, in index points, refreshed by polling. BTC and ETH.
options_oiputs, callsvenue: deribit (the default) or binancePut and call open interest of the chart's coin on that venue, in the venue's own units (contracts on Deribit, the base coin on Binance), refreshed by polling. BTC and ETH; another coin is refused by name.
options_volumeputs, callsvenue, as abovePut and call volume of the chart's coin on that venue, in contracts, refreshed by polling. BTC and ETH; another coin is refused by name.

These are per-bar summaries. The chain itself, contract by contract, is the celled options_chain source (Options chain).

Beyond the chart's venue

SourceFieldsKnobsWhat the chart serves
etf_flowflow_usdfund: an ETF ticker (IBIT, FBTC, ETHA, ...) or all (required)Daily net spot-ETF flow in USD, on a chart of BTC, ETH or SOL; another coin, or a ticker the coin does not list, is refused by name.
etf_holdingsholdingsfund: an ETF ticker or all (required)The fund's holdings, in coins of its underlying; all sums every fund listed for the chart's coin. A coin without listed funds, or a ticker the coin does not list, is refused by name.
etf_premiumpremium_ratefund: one ETF ticker (required)The fund's premium to its net asset value, in percent, daily. all is refused by name: a rate does not sum across funds.
ethena_positionscollateralnoneEthena's collateral held against the chart's coin, in USD, summed over the venues that hold it. BTC and ETH; another coin is refused by name.
bitfinex_fundingfunding_size, credit_size, active_credit_size, margin_ratenoneBitfinex margin funding for the chart's coin: the total funding provided, the funding used in positions and the active credits, each in the funding currency, and the margin rate as an APR in percent.
treasury_balancebalanceasset (optional): an upper-case ticker, the chart's coin when absentBinance's own balance of that coin (its liability minus its customer liability), in coins, published monthly.
token_supplymarketcap, first_marketcap, marketcap_dominance_percent, circulating_supply, total_supply, max_supply, total_value_locked, fully_diluted_valuation, cg_marketcap_rank, total_volume, usd_pricetoken: the token's display name (required)A token's supply and market-cap series, daily. The token is named the way the series spells it (Bitcoin, Ethereum, Solana), never by its ticker; a name the chart does not list is refused by name, and so is a one-minute chart.
economicvaluepublisher and series (both required)One economic series in the publisher's own unit, daily or slower: { publisher: "FRED", series: "DGS10" } is the US 10-year Treasury yield in percent. A publisher the chart does not know is refused by name.

Every feed in the last two tables is refreshed by polling, and none takes a market or an interval pin: each follows the chart's coin or the series its knobs name.

A few behaviors to plan for:

  • Funding exists on perpetuals only. On a spot, FX or prediction market, a funding input that declares missing: "nan" or "zero" reads that fill on every bar; on the default carry policy the run stops with a message naming the funding rate.
  • ETF flows are daily. On an intraday chart the day's value lands on the first bar of its day and the later bars of that day follow the missing policy (carry repeats it across the day, nan and zero leave a one-bar spike); on a daily chart it joins row for row; on a coarser chart each bar sums its days. A day's flow is known after the US session, so on an intraday chart it is a same-day display, never a signal that was available at the day's open.
  • Daily levels land the same way, and never sum. etf_premium, economic and treasury_balance are daily or slower: a chart bar reads the newest value at or before its day. On an intraday chart the day's value lands on the first bar of its day and the later bars follow the missing policy; on a daily chart it joins row for row; on a coarser chart a bar reads its last day. A slower series (a monthly balance, a weekly print) carries between its prints under carry.
  • Polymarket odds. Pin the market by its condition id (0x followed by 64 hex characters) in symbol. The exchange is implied (declaring one is refused), and the chart has no market picker, so a binding is refused. outcome: "NO" reads 1 minus the close. An odds input may be the first input (the rows are still the chart's candles), and it needs a chart of one minute or coarser. The prediction-market starters in the template picker ship a placeholder market: Run refuses them until you paste a real condition id into the input's symbol.
  • Required and optional sources. A source the chart must have (volume profile, implied volatility, skew, the options chain, ETF flows, funding on the default policy, the candles of a pinned or intrabar input) that answers empty stops the run with a toast naming it. Trades, open interest, liquidations and the book may legitimately be empty for a stretch, so they read their missing fill (the book an empty block) instead. Every other feed of the options and beyond-the-venue tables, and long_short_ratio, follows the funding rule: an empty answer stops the run with a message naming the feed, unless every input that reads it declares missing: "nan" or "zero", in which case those inputs read that fill. A volume profile declared missing: "empty" is optional: on a market that serves none (a stock, an index), or a stretch with no profile, every bar reads an empty block (in_<name>_cells() is 0) and the run goes on, so the parts of the indicator that read it blank while the rest draws as usual. A bar whose profile has more buckets than the input's max_cells reads the same empty block on an optional profile (on a required one it stops the run: a block is never truncated). "empty" is the one word a celled input takes, and only a volume profile takes it.

A code-first file that exercises the per-source requirements together (a required side, a required tenor, an optional side with a missing policy) and puts the units side by side:

input("close", ohlcv.close);
input("buy_volume", trades.volume, { side: "BUY" });
input("iv_1m", implied_volatility.implied_volatility, { tenor: "ONE_M" });
input("sell_liqs", liquidations.liquidations, { side: "SELL", missing: "zero", description: "SELL-side liquidations, 0 on bars without any" });
output("stress", line, lower, { description: "SELL liquidations per dollar of buy volume, scaled by one-month IV" });

function onBar(): void {
  // trades volume is in coins; liquidations are already in USD.
  const buyUsd = in_buy_volume() * bar.close();
  const iv = in_iv_1m();
  if (isNaN(buyUsd) || buyUsd <= 0.0 || isNaN(iv)) return;
  out_stress((in_sell_liqs() / buyUsd) * iv);
}

The option summaries read the same way. This one puts five of them side by side on a BTC or ETH perpetual chart of 5 minutes or coarser: put/call ratios by open interest (Deribit, the default venue) and by volume (Binance), the volatility index, a one-week skew at 5 delta, and the top traders' long/short ratio:

input("close", ohlcv.close);
input("put_oi", options_oi.puts); // venue absent: Deribit, in contracts
input("call_oi", options_oi.calls);
input("put_volume", options_volume.puts, { venue: "binance" });
input("call_volume", options_volume.calls, { venue: "binance" });
input("dvol", volatility_index.close);
input("wing_skew", skew.skew, { tenor: "ONE_W", delta: 5 });
input("long_short", long_short_ratio.top_trader_position, { missing: "nan", description: "Top traders' long/short ratio by position, NaN on a bar without a print" });
output("put_call_oi", line, lower, { description: "Puts over calls by open interest, Deribit" });
output("put_call_volume", line, lower, { description: "Puts over calls by volume, Binance" });
output("dvol", line, lower, { description: "Deribit volatility index, index points" });
output("wing_skew", line, lower, { description: "One-week skew at 5 delta" });
output("long_short", line, lower, { description: "Top traders' long/short ratio by position" });

function onBar(): void {
  if (isNaN(bar.close())) return;
  // A ratio needs a positive denominator: NaN before the first print, never a division by zero.
  const callOi = in_call_oi();
  const callVolume = in_call_volume();
  out_put_call_oi(callOi > 0.0 ? in_put_oi() / callOi : NaN);
  out_put_call_volume(callVolume > 0.0 ? in_put_volume() / callVolume : NaN);
  out_dvol(in_dvol());
  out_wing_skew(in_wing_skew());
  out_long_short(in_long_short());
}

And the series from beyond the venue, on a BTC chart. Holdings and the treasury balance arrive in coins, so the chart's close prices them; the ETF premium, the yield and the token's market cap arrive in their own units and are plotted as they are:

input("close", ohlcv.close);
input("etf_coins", etf_holdings.holdings, { fund: "all" }); // coins held by every listed fund
input("marketcap", token_supply.marketcap, { token: "Bitcoin" }); // USD; the token by its display name
input("premium", etf_premium.premium_rate, { fund: "IBIT" }); // percent, daily
input("us10y", economic.value, { publisher: "FRED", series: "DGS10" }); // the publisher's unit: percent
input("treasury", treasury_balance.balance, { asset: "BTC" }); // coins; monthly
input("ethena", ethena_positions.collateral);
input("provided", bitfinex_funding.funding_size); // funding currency
input("lent", bitfinex_funding.credit_size); // the part in use
output("etf_share", line, lower, { unit: "%", description: "Spot-ETF holdings as a share of the market cap" });
output("premium", line, lower, { unit: "%", description: "IBIT premium to net asset value" });
output("us10y", line, lower, { unit: "%", description: "US 10-year Treasury yield" });
output("treasury_usd", line, lower, { description: "Binance's own BTC balance, in USD" });
output("ethena", line, lower, { description: "Ethena collateral in the chart's coin" });
output("utilization", line, lower, { unit: "%", description: "Bitfinex funding in use, as a share of funding provided" });

function onBar(): void {
  const close = bar.close();
  if (isNaN(close)) return;
  // Holdings and the treasury balance are coins: the close turns them into USD.
  const cap = in_marketcap();
  out_etf_share(cap > 0.0 ? (100.0 * in_etf_coins() * close) / cap : NaN);
  out_premium(in_premium());
  out_us10y(in_us10y());
  out_treasury_usd(in_treasury() * close);
  out_ethena(in_ethena());
  const provided = in_provided();
  out_utilization(provided > 0.0 ? (100.0 * in_lent()) / provided : NaN);
}

Alignment and the missing policy

The rows are the chart's own candles: the first input follows the chart's market and interval, and every other input is aligned onto those rows. Sources at the chart's interval join by bar open, row for row; a coarser interval pin contributes to a row only as of that row's close, so a forming 4h candle never leaks into the 1h rows under it (Multi-timeframe). A scalar source with no observation on a bar delivers, by policy, the latest value carried forward (missing: "carry", the default; NaN before the first observation), NaN ("nan"), or 0 ("zero"). Celled blocks never carry: a bar with no observation gets an empty block.

The time source

time.bar_open_sec carries the bar's open timestamp (epoch seconds, UTC), taken from the chart's own candle, so a module can do session and calendar math deterministically (Time and sessions); bar.time() reads it with no declaration. Two more fields carry the bar's market facts, so an indicator never has to know the venue's calendar:

  • time.trade_date: the bar's exchange trade date, as epoch seconds at 00:00 UTC of that date. On CME futures the trade date starts at the 17:00 Chicago open, so an evening bar belongs to the next date; on US stocks it is the New York calendar date; on crypto, forex, HIP-3 and Polymarket markets it is the UTC date.
  • time.session: 1 regular, 2 pre-market, 3 after-hours, 0 closed. US stocks follow the New York pre-market, regular (09:30 to 16:00) and after-hours sessions; CME futures read 1 inside the contract's regular trading hours (all session long for a contract without them), 2 before and 3 after them within the same trade date, and 0 while the market is closed; a market without sessions reads 1 on every bar.

A bar of one day or longer (a 1D, 3D, 1W or 1M chart) is a whole trading day or more, so it takes the date of its own stamp and session 1 on every market: the chart stamps such bars at 00:00 UTC of their trade date. Both fields are facts of the market, the bar and its interval alone: switching the chart between regular and extended hours never changes them. Every field is filled by the chart (there is no feed behind it), every other knob is refused, and a time input is never the first input.

Celled sources

A feed source serves one number per bar. A celled source serves a whole BLOCK of rows per bar, which is what footprint-style indicators need: a footprint is the same candle sliced by price, one [low, high, buy, sell] row per price bucket, so "did buyers or sellers do the volume, and at which prices" is answerable inside one bar instead of only as a per-bar total. Celled inputs are declared as input(name, <source>.cells, { max_cells }), which moves the derived sheet to at least the second contract (abi_version: "wrun-2"); the module-side accessors are in TA library:

SourceOne tuple perWhat the chart servesKnobs
volume_profileprice bucket: [low, high, buy, sell]the chart's own volume profile for each bar, with real bucket bounds, in ascending price order; history plus livemax_cells; ticks_per_bar (1 to 500 of the market's buckets merged into one, or "@<param>" naming a param.int whose value it takes); currency ("USD" for dollar volumes, "Coin" by default); missing: "empty" (the profile optional: below)
booklevel: [price, size, side], side +1 bid, -1 askthe chart's own order book for each bar, up to 500 levels a side: bids first, then asks from the highest price down; history plus livemax_cells (1000 holds both sides); block_size (the declaration requires one, but the chart reads its own order-book feed and does not use it or max_depth)
intrabarclosed finer bar: [offset_ms, open, high, low, close, volume], offset_ms the finer open minus the bar's openthe finer candles inside each bar, of the chart's market or a pinned oneinterval (required), max_cells, symbol + exchange
candlesclosed candle of the stream's interval: [offset_ms, open, high, low, close, volume], offset_ms the candle's open minus the bar's opena stream of closed candles: the backlog on the first bar, then each candle on the bar it closes with, of the chart's market or a pinned oneinterval (required, any word), bars (1 to 5000), max_cells (the editor fills it when left out), symbol + exchange
trade_volume_by_sizeUSD trade-size bucket: [bucket, buy_usd, sell_usd, buy_count, sell_count]the chart market's traded volume split by the size of each trade (each fill bucketed by its own dollar value), one tuple per bucket that traded in the bar, in ascending bucket ordermax_cells (7 holds a full bar)
options_chainlisted contract: [strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega]the newest option chain, on the newest bar onlyvenue, expiries, max_cells

Every celled source reads the chart's own market (a market pin is refused) except intrabar and candles, which may name another market with symbol and exchange together. A block over max_cells refuses the run instead of being truncated, except on a volume profile declared missing: "empty", where that bar reads an empty block. Order-book depth figures (the summed bid size, the largest ask, ...) are loops over the book block (Order flow); the intrabar and candles rules are on Multi-timeframe.

Reading a block

A celled input gets three readers and two constants, and the build owns one buffer per celled input, allocated at module start (max_cells x the tuple width, so 8192 volume profile tuples are 262,144 bytes):

  • in_<name>_cells(): i32: the f64 values in this bar's block; -1 when the bar carries none, 0 for an empty block.
  • in_<name>_view(): StaticArray<f64>: the build's own buffer, no copy. Only the first in_<name>_cells() values belong to this bar: on an absent or empty bar the buffer still holds the previous block, so always bound the loop by the count.
  • in_<name>_read(ptr: i32): i32: copies the block to memory at ptr and returns the bytes written (count x 8), -1 absent, 0 empty. It is for code that owns a buffer of its own, which then holds the block twice; prefer the view.
  • in_<name>_max_cells and in_<name>_capacity (max_cells x tuple width): constants.

Read the block where you write, bounded by the count:

wrun
function onBar(): void {
  const n = in_profile_cells(); if (n <= 0) return;
  const cells = in_profile_view();
  let buy = 0.0; let sell = 0.0;
  for (let i = 0; i + 3 < n; i += 4) { buy += cells[i + 2]; sell += cells[i + 3]; }
  if (buy + sell > 0.0) out_buy_share((100.0 * buy) / (buy + sell));
}

Volume by trade size

trade_volume_by_size answers "who traded this bar": the same volume split by how large each trade was. A trade is one fill, bucketed by its own dollar value, so an order that fills in pieces lands in smaller buckets: read the large buckets as a floor for what large traders did. bucket runs from 1 to 7 by the USD value of one trade:

bucketTrade size, USD
1under 1K
21K to 10K
310K to 100K
4100K to 500K
5500K to 1M
61M to 10M
710M and up

buy_usd and sell_usd are USD notional, buy_count and sell_count are trade counts. A bucket that did not trade has no tuple, and a bar with no trades is an empty block. The share of a bar's volume that arrived in trades of 100K and up:

input("close", ohlcv.close);
input("sizes", trade_volume_by_size.cells, { max_cells: 7 }); // seven buckets hold a full bar
output("large_share", line, lower, { unit: "%", description: "Share of the bar's USD volume in trades of 100K and up" });

function onBar(): void {
  const n = in_sizes_cells();
  if (n <= 0) return;
  const cells = in_sizes_view();
  let total = 0.0;
  let large = 0.0;
  // One [bucket, buy_usd, sell_usd, buy_count, sell_count] row per bucket that traded.
  for (let i = 0; i + 4 < n; i += 5) {
    const usd = cells[i + 1] + cells[i + 2];
    total += usd;
    if (cells[i] >= 4.0) large += usd; // bucket 4 starts at 100K
  }
  out_large_share(total > 0.0 ? (100.0 * large) / total : NaN);
}

What the chart does not serve

  • Live individual trades (the tape source): the declaration compiles, and Run refuses it by name. Per-bar buy and sell volume is trades; the buy/sell split by price is volume_profile; the split by trade size is trade_volume_by_size.
  • Another indicator's outputs, or a saved series: the editor has no declaration for either; an indicator reads market data only.
  • CME open interest: no source serves it. Open interest on the venues the chart serves is oi.

There is no cvd source: cumulative volume delta is buy minus sell trades volume, accumulated in the module.

Options chain

options_chain is the celled source behind a gamma map: the chart market's option chain, one tuple per listed contract, so exposure by strike is a loop inside the indicator instead of a server aggregate. Declare the chart's candles first, then the chain input:

wrun
input("close", ohlcv.close);
input("chain", options_chain.cells, { max_cells: 2000 });

Each contract occupies ten f64 values: [strike, expiry_ms, side, oi, gamma, delta, mark_iv, underlying, multiplier, vega]. expiry_ms is the expiry in epoch milliseconds; side is +1 for a call and -1 for a put; oi is the open interest in the venue's amount units; gamma and delta are the venue's served greeks; mark_iv is the mark implied volatility; underlying is the chain's underlying price; multiplier is the USD value of one price unit per OI unit (1 where the OI already carries it, the CME point value on a CME chain); vega is the venue's vega, the price change per one IV point (0 where the venue serves none). Gamma exposure per contract is gamma * oi * multiplier * underlying * underlying * 0.01, summed by strike with the sign of side; vega exposure is vega * oi * multiplier, summed the same way.

The chart serves the LATEST chain, not a history of chains, so the block is present on the newest bar only: every older bar is an empty block (in_chain_cells() reads 0), and the indicator reads the chain when bar.isLast() is true. The chart refreshes it from the same 30-second snapshot its options panes use. venue: "auto" (the default) reads the chart's own market when it is an options venue (a CME futures chart reads the CME chain of its root), else the coin's Deribit chain. Every other word pins one venue's chain: "deribit", "cme", "binance", "okx", "bybit", "bullish" or "derive". A pinned venue is refused by name on a chart it cannot serve: a coin that venue does not list, or a venue the chart does not offer you. expiries is "all" (the default) or "nearest:N", the N nearest live expiries. max_cells counts contracts: a chain over the cap refuses the run by name, never truncated, so size it for the venue (a full BTC chain is about 1,500 contracts; 2000 leaves headroom). A market pin, an unknown venue word and an expiries selector outside that grammar are refused by name.

The worked footprint example

The vp-buy-share-codefirst template is the footprint loop end to end: a celled volume_profile input, the buy share of each bar's profile as a numeric output, and a text renderer fed from a string slot on the newest bar. The whole file (code-first, so the sheet is derived):

input("close", ohlcv.close); // the first input is the bar grid: the chart's own candles
input("profile", volume_profile.cells, { max_cells: 8192 });
output("buy_share", line, lower, { label: "Buy share", format: "%", decimals: 1 });
string("summary", { max_bytes: 64 });
render.text("flow", { y: "buy_share", text: "summary" });

function onBar(): void {
  const n = in_profile_cells(); if (n <= 0) return; // no profile on this bar: nothing is drawn
  const cells = in_profile_view(); // this bar's cells, four numbers each: low, high, buy, sell
  let buy = 0.0; let sell = 0.0;
  for (let i = 0; i + 3 < n; i += 4) { buy += cells[i + 2]; sell += cells[i + 3]; }
  if (buy + sell <= 0.0) return;
  const share = (100.0 * buy) / (buy + sell);
  out_buy_share(share); if (!bar.isLast()) return; // the text rides the newest bar only, so the pane stays readable
  sb_clear(); sb_text("buy "); sb_f64(share, 1); sb_text("%"); str_summary_sb();
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

To run it, open the editor, click the Templates icon ("Browse starter templates") in the Explorer, pick VP Buy Share under Order flow, and press Run. A bar whose profile splits 60/40 to the buy side computes buy_share = 60; only the newest bar also gets its text, such as buy 60.0%, so the pane stays readable. A book-driven variant loops over book.cells the same way.

How many sources you can open

On the chart a wrun indicator you add counts against your plan's per-chart indicator limit by the number of distinct sources it reads, where a source is the source name plus its pinned market: close, high and volume of the chart's own candles count once, and a pinned ohlcv input on another market counts again. Inside the indicator a derived value (an average of an input) costs nothing, since it is your arithmetic, and two inputs with the same source, market and knobs are two declarations over one feed. The practical cost of each extra source is fetch time over the loaded window (a coarse interval pin widens its fetch by two of its candles, and bars by as many as it asks). The one hard ceiling inside the module is on the other side: it may write at most 256 output slots.

Availability

The editor accepts any declared source and field the kit knows; whether the chart can serve it on the market in front of you is answered when you press Run, by name, never with silent empty data. A source the chart cannot serve there shows a Could not load chip on the indicator's legend with the reason and a Retry button, and the editor's Console shows the same sentence. On a one-second chart, sources the chart does not serve at one second are declined. Where each part of an indicator runs, and what an alert can evaluate, is on Execution model.