Core Concepts

Multi-Source & Aggregation

Combine multiple data sources in one script: other symbols, other venues, other data types, aggregated metrics like cross-exchange CVD, and the request(), ltf(), and requestBars() helpers.

Advanced 12 min read

Verified against engine 3.0.26. The per-source deep-fetch behavior of higher-timeframe request() (and the source-count shift that comes with it) is since engine 3.0.26 (in-flight) and corrects an earlier "bounded by the chart window" claim. The depth and source-count facts are backed by the engine's passing g6-request-source-discovery.test.ts suite.

Introduction

A kScript script is not limited to its chart's data. You can load any number of sources in one script: other symbols, other exchanges, other data types (open interest, funding, order book, buy/sell volume), and combine them with ordinary math. "Aggregated" indicators (one metric summed across venues) are not a special feature; they fall out of this plus collections.

timeseries btc = source("ohlcv", "BTCUSDT", "BINANCE_FUTURES")
timeseries eth = source("ohlcv", "ETHUSDT", "BINANCE_FUTURES")

timeseries ratio = eth.close / btc.close
plotLine(ratio, label=["ETH/BTC"])

How multiple sources coexist

Three engine rules make cross-source math safe by default:

  1. One unified timeline. The engine unions all sources' timestamps into the chart's bar sequence. Every series is readable on every bar.
  2. Values forward-fill. Reading a series at a bar where that source has no row (slower cadence, late listing, different venue calendar) returns its last known value, so arithmetic keeps working. Funding (8h cadence) against 1h candles just works.
  3. Events never fabricate. The strictness rules from the TA conventions apply: a missing condition row is false, and crosses never fire on na. Stale venue data cannot invent signals.

Multi-symbol

Any source call can name a symbol other than the chart's:

timeseries spy = source("ohlcv", "ETHUSDT", "BINANCE_FUTURES")
timeseries lead = htf(spy, "4h")           // other-symbol HTF, still no-repaint
var leading = rsi(source=spy.close, period=14)

Everything downstream is source-agnostic: indicators, htf(), drawings, reducers. For the common case there is sugar:

timeseries eth4h = request("ETHUSDT", "4h")        // = htf(source("ohlcv", "ETHUSDT", currentExchange), "4h")
timeseries ethNow = request("ETHUSDT")             // raw, chart timeframe

request(symbol, timeframe?, type?, exchange?, opts?) keeps every htf guarantee (confirmed buckets, no repaint) and counts toward the source budget like any fetch. When the timeframe is a statically-known string coarser than the chart (e.g. "4h" on a 1h chart), request fetches that interval natively (a distinct source keyed by interval) and step-projects it onto the chart timeline, rather than aggregating chart bars. It falls back to chart-bar aggregation only when the venue serves no native data for that interval, when the timeframe is dynamic (a variable or input, which cannot be prefetched), or when the source is array-celled (footprint and similar). The opts object (mode/offset/bars) is documented under Multi-Timeframe opts.

Higher-timeframe requests fetch their own depth

Corrected since engine 3.0.26 (in-flight). Earlier docs said a higher-timeframe request() was "bounded by the chart's loaded window" and could not reach a far-back anchor. That is no longer true: a coarser request() now fetches its own deep history, scaled by the requested interval rather than the chart interval.

A 1d source on a 1m chart is fetched as a deep daily window (the default is ~400 bars of the requested interval), not as the chart's few-bar span, so a daily, weekly, monthly, or yearly level resolves even on a fine chart. The base chart-interval sources keep their existing shallow fetch window unchanged; only the higher-timeframe request pulls deep.

  • Auto depth. Omit bars and the request loads a period-sized window deep enough to reach the prior completed period (and the current forming one).
  • opts.bars: N overrides that with exactly N bars of the requested timeframe: request("ETHUSDT", "4h", { bars: 500 }) fetches 500 native 4h bars. For a calendar token the count is converted to the backing interval. bars is request-only.
  • Calendar backing. Calendar tokens aggregate engine-side from a shared, deduped backing source: 1D/1W/1M from a deep daily source, 1Q/1Y from a coarser weekly source (about 5x fewer bars). Tokens that share a backing interval (plus a plain request(sym, "1d") or request(sym, "1w")) collapse to one cached fetch each, so a symbol's calendar requests use at most two backing sources.

Source-count consequence. Because a coarser request is keyed by its interval, request("ETHUSDT") (chart interval) and request("ETHUSDT", "4h") (native 4h) are now two distinct sources, not one. This is a budget shift from earlier versions where they shared a key. Duplicate identical requests still dedupe. This per-source deep fetch and its source-count behavior are verified by the engine's src/__tests__/g6-request-source-discovery.test.ts suite (the EC1 deep-fetch-range and B1a calendar-backing cases), passing on 3.0.26.

For history depth on the chart's own series (cumulative vwap(), anchored VWAPs) that survives panning, you still declare it with define(..., maxBarsBack=N); that is about the base series, separate from a higher-timeframe request's independent fetch.

Multi-venue aggregation

The flagship pattern: one metric, summed across exchanges.

Aggregated CVD:

define("Aggregated CVD", "offchart", true)

timeseries bsBinance = source("buy_sell_volume", "BTCUSDT", "BINANCE_FUTURES")
timeseries bsBybit = source("buy_sell_volume", "BTCUSDT", "BYBIT")
timeseries bsOkx = source("buy_sell_volume", "BTCUSDT", "OKX")

func delta(bs) {
  return bs.buy - bs.sell
}

timeseries aggDelta = delta(bsBinance) + delta(bsBybit) + delta(bsOkx)
timeseries aggCvd = cum(aggDelta)

plotLine(aggCvd, colors=[aggDelta > 0 ? "#16a34a" : "#dc2626"], label=["Aggregated CVD"], desc=["cumulative delta across venues"])

Aggregated open interest, venue-weighted:

timeseries oiBinance = source("open_interest", "BTCUSDT", "BINANCE_FUTURES")
timeseries oiBybit = source("open_interest", "BTCUSDT", "BYBIT")

var weights = {}
weights.set("binance", 0.6)
weights.set("bybit", 0.4)

// open_interest is OHLC-shaped; .close is the current OI value
timeseries aggOI = oiBinance.close * weights.get("binance")
  + oiBybit.close * weights.get("bybit")

plotLine(aggOI)
plotLine(ema(source=aggOI, period=21), colors=["#94a3b8"], zOrder=0, label=["OI EMA"], desc=["smoothed aggregated open interest"])

Venue dominance (one venue's share of the aggregate):

timeseries share = delta(bsBinance) / nz(aggDelta, 1)

The same shape covers aggregated funding, liquidation totals, and cross-venue spreads. Package the helpers as a library (import "agg" as agg) and the whole family becomes one import.

Lower-timeframe data: ltf()

ltf(interval) fetches finer bars than the chart and delivers them as cells on the chart's own bars (the chart timeline is untouched):

// on a 1h chart: each bar carries its four 15m bars
timeseries fine = ltf("15m")

// intrabar drift: sum of the finer bars' close-open
timeseries drift = fine.cells.map((c) => c[4] - c[1]).reduce((s, x) => s + x, 0)
plotLine(drift)

Cells use the same .cells machinery as order-flow sources, so reducers, structs, and the causality guarantees all apply. The interval must be a literal string, strictly finer than the chart, and (like everything here) it errors loudly with line:column when misused.

requestBars(): raw bars for drawing

When you want to draw on top of a coarser timeframe (mark the last N daily highs/lows, box weekly ranges, label monthly opens) rather than feed a series into TA, reach for requestBars(symbol, timeframe, type?, exchange?, opts). It returns the last opts.bars native bars of symbol at timeframe as a plain array of [time, open, high, low, close, volume] rows, oldest first. The timeframe is a coarse native interval string ("1d", "4h", "1w") and is required; opts.bars is a required positive integer; type defaults to "ohlcv" and exchange defaults to the current venue. There is no mode/offset here (those are request()/htf() period concepts).

The result is draw-only and inert: a raw array indexed by position (rows[i]), not a chart-indexed timeseries. It carries no [n] bar-offset history and is not chainable into TA functions; you iterate it and draw. Crucially it is independent of maxBarsBack, reading the deep source directly, so a 1m chart can draw the last 20 daily candles even with a tiny lookback window.

This is the counterpart to request(). Where request() returns a chart-aligned, bar-indexed, TA-chainable timeseries (and request(..., { bars: N }) only sets the fetch depth of that still-projected series), requestBars() hands you the raw native rows for drawing:

define("daily levels", "onchart", true, maxBarsBack=50)
if (isLastBar) {
  var rows = requestBars(currentSymbol, "1d", { bars: 20 })
  for (var i = 0; i < 20; i = i + 1) {
    var r = rows[i]                 // r = [t, o, h, l, c, v]
    line.new(r[0], r[2], r[0], r[3], { color: "#ffffff", width: 1 })
  }
}

Budgets and good citizenship

A script can subscribe to at most 20 distinct sources. The editor and the runtime enforce the same number, so a script that runs also passes validation; a 21st source fails with Script exceeds the source budget (21/20). Identical source(...) calls dedupe to one, and htf() over a source you already opened is free since it transforms already-fetched data rather than fetching. A coarser request(sym, "4h"), however, is keyed by its interval, so it is its own source distinct from a chart-interval request(sym) (see Higher-timeframe requests fetch their own depth); a symbol's calendar requests collapse onto at most two shared backing sources. Heavy source types count for more than one (orderbook counts as 3), so a handful of order books reaches the cap sooner. Twenty distinct streams is ample headroom for multi-venue aggregation.

Availability

The language accepts any registered (type, symbol, exchange) triple; whether a venue serves a given data type is a platform question. The platform exposes a machine-readable catalog of servable source types, and an unavailable request fails loudly with the list of what exists, never with silent empty data.