The market feeds an Indicator can read, the fields each one serves, the
knobs each one takes or requires, and the celled source classes that serve
a whole block of rows per bar. A source is one feed (candles, funding, a
book snapshot, a footprint) for one market on one venue, declared as an
input; the module itself has no network, and every number it sees
arrives through one of these. If you know kScript (legacy), the mapping
table below names the Indicator source for each of its twenty source types.
Loading a source
One declaration per field you read. The source is a namespace from
./sdk/declare, the field is a member, and the options object carries the
knobs:
input("close", ohlcv.close); // the selector's market
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" }); // a pinned market
input("daily", ohlcv.close, { interval: "1d" }); // a coarser interval, as of its close
input("liqs", liquidations.liquidations, { side: "SELL", missing: "zero" });
input("bar_t", time.bar_open_sec); // the bar's open, epoch seconds
input("profile", volume_profile.cells, { max_cells: 512 }); // a celled classIn the sheet the same declarations are inputSources entries keyed by
input name ({ "source": "trades", "field": "volume", "side": "BUY" });
the declaration form derives them. Every value is read in state()
through in_<name>(), celled blocks through in_<name>_cells() and
in_<name>_read(ptr).
Symbol and exchange are literals
kScript resolved a source's symbol and exchange before the bar loop
and refused a script variable there. An Indicator is stricter in the same
direction: every option is a string literal, read from the text without
running it. An unpinned input follows the selector (the chart's market, or
the --symbol / --exchange you evaluate with on your machine); a pinned
input names symbol and exchange together, since half a pin names a
market that does not exist. There is no currentSymbol: leaving the pin
off is how an input follows the chart.
kScript sources, mapped
| kScript (legacy) source | Indicator |
|---|---|
ohlcv (open, high, low, close, volume) | ohlcv.open .. ohlcv.volume |
open_interest (open, high, low, close) | oi.open .. oi.close |
buy_sell_volume (buy, sell) | trades.volume with side: "BUY" and a second input with side: "SELL" |
funding_rate (value) | funding.rate_close (plus rate_open, rate_high, rate_low, and the predicted_* fields) |
liquidations (buy, sell) | liquidations.liquidations with side: "BUY" / "SELL", or no side for the total |
orderbook (bids, asks, array-celled) | book.cells with block_size (and max_depth) |
volume_profile (array-celled) | volume_profile.cells |
deribit_implied_volatility (one_week, one_month, three_months) | implied_volatility.implied_volatility with tenor: "ONE_W", "ONE_M", "THREE_M" |
skew (one_week, onemonth, three_months) | skew.skew with the same tenors |
time (a member of every source) | time.bar_open_sec, its own source |
trade_volume_by_size (tape by order size) | Not in Indicators yet. The trade_volume_by_size class is declared for cross-host parity and refused by name at fetch (wrun_cells_unavailable); the buy/sell split per price lives in volume_profile. |
cme_oi, deribit_volatility_index, etf_flow, etf_holding, etf_premium_rate, options_volume, options_open_interest, ethena_positions, long_short_ratio, binance_treasury_balance | Not in Indicators yet. No feed source serves them; a kScript that leans on one stays on kScript. |
| (no kScript counterpart) | token_supply (market cap, dominance, supply, TVL, by token), odds (Polymarket probabilities, pinned or bound) |
Feed sources
All feed sources require field. Pins (symbol + exchange together,
interval) work on every feed source per functions/script-definition.md;
the extra knobs below are per-source.
| Source | Fields | Required knobs | Notes |
|---|---|---|---|
ohlcv | open, high, low, close, volume | none | Candles on the selector's (or pinned) market. Close prices are ohlcv + close; there is no "market"/"price" source. |
trades | open, high, low, close, volume | side (BUY or SELL) | Side-split per-period trade aggregates. |
funding | rate_open, rate_high, rate_low, rate_close, predicted_open, predicted_high, predicted_low, predicted_close | none | Perp funding rate, realized and predicted. Units differ by lane (the chart serves rate_close as a percent), so keep thresholds as params. |
oi | open, high, low, close | none | Open interest. |
liquidations | liquidations | none (side optional: BUY or SELL) | Liquidation volume per period. The feed serves one series per side; with side pinned the scalar is that side's volume, with side absent it is the per-bar TOTAL over both sides (a bar present on one side only contributes that side). Naturally sparse: rows exist only where liquidations happened, so as a primary it usually declares missing: "zero". |
implied_volatility | implied_volatility | tenor | Options IV at a tenor (ONE_D, THREE_D, ONE_W, ONE_M, TWO_M, THREE_M, SIX_M, ONE_Y). |
skew | skew | tenor | Options skew at the same tenor vocabulary. |
token_supply | marketcap, first_marketcap, marketcap_dominance_percent, circulating_supply, total_supply, max_supply, total_value_locked, fully_diluted_valuation, cg_marketcap_rank, total_volume, usd_price | token | Supply and cap series keyed by token name. |
odds | open, high, low, close (default), volume | none | Polymarket probabilities; own pin rules (functions/script-definition.md): conditionId as symbol or binding: "required", outcome YES/NO, exchange implicit. |
A sheet that exercises the per-source requirements together (the same declarations, in the wire form the registry and every runtime read):
{
"id": "context-pack",
"abi_version": "wrun-1",
"warmup_bars": 1,
"params": [],
"inputSources": {
"close": { "source": "ohlcv", "field": "close" },
"buy_volume": { "source": "trades", "field": "volume", "side": "BUY" },
"iv_1m": { "source": "implied_volatility", "field": "implied_volatility", "tenor": "ONE_M" },
"btc_mcap": { "source": "token_supply", "field": "marketcap", "token": "BTC" },
"bar_time": { "source": "time", "field": "bar_open_sec" },
"sell_liqs": { "source": "liquidations", "field": "liquidations", "side": "SELL", "missing": "zero" }
},
"inputs": [
{ "index": 0, "name": "close" },
{ "index": 1, "name": "buy_volume" },
{ "index": 2, "name": "iv_1m" },
{ "index": 3, "name": "btc_mcap" },
{ "index": 4, "name": "bar_time" },
{ "index": 5, "name": "sell_liqs", "description": "SELL-side liquidations, 0 on bars without any" }
],
"outputs": [{ "index": 0, "name": "stress", "plot": "line", "panel": "lower" }]
}Alignment and the missing policy
The first input is the primary: it defines the grid (market and interval)
every other input aligns to. Equal-or-finer sources align by bar open, row
for row; a coarser source (an interval pin) contributes to a primary row
only as of its candle's close, so a forming 4h candle never leaks into the
1h rows under it. A scalar source with no observation on a primary bar
delivers, by policy, the latest eligible value carried forward (missing: "carry", the default), NaN ("nan"), or 0 ("zero"). On the primary
input "nan" and "zero" change the grid itself: the package computes on
a dense grid of bar opens at the primary's interval, which is how a sparse
series such as liquidations becomes a dense primary. Every rule is in
functions/script-definition.md.
The time source
time.bar_open_sec ({ "source": "time", "field": "bar_open_sec" } in a
sheet) carries the primary bar's open timestamp (epoch seconds, UTC),
host-supplied off the primary grid, so a module can do session and
calendar math deterministically (time-and-sessions.md). Exactly one
field; every other knob is refused (there is no feed behind it); never the
primary input.
Metric composition
{ "source": "metric", "metric": "wrun/@scope/name/output" } feeds another
installed package's output in as an input, with optional params for the
inner package. Rules in functions/script-definition.md; note composition
inputs are metadata-first only in code-first workspaces (the declaration
grammar refuses them by name), and a metric source pinned coarser than
the primary grid is refused by name.
Permissions
read:openmarket covers every feed and celled source and is required
whenever any of them follows the selector; metric and time sources need
no permission. Venue-scoped reads (read:binance, read:bybit,
read:hyperliquid, read:polymarket) are a narrowing option only for
packages whose EVERY feed and celled source pins one of those venue
families.
Celled source classes
A scalar source serves one number per bar. A celled source class 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, <class>.cells, { max_cells })
(cellType: "array" + max_cells in a sheet), which switches the derived
sheet to the second runtime contract (abi_version: "wrun-2"); the sheet
rules, and the exact-join alignment that never forward-fills a block, are
in functions/script-definition.md, and the module-side accessors are in
functions/ta-library.md:
| Class | Cell tuple | Serving | Knobs |
|---|---|---|---|
volume_profile | [low, high, buy, sell] per price bucket | FETCHES live (VOLUME_PROFILE_AGG; the API's flat [price, buy, sell] triplets arrive with low = high = price) | optional symbol + exchange pin pair |
book | [price, size, side] per level, side +1 bid / -1 ask, bids descending then asks ascending | FETCHES live (BLOCK_BOOK_SNAPSHOT_AGG) | block_size REQUIRED (the venue price-bucket width; om block-sizes lists them), optional max_depth (top N levels per side), optional pin pair |
tape | [offsetMs, price, size, side] per print, side +1 BUY / -1 SELL | Live daemon buffer (TRADE websocket) | min_size REQUIRED; history defaults to "0", other values refused; optional pin pair |
trade_volume_by_size | declared for cross-host parity | REFUSED by name (wrun_cells_unavailable): no point lane on this data plane yet | none |
The raw tape class is live only. The REST API has no raw-trade data type
or minimum-size filter, so tape cannot backfill historical prints.
TRADE_AGG still serves per-period scalars through trades above.
kScript's orderbook depth functions (sumBids, maxAskAmount, ...) are
loops over the book block (functions/orderbook-functions.md).
Two named refusals bound the surface: an unserved class (or cellType on a
scalar source) refuses at fetch with wrun_cells_unavailable before
anything is fetched, and backtests / screens refuse whole celled packages
with wrun_celled_metric_unsupported (their replay and fan-out paths carry
no cell blocks yet). Alerts, om metric get / om metric series, and
chart previews are the supported consumers.
Live tape
tape is a celled source for live individual trades on the daemon.
Declare a scalar primary input first, then a tape input:
{
"inputSources": {
"close": { "source": "ohlcv", "field": "close" },
"prints": { "source": "tape", "min_size": 1, "history": "0" }
},
"inputs": [
{ "index": 0, "name": "close" },
{ "index": 1, "name": "prints", "cellType": "array", "max_cells": 4096 }
]
}Use abi_version: "wrun-2" or later. min_size is required, positive,
and measured in the raw trade's amount units. history defaults to "0".
Every other history value refuses with wrun_tape_history_unavailable:
history needs the raw trade lane; live prints only.
Each print occupies four f64 values: [offsetMs, price, size, side].
offsetMs starts at the primary bar's open; side is +1 BUY or -1 SELL.
Generated accessors reserve max_cells * 4 f64 values. A bar exceeding
max_cells refuses the evaluation; it is never clipped.
The daemon buffer retains at most two hours or 200,000 prints, deduplicates
trade ids, and keeps sizes at or above the smallest active threshold.
Every input applies its own threshold when reading. Older bars and a buffer
that has not started return empty blocks. Historical REST backfill is
unavailable. trades still means scalar side-split aggregates.
The daemon opens the market's tape the first time a package with a tape
input is evaluated and keeps it streaming while evaluations keep touching
it (two primary bars plus fifteen idle minutes close it). Bars longer than
the two-hour retention see only the retained prints. A lane with no live stream, such as a
backtest replay or a one-shot read outside the daemon, refuses with
wrun_tape_unavailable. The celled contract (docs/WRUN2_ABI.md, section
"Live tape", in the openmarket repository) lists the lifecycle; the caps
are on Limits.
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 per-bar text renderer fed from a string slot. The
whole user file (code-first, so the sheet is derived):
import { input, line, lower, ohlcv, output, render, string, volume_profile } from "./sdk/declare";
import { in_profile_capacity, in_profile_cells, in_profile_read } from "./gen/inputs";
import { emitRow, out_buy_share } from "./gen/outputs";
import { sb_clear, sb_f64, sb_text, str_summary_sb } from "./gen/strings";
input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 512 });
output("buy_share", line, lower);
string("summary", { max_bytes: 64 });
render.text("flow", { y: "buy_share", text: "summary" });
const cells = new StaticArray<f64>(in_profile_capacity); let share: f64 = NaN;
export function init(): void {}
export function state(): i32 {
const n = in_profile_cells(); if (n <= 0 || in_profile_read(i32(changetype<usize>(cells))) < 0) return 0;
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]; }
share = buy + sell > 0.0 ? (100.0 * buy) / (buy + sell) : NaN; return isNaN(share) ? 0 : 1;
}
export function finalize(): void { out_buy_share(share); sb_clear(); sb_text("buy "); sb_f64(share, 1); sb_text("%"); str_summary_sb(); emitRow(); }
export function reset(): void { share = NaN; }Scaffold it, install it, and read a live value on real volume-profile rows:
om wrun create @you/vp-flow ./vp-flow --template vp-buy-share-codefirst
om wrun install ./vp-flow --replace
om metric get --metric wrun/@you/vp-flow/buy_share --symbol BTCUSDT --exchange BINANCE_FUTURES
om metric series --metric wrun/@you/vp-flow/buy_share --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 48A bar whose profile splits 60/40 to the buy side computes buy_share = 60
and renders the text buy 60.0% at that bar. For a book-driven variant,
pick the venue's bucket width first:
om block-sizes --exchange BINANCE_FUTURESHow many sources you can open
kScript budgeted ten weighted source slots per script. An Indicator has no slot budget: every input is one series requirement, and the practical cost is fetch time on the widest window (a coarse pin widens the fetch by two source intervals). Two inputs with the same source, market, and knobs are two declarations over one feed; a derived value (an average of an input) costs nothing, since it is your arithmetic. The one hard ceiling is on the other side: a module may write at most 256 output slots.
Availability
The declaration grammar accepts any registered source and field; whether a
venue serves it for a market is a platform question, answered by name
before the module runs (wrun_cells_unavailable for an unserved celled
class, a chart message naming the source for a feed the browser lane does
not serve), never with silent empty data. execution-model.md lists what
each host serves.