Core Concepts

Collections

Arrays and maps in kScript, the reducer methods that iterate them, and the two limits that keep them safe.

Core Concept 5 min read

What you get

kScript gives you two collection types and a set of functional methods to work them:

  • Arrays are ordered lists. Build them with [...] literals and work them with push, get, set, length(), reducers, and mutators.
  • Maps are key/value stores. Create one with {} and use set, get, and size().
  • Arrow lambdas ((x) => ...) feed the array reducers: map, filter, reduce, find, some, every, and forEach.

Together these replace most hand-written loops. The reducers are the idiomatic way to transform a list, and they come with iteration guards built in, so a runaway transform stops loudly instead of hanging the chart. For a deeper tour of lambdas and the reducer family, see Lambdas & Reducers.

Quick reference

var xs = [1, 2, 3]          // array literal
xs.push(4)                  // append
xs.get(0)                   // read by index
xs.set(0, 99)               // write by index
xs.length()                 // count

var m = {}                  // map literal
m.set("k", 10)              // write
m.get("k")                  // read
m.size()                    // entry count

xs.map((v) => v * 2)        // transform each element
xs.filter((v) => v > 2)     // keep matching elements
xs.reduce((sum, v) => sum + v, 0)  // fold to one value
xs.find((v) => v > 2)       // first match, || na
xs.some((v) => v > 2)       // true if any match
xs.every((v) => v > 0)      // true if all match
xs.forEach((v) => { ... })  // run a side effect per element

xs.avg()                    // numeric reducers, since 3.0.13 (in-flight)
xs.sum()
xs.median()
xs.stdev()                  // population standard deviation
xs.variance()               // population variance
xs.min()
xs.max()
xs.range()

xs.reverse()                // reverse in place and return the array
xs.first()                  // first element
xs.last()                   // last element
xs.shift()                  // remove and return first element
xs.unshift(0)               // prepend

A worked example

This script exercises the whole surface in one pass: it builds an array from the current bar's prices, mutates it, runs every reducer over it, stuffs results into a map, then sums everything into one plottable score. It is dense on purpose, as a map of what fits together.

scripts/probes/lang-collections/collections_happy.ks
//@version=2
define(title="Collections Happy Path", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

var prices = [trade.open, trade.high, trade.low]
prices.push(trade.close)
prices.set(0, prices.get(0) + barIndex / 10)

var transformed = prices.map((value, index) => value + index)
var filtered = transformed.filter((value) => value > trade.low)
var reduced = filtered.reduce((total, value) => total + value, 0)
var firstLarge = transformed.find((value) => value > trade.close)
var hasHigh = transformed.some((value) => value > trade.high)
var allPositive = transformed.every((value) => value > 0)
var sideEffectTotal = 0
transformed.forEach((value) => {
  sideEffectTotal += value / 1000
})

var levels = {}
levels.set("reduced", reduced)
levels.set("first", nz(firstLarge, trade.close))
levels.set("flags", (hasHigh ? 1 : 0) + (allPositive ? 1 : 0))

var score = levels.get("reduced") + levels.get("first") + levels.get("flags") + prices.length() + levels.size() + sideEffectTotal

plotLine(value=score, colors=["#2563eb"], width=2, label=["Collection score"], desc=["arrays maps lambdas and reducers"])

A few idioms worth lifting out of that wall:

  • prices.set(0, prices.get(0) + barIndex / 10) reads, modifies, and writes one slot in place.
  • nz(firstLarge, trade.close) matters because find returns na when nothing matches; nz supplies a fallback so the arithmetic stays finite. See na and Color.
  • The (value, index) lambda in map shows the optional index parameter; drop it when you don't need it.

What you'll see: one line whose value moves every bar, since prices is rebuilt from live OHLC and barIndex feeds into it.

Numeric reducers and manipulators

Since 3.0.13 (in-flight), numeric arrays expose avg, sum, median, stdev, variance, min, max, and range. The statistics use population math: variance() divides by n, and stdev() is the square root of that population variance. Empty-array reducers return na. Arrays also expose reverse, first, last, shift, and unshift for common positional edits.

The probe below reported ok: true. The reducer score for [1, 2, 3, 4] started at 25.368033988749893, which includes population stdev = 1.118033988749895 and variance = 1.25; the empty reducer score started at 8, proving all eight empty reducers returned na before the probe counted them.

scripts/probes/lang-collections/array_reducers_manipulators.ks
//@version=2
define(title="Array Reducers Manipulators", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

var values = [1, 2, 3, 4]
var reducerScore = values.avg() + values.sum() + values.median() + values.stdev() + values.variance() + values.min() + values.max() + values.range()

var reversed = values.reverse()
var shifted = reversed.shift()
reversed.unshift(trade.close)
var manipulatorScore = shifted + reversed.first() + reversed.last() + reversed.length()

var empty = []
var emptyScore = (isna(empty.avg()) ? 1 : 0) + (isna(empty.sum()) ? 1 : 0) + (isna(empty.median()) ? 1 : 0) + (isna(empty.stdev()) ? 1 : 0) + (isna(empty.variance()) ? 1 : 0) + (isna(empty.min()) ? 1 : 0) + (isna(empty.max()) ? 1 : 0) + (isna(empty.range()) ? 1 : 0)

plotLine(value=reducerScore + barIndex / 1000, colors=["#2563eb"], width=2, label=["Reducers"], desc=["array numeric reducers use population statistics"])
plotLine(value=manipulatorScore, colors=["#16a34a"], width=2, label=["Manipulators"], desc=["reverse first last shift and unshift mutate or read arrays"])
plotLine(value=emptyScore + barIndex / 1000, colors=["#dc2626"], width=2, label=["Empty"], desc=["empty array reducers return na"])

The two limits

Collections are bounded so a script can't exhaust memory or silently mis-type a list.

Size ceiling: 100,000 elements

A single collection tops out at 100,000 elements. Push past it and the run stops with Collection size limit exceeded at 9:5 (max 100000), pointing at the offending line. In practice you only hit this by accident, usually an unbounded push in a loop.

scripts/probes/lang-collections/collection_size_boundary.ks
//@version=2
define(title="Collection Size Boundary", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (isLastBar) {
  var values = []
  for (var n = 0; n < 100001; n += 1) {
    values.push(n)
  }
}

plotLine(value=trade.close, colors=["#dc2626"], width=2, label=["Close"], desc=["anchor for collection size boundary"])

Arrays are typed

An array has one element type, inferred from how you first fill it. Mixing types is rejected at compile time. Here a numeric array gets a string pushed onto it:

scripts/probes/lang-collections/array_type_mismatch_boundary.ks
//@version=2
define(title="Array Type Mismatch Boundary", position="offchart", axis=true)

timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
var values = [1, 2, 3]
values.push("not a number")

plotLine(value=trade.close, colors=["#dc2626"], width=2, label=["Close"], desc=["anchor for array type mismatch boundary"])

This fails to compile with Array element type mismatch: expected number, got string at 6:13. The takeaway: keep an array homogeneous. If you genuinely need to carry mixed data per row, reach for a struct instead of forcing it into one array.