Combine several markets, venues, and data types in one Indicator: other
symbols and other venues through symbol + exchange pins, other feeds
through their sources, side-split flow through side, sparse feeds through
missing, and aggregation through ordinary arithmetic across inputs.
kScript (legacy) did this with source(...) calls under a ten-slot budget
plus request(), ltf(), and requestBars(); an Indicator does it with
input declarations, and the last two have no counterpart yet.
Introduction
An Indicator is not limited to its chart's data. On your machine it can declare any number of inputs: other symbols, other exchanges, other data types (open interest, funding, side-split trades, a book snapshot), and combine them with ordinary math. "Aggregated" indicators (one metric summed across venues) are not a special feature; they fall out of this plus a few lines of arithmetic.
input("close", ohlcv.close); // the primary: the selector's market
input("btc", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES" }); // a pinned reference market
// in finalize(): out_ratio(close / btc);How multiple sources coexist
Three host rules make cross-source math safe by default:
- One grid, defined by the primary. The first input's market and interval are the grid; every other input is aligned onto it, row for row. Every input is readable on every bar.
- Values align by policy. An equal-or-finer source aligns by bar
open. A coarser source contributes only as of its candle's close
(
multi-timeframe.md). A scalar source with no observation on a bar delivers the latest eligible value carried forward (missing: "carry", the default),NaN("nan"), or0("zero"), and you choose per input. Funding (an 8h cadence) against 1h candles just works. - Events never fabricate. Every comparison with
NaNis false, andCross.update()answers0when either side isNaN, so a venue that has not reported cannot invent a signal.
Multi-symbol
Any input can name a market other than the chart's. The two halves pin
together (a lone symbol or a lone exchange is refused at the sheet),
and everything downstream is source-agnostic: classes, folds, gates, shapes.
The ETH/BTC ratio and the relative-strength reading between them:
import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_btc, in_close } from "./gen/inputs";
import { emitRow, out_lead_rsi, out_ratio, out_ratio_sma } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Rsi, Sma } from "./sdk/ta";
param("period", 14, { min: 2, max: 200, description: "Lookback for the ratio average and the leader's RSI" });
// The primary follows the selector (ETHUSDT on an ETH chart); the reference is pinned.
input("close", ohlcv.close);
input("btc", ohlcv.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES", description: "BTC close, the reference market" });
output("ratio", line, lower, { color: "#2563eb", width: 2, description: "Selector close divided by the BTC close" });
output("ratio_sma", line, lower, { color: "#94a3b8", description: "Average of the ratio" });
output("lead_rsi", line, lower, { color: "#f59e0b", description: "RSI of the reference market: the leader's momentum" });
let sma = new Sma(14);
let rsi = new Rsi(14);
let ratio: f64 = NaN;
let ratioSma: f64 = NaN;
let leadRsi: f64 = NaN;
export function init(): void {
sma = new Sma(i32(p_period()));
rsi = new Rsi(i32(p_period()));
}
export function state(): i32 {
const close = in_close();
const btc = in_btc();
ratio = btc > 0.0 ? close / btc : NaN;
ratioSma = isNaN(ratio) ? NaN : sma.update(ratio);
leadRsi = rsi.update(btc);
return isNaN(ratioSma) || isNaN(leadRsi) ? 0 : 1;
}
export function finalize(): void {
out_ratio(ratio);
out_ratio_sma(ratioSma);
out_lead_rsi(leadRsi);
emitRow();
}
export function reset(): void {
sma.reset();
rsi.reset();
ratio = NaN;
ratioSma = NaN;
leadRsi = NaN;
}For the common cases kScript spelled request("ETHUSDT") and
request("ETHUSDT", "4h"), the declaration adds interval to the pin:
input("eth_4h", ohlcv.close, { symbol: "ETHUSDT", exchange: "BINANCE_FUTURES", interval: "4h" }), read as of the 4h close
(multi-timeframe.md). Cross-symbol arithmetic is only dimensionally sane
under the shared default USD quote.
Multi-venue aggregation
The flagship pattern: one metric, summed across exchanges. The cookbook's
cumulative volume delta sums side-split trades volume over four venues
(missing: "nan" so a venue that did not report reads NaN, counts as not
live, and contributes zero). Open interest, venue-weighted, is the same
shape with oi.close and a pair of weights:
import { input, line, lower, ohlcv, oi, output, param } from "./sdk/declare";
import { in_oi_binance, in_oi_bybit } from "./gen/inputs";
import { emitRow, out_agg_oi, out_binance_share, out_oi_ema } from "./gen/outputs";
import { p_w_binance, p_w_bybit } from "./gen/params";
import { Ema } from "./sdk/ta";
param("w_binance", 0.6, { min: 0, max: 1, description: "Weight of Binance open interest" });
param("w_bybit", 0.4, { min: 0, max: 1, description: "Weight of Bybit open interest" });
// The chart's own candles are the grid; every venue aligns onto it.
input("close", ohlcv.close);
input("oi_binance", oi.close, { symbol: "BTCUSDT", exchange: "BINANCE_FUTURES", missing: "nan", description: "Binance BTC open interest" });
input("oi_bybit", oi.close, { symbol: "BTCUSDT", exchange: "BYBIT", missing: "nan", description: "Bybit BTC open interest" });
output("agg_oi", line, lower, { color: "#2563eb", width: 2, description: "Venue-weighted open interest" });
output("oi_ema", line, lower, { color: "#94a3b8", description: "Smoothed aggregated open interest" });
output("binance_share", line, lower, { color: "#f59e0b", unit: "%", description: "Binance share of the reporting venues' open interest" });
const weights = new Map<string, f64>();
let ema = new Ema(21);
let agg: f64 = NaN;
let smoothed: f64 = NaN;
let share: f64 = NaN;
// A venue with no observation this bar contributes nothing, and its weight drops out of the sum.
function weighted(value: f64, weight: f64): f64 {
return isNaN(value) ? 0.0 : value * weight;
}
function liveWeight(value: f64, weight: f64): f64 {
return isNaN(value) ? 0.0 : weight;
}
export function init(): void {
weights.set("binance", p_w_binance());
weights.set("bybit", p_w_bybit());
ema = new Ema(21);
}
export function state(): i32 {
const binance = in_oi_binance();
const bybit = in_oi_bybit();
const wb = weights.get("binance");
const wy = weights.get("bybit");
const totalWeight = liveWeight(binance, wb) + liveWeight(bybit, wy);
agg = totalWeight > 0.0 ? (weighted(binance, wb) + weighted(bybit, wy)) / totalWeight : NaN;
smoothed = isNaN(agg) ? NaN : ema.update(agg);
const both = (isNaN(binance) ? 0.0 : binance) + (isNaN(bybit) ? 0.0 : bybit);
share = both > 0.0 && !isNaN(binance) ? (100.0 * binance) / both : NaN;
return isNaN(agg) ? 0 : 1;
}
export function finalize(): void {
out_agg_oi(agg);
out_oi_ema(smoothed);
out_binance_share(share);
emitRow();
}
export function reset(): void {
ema.reset();
agg = NaN;
smoothed = NaN;
share = NaN;
}Venue dominance is the last output: one venue's share of the aggregate,
NaN rather than a fake 100% when only one venue reported. The same
shape covers aggregated funding, liquidation totals, and cross-venue
spreads: one pinned input per venue, one term per input, and a missing
policy that says what a silent venue means. Package the helpers as a class
and the whole family is one paste.
A note on the chart: every input on the chart reads the chart's own market,
so both venue inputs above would read the same feed there. The
multi-venue variants are packages for your machine (om wrun install, then
om metric series or a watch condition); the cookbook's CVD page shows the
single-venue chart form beside the aggregated one.
Higher-timeframe views of aggregates
A fold composes with any input: bucket the aggregated series by
time.bar_open_sec and fold it on the next bucket, and the 4h view of a
cross-venue sum is confirmed and repaint-free exactly like a single-source
one (multi-timeframe.md).
Lower-timeframe data and raw bars
Not in Indicators yet. kScript's ltf(interval) delivered the finer bars
inside each chart bar as cells, and requestBars(symbol, timeframe, { bars }) returned the last N native bars of another market as a raw array
for drawing. There is no lower-timeframe celled class and no raw-bar
request: the celled classes slice the same bar by price (volume_profile),
by level (book), or by live print (tape, daemon only), and a drawing's coordinates come from outputs the
module computes on the primary grid (functions/drawing-objects.md). To
mark the last twenty daily highs, keep them in a ring buffer from a daily
fold or a 1d pin and draw them as segments.
Sparse flow sources align to the grid
Some market-flow feeds are naturally sparse: a liquidation, a funding
print, or a side-split trade bucket may simply have no event for a candle.
The missing policy is how you say what that means, per input:
| Policy | A bar with no observation reads | Use it for |
|---|---|---|
"carry" (default) | the latest eligible value, carried forward | levels and states: open interest, a coarse close, a funding rate between prints |
"nan" | NaN | "did this venue report?" logic: a NaN counts as not live and never fires a comparison |
"zero" | 0 | flows to be summed: liquidation volume, side-split volume, where nothing happened means zero |
On the primary input, "nan" and "zero" densify 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.
The practical rule from kScript still holds: keep an ohlcv spine as the
first input in a multi-source flow package, then read the flow sources
beside it. The candle grid stays stable, sparse "nothing happened" bars
become 0 or NaN as you choose, and truly unavailable history stays
distinguishable from zero activity.
Budgets and good citizenship
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. Declare the inputs the computation
needs, pin symbol and exchange together, keep the primary
selector-following (a fully pinned package computes the same value for
every selector symbol, which mislabels screens and legends), and put the
sparse feeds on the policy that matches their meaning.
Availability
Whether a venue serves a feed for a market is a platform question, answered
by name before the module runs, never with silent empty data
(data-sources.md).