Updated for engine 3.0.49.
requestBars()gainsanchor: "latest"/"window"and serves composed intervals like"2h"; both are backed by the engine's passingg6-request-bars.test.tssuite and a live pan-back test against the chart backend.
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:
- One unified timeline. The engine unions all sources' timestamps into the chart's bar sequence. Every series is readable on every bar.
- 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.
- 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 timeframerequest(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 (volume_profile and similar). The opts object (mode/offset/bars) is documented under Multi-Timeframe opts.
Higher-timeframe requests fetch their own depth
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 a shallow fetch window; only the higher-timeframe request pulls deep.
- Auto depth. Omit
barsand the request loads a period-sized window deep enough to reach the prior completed period (and the current forming one). opts.bars: Noverrides that with exactlyNbars 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.barsis request-only.- Calendar backing. Calendar tokens aggregate engine-side from a shared, deduped backing source:
1D/1W/1Mfrom a deep daily source,1Q/1Yfrom a coarser weekly source (about 5x fewer bars). Tokens that share a backing interval (plus a plainrequest(sym, "1d")orrequest(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 two distinct sources, not one. Duplicate identical requests still dedupe.
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 native interval string ("15m", "1h", "1d") and is required; opts.bars is a required positive integer; type defaults to "ohlcv" and exchange defaults to the current venue. opts.anchor (below) chooses which window the bars come from. There is no mode/offset here (those are request()/htf() period concepts). Calendar tokens (1W, 1M, 1Q, 1Y) are rejected — requestBars is for native venue intervals. Intervals the venue does not serve directly but that compose from a finer one ("2h", "3h", "12h", "2d", "45m") are served by aggregating the largest fetchable divisor (2h = 2×1h), UTC-bucketed, so they return true bars at the requested timeframe.
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 })
}
}anchor: "latest" — bars pinned to the market, not the pan position
By default, requestBars() is window-relative: the rows come from the range the script is executing over, so when the user pans the chart back a week, a re-run over that window returns bars from the historical week. That is right for "levels near what I'm looking at", but wrong for dashboards that should always show current market data — the panels of a plotMiniChartGrid would silently turn into last week's candles.
anchor: "latest" pins the request to the newest native bars instead:
var rows = requestBars(currentSymbol, "15m", "ohlcv", currentExchange, { bars: 18, anchor: "latest" })- The rows are always the latest
barsbars of that timeframe — independent of how far back the user has panned and of the script's execution window. They update live: the last row is the forming bar, and the tail advances when a bar closes. anchormust resolve statically to"latest"or"window"— any other value is a hard error, and so is a value the compiler cannot pin down at build time (say, a ternary on bar data), because the anchor is part of the source's identity and cannot vary at runtime. A top-levelvar mode = "latest"used asanchor: modedoes resolve statically and is accepted; write the literal for clarity."window"spells the default explicitly and behaves byte-identically to omittinganchor(it also dedupes with an anchor-less request of the same parameters).- An anchored and a window-relative request of the same symbol and timeframe are two distinct sources and cost two source-budget slots — they fetch different windows, so they cannot share one subscription.
Sparse flow sources align to the chart timeline
Some market-flow feeds are naturally sparse: a liquidation, funding, or buy/sell
volume bucket may simply have no event for a candle. Source
types that mark missing buckets as zero are aligned to the chart's candle
timeline before your script reads them. Once the source has started returning
real rows, a missing bucket reads as 0 instead of shifting every later value
onto the wrong candle. Bars before the source's first real row still read as
na, because the engine does not invent data for a period where coverage is
unknown.
For authors, the practical rule is: keep an ohlcv(...) spine in multi-source
flow scripts, then read the flow source directly. The candle timeline stays
stable, sparse "nothing happened" buckets become zero, and truly unavailable
history remains distinguishable from zero activity.
Budgets and good citizenship
A script has a budget of 10 weighted source slots (MAX_SOURCES_PER_SCRIPT): a native stream costs one slot, heavier source types can be configured to cost more, and the chart's own price spine can cost zero. 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. Ten single-slot sources fill the budget exactly; design within the 10 weighted slots.
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.