---
title: "Multi-source and aggregation"
description: "Combine several markets, venues, and data types in one Indicator: other symbols and other venues through symbol + exchange pins, other feeds through their…"
order: 21
section: "core-concepts"
---

<!-- source: docs/indicators/core-concepts/multi-source.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Multi-source and aggregation

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.

```text
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:

1. **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.
2. **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"`), or `0` (`"zero"`), and you
   choose per input. Funding (an 8h cadence) against 1h candles just works.
3. **Events never fabricate.** Every comparison with `NaN` is false, and
   `Cross.update()` answers `0` when either side is `NaN`, 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:

```typescript
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:

```typescript
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`).
