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 withpush,get,set,length(), reducers, and mutators. - Maps are key/value stores. Create one with
{}and useset,get, andsize(). - Arrow lambdas (
(x) => ...) feed the array reducers (map,filter,reduce,find,some,every,forEach) and the optional comparator ofsort.
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
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.sort() // numeric ascending sort, in place, returns the array
xs.sort((a, b) => b - a) // custom order: comparator returns a number
xs.first() // first element
xs.last() // last element
xs.shift() // remove and return first element
xs.unshift(0) // prependA 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.
//@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 becausefindreturnsnawhen nothing matches;nzsupplies a fallback so the arithmetic stays finite. See na and Color.- The
(value, index)lambda inmapshows 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
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.
//@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"])Sorting
sort() orders an array in place and returns it. With no argument the order is numeric ascending: [30, 4, 100, 25] comes out [4, 25, 30, 100]. That is a true numeric sort, not JavaScript's default lexicographic one, which would put 100 first.
The examples in this section are self-checking: each counts failed expectations and detonates a deliberate runtime error if any assertion misses, so a script that runs at all is proof of the values in its comments.
//@version=3
define(title="Sort Argless Ascending", position="offchart", axis=true)
timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
// Argless sort() is NUMERIC ascending (a JS-default lexicographic sort would
// yield [100, 25, 30, 4]; kScript must yield [4, 25, 30, 100]) and sorts in place.
var xs = [30, 4, 100, 25]
xs.sort()
var failures = 0
failures += xs.get(0) == 4 ? 0 : 1
failures += xs.get(1) == 25 ? 0 : 1
failures += xs.get(2) == 30 ? 0 : 1
failures += xs.get(3) == 100 ? 0 : 1
failures += xs.length() == 4 ? 0 : 1
if (failures > 0) {
var bomb = [1]
bomb.removeAt(9)
}
plotLine(value=xs.first() + barIndex / 1000, colors=["#2563eb"], width=2, label=["Sorted min"], desc=["first element after argless numeric ascending sort"])Sorting is not v3-gated: the identical body under //@version=2 compiles and runs with the identical result.
Comparators
sort takes at most one argument, an arrow lambda (a, b) => number in the JavaScript convention: a negative return puts a first, a positive return puts b first. So (a, b) => a - b is ascending and (a, b) => b - a is descending. The comparator is an ordinary lambda (syntax and closures in Lambdas & Reducers); what is special is the contract on its return value, enforced at compile time and again at run time (see the rejection list below).
//@version=3
define(title="Sort Comparator Descending", position="offchart", axis=true)
timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
// The JS-idiom comparator: (a, b) => number. Negative = a first, positive = b first.
// (a, b) => b - a is descending.
var xs = [5, 1, 4, 2]
xs.sort((a, b) => b - a)
var failures = 0
failures += xs.get(0) == 5 ? 0 : 1
failures += xs.get(1) == 4 ? 0 : 1
failures += xs.get(2) == 2 ? 0 : 1
failures += xs.get(3) == 1 ? 0 : 1
// copy-then-sort idiom (sort is in-place; slice() makes the copy)
var ys = [3, 1, 2]
var sortedCopy = ys.slice(0, ys.length()).sort((a, b) => a - b)
failures += ys.get(0) == 3 ? 0 : 1
failures += sortedCopy.get(0) == 1 ? 0 : 1
failures += sortedCopy.get(2) == 3 ? 0 : 1
if (failures > 0) {
var bomb = [1]
bomb.removeAt(9)
}
plotLine(value=xs.first() + trade.close / 1000, colors=["#2563eb"], width=2, label=["Desc first"], desc=["largest element after descending comparator sort"])Two idioms worth lifting out:
(a, b) => b - aturns[5, 1, 4, 2]into[5, 4, 2, 1].sortmutates the receiver, so sorting a copy is spelledys.slice(0, ys.length()).sort(...): theslicemakes the snapshot, and the originalysstill starts with3afterwards.
Structs sort by any field, stably
To order a struct array, return the numeric difference of the field you care about. The sort is stable: elements that compare equal keep their original relative order. Below, the two price=1 rows keep their original sequence (seq 2 then 4) and the two price=2 rows keep 1 then 3.
//@version=3
define(title="Sort Structs Stable", position="offchart", axis=true)
type Level {
price: number
seq: number
}
timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
// Comparator sort over a struct array: order by any field.
// Duplicate keys prove stability (equal elements keep their original relative order).
var levels = [Level.new(price=2, seq=1), Level.new(price=1, seq=2), Level.new(price=2, seq=3), Level.new(price=1, seq=4)]
levels.sort((a, b) => a.price - b.price)
var failures = 0
failures += levels.get(0).price == 1 ? 0 : 1
failures += levels.get(1).price == 1 ? 0 : 1
failures += levels.get(2).price == 2 ? 0 : 1
failures += levels.get(3).price == 2 ? 0 : 1
// stability: within equal prices, seq order 2,4 and 1,3 is preserved
failures += levels.get(0).seq == 2 ? 0 : 1
failures += levels.get(1).seq == 4 ? 0 : 1
failures += levels.get(2).seq == 1 ? 0 : 1
failures += levels.get(3).seq == 3 ? 0 : 1
if (failures > 0) {
var bomb = [1]
bomb.removeAt(9)
}
plotLine(value=levels.get(0).seq + trade.close / 1000, colors=["#16a34a"], width=2, label=["Stable seq"], desc=["seq of first struct after stable sort by price"])In place, plus a chainable return
The receiver itself is reordered, and the return value is there so calls chain: ys.sort().first() is the minimum, and zs.sort((a, b) => b - a).first() is the maximum. The return is not an alias, though. kScript array assignment always copies (a plain var alias = xs followed by alias.push(99) leaves xs at its old length), so var ret = xs.sort() snapshots an independent copy: ret.push(99) grows ret to 4 while xs stays at 3.
//@version=3
define(title="Sort In Place Returns Array", position="offchart", axis=true)
timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
var failures = 0
// sort() mutates the RECEIVER in place; the return value lets calls chain.
var xs = [3, 1, 2]
var ret = xs.sort()
failures += xs.get(0) == 1 ? 0 : 1
failures += xs.get(1) == 2 ? 0 : 1
failures += xs.get(2) == 3 ? 0 : 1
// kScript array ASSIGNMENT copies (true of any array assignment, not sort-specific):
// the assigned return is an independent snapshot, so mutating it leaves xs alone.
ret.push(99)
failures += ret.length() == 4 ? 0 : 1
failures += xs.length() == 3 ? 0 : 1
// returning the array is what makes chaining work
var ys = [9, 7, 8]
var lowest = ys.sort().first()
failures += lowest == 7 ? 0 : 1
failures += ys.get(0) == 7 ? 0 : 1
// comparator form is equally in-place and chainable
var zs = [1, 3, 2]
var top = zs.sort((a, b) => b - a).first()
failures += top == 3 ? 0 : 1
failures += zs.get(0) == 3 ? 0 : 1
failures += zs.get(2) == 1 ? 0 : 1
if (failures > 0) {
var bomb = [1]
bomb.removeAt(9)
}
plotLine(value=lowest + top + trade.close / 1000, colors=["#2563eb"], width=2, label=["Chain score"], desc=["chained sort().first() results plus close drift"])Strings have no built-in ordering
< and > compile on strings but are not an ordering: "a" < "b", "b" < "a", "a" > "b", and "b" > "a" all evaluate false. A comparator like (a, b) => a < b ? -1 : 1 therefore returns 1 for every pair and scrambles the array instead of sorting it: ["ccc", "a", "bb"] comes back ["bb", "a", "ccc"]. Order strings by deriving a numeric key instead, here by length:
//@version=3
define(title="Sort Strings Comparator", position="offchart", axis=true)
timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
// Non-numeric elements sort fine when the comparator DERIVES a number.
// (There is no lexicographic ordering: '<' on strings is not an ordering in
// kScript, so order strings by a numeric key such as length().)
var ss = ["ccc", "a", "bb"]
ss.sort((a, b) => a.length() - b.length())
var failures = 0
failures += ss.get(0) == "a" ? 0 : 1
failures += ss.get(1) == "bb" ? 0 : 1
failures += ss.get(2) == "ccc" ? 0 : 1
if (failures > 0) {
var bomb = [1]
bomb.removeAt(9)
}
plotLine(value=ss.get(0).length() + barIndex / 100, colors=["#2563eb"], width=2, label=["Shortest len"], desc=["length of shortest string after keyed sort"])na elements
Argless sort treats na as equal to everything: an array containing na does not throw, but how much of it actually gets sorted depends on where the na sits. [3, na, 1] comes back exactly [3, na, 1], silently unsorted, yet the same values as [3, 1, na] come back [1, 3, na], and [5, 2, na, 4, 1] comes back fully sorted as [1, 2, 4, 5, na]. Treat the result as unreliable, not merely unsorted.
//@version=3
define(title="Sort Argless With Na", position="offchart", axis=true)
timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
// Argless sort with an na element does NOT throw, but every comparison against
// na reports "equal", so the outcome depends on where the na sits:
// [3, na, 1] comes back UNCHANGED, yet [3, 1, na] comes back [1, 3, na].
var xs = [3, na, 1]
xs.sort()
var failures = 0
failures += xs.length() == 3 ? 0 : 1
failures += xs.get(0) == 3 ? 0 : 1
failures += isna(xs.get(1)) ? 0 : 1
failures += xs.get(2) == 1 ? 0 : 1
var naCount = xs.reduce((acc, v) => acc + (isna(v) ? 1 : 0), 0)
failures += naCount == 1 ? 0 : 1
var ys = [3, 1, na]
ys.sort()
failures += ys.get(0) == 1 ? 0 : 1
failures += ys.get(1) == 3 ? 0 : 1
failures += isna(ys.get(2)) ? 0 : 1
if (failures > 0) {
var bomb = [1]
bomb.removeAt(9)
}
plotLine(value=xs.get(0) + trade.close / 1000, colors=["#d97706"], width=2, label=["Unsorted head"], desc=["argless sort with na is position dependent not a no-op"])What sort rejects
The compile-time contract: the argument must be a lambda, at most one of them, returning a number. The classic JavaScript habit of returning a boolean is the first thing it catches:
//@version=3
define(title="Sort Reject Bool Comparator", position="offchart", axis=true)
timeseries trade = ohlcv(symbol=currentSymbol, exchange=currentExchange)
// EXPECTED COMPILE FAILURE: the JS habit (a, b) => a > b returns a boolean.
// kScript rejects it at compile time; the comparator must return a number.
var xs = [3, 1, 2]
xs.sort((a, b) => a > b)
plotLine(value=trade.close, colors=["#dc2626"], width=2, label=["Close"], desc=["anchor for bool comparator rejection"])This fails to compile with Array method 'sort' comparator must return a number, got boolean at 9:1. Return a - b, not a > b. The full rejection list, with observed errors:
- A comparator returning a string,
xs.sort((a, b) => "x"): fails to compile withArray method 'sort' comparator must return a number, got string at 8:1. - A non-lambda argument,
xs.sort(1):Array method 'sort' expects a lambda callback at 8:9. - Two arguments,
xs.sort((a, b) => a - b, 1):Array method 'sort' expects 0-1 arguments, got 2 at 8:1. - A comparator producing a non-finite number at run time,
xs.sort((a, b) => (b - a) / 0): compiles, then the run stops withArray method 'sort' comparator must return a finite number. - A mixed return type that evades the static check,
xs.sort((a, b) => a < b ? -1 : "x")(static typenumber|string): the same runtime guard fires,Array method 'sort' comparator must return a finite number.
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.
//@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:
//@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.