Arrays and maps in an Indicator, the loops that replace the reducer
methods of kScript (legacy), sorting with a comparator, and the two limits
that keep a module safe: memory is allocated once and never freed, and
every array has one element type. kScript gave you [...] literals, {} maps, and a
family of reducers with iteration guards; an Indicator gives you
AssemblyScript's StaticArray, Array, and Map, and you write the loop.
What you get
StaticArray<T>is a fixed-size block, allocated once. It is the right shape for a window: size it from a param's declaredmax, index it with a cursor, and it never grows.Array<T>is a growable list withpush,pop,length, and the familiar methods. Grow it ininit(), not per bar.Map<K, V>is a key-value store withset,get,has,delete,size,keys(), andvalues().- Loops (
for,while,do) are how you iterate. The reducer methods exist onArray, but eachmaporfilterallocates a new array, so they belong ininit(), not in the per-bar path (lambdas-and-reducers.md).
The rule under all three: the module has no garbage collector. Memory that
is allocated stays allocated until the run ends, and the host caps a
module at 4 MiB. Allocate at module scope or in init(), and the per-bar
path stays flat.
Quick reference
const xs = new StaticArray<f64>(50); // fixed size, allocated once
xs[0] = 1.0; xs[0]; xs.length; // write, read, count
xs.fill(NaN); // fill every slot
xs.sort(); // numeric ascending, in place
const ys = new Array<f64>(); // growable
ys.push(4.0); ys.pop(); // append, remove last
ys[0]; ys.length; // read by index, count
ys.slice(0, 2); ys.reverse(); // copy a range (allocates), reverse in place
ys.includes(1.0); ys.indexOf(2.0); // search
ys.shift(); ys.unshift(0.0); // remove first, prepend
ys.sort((a: f64, b: f64): i32 => (a > b ? 1 : a < b ? -1 : 0));
const weights: f64[] = [0.6, 0.4]; // typed literal
const names: string[] = ["binance", "bybit"];
const m = new Map<string, f64>(); // key-value store
m.set("k", 10.0); m.get("k"); m.has("k"); m.delete("k");
m.size; m.keys(); m.values(); // count, key array, value arrayArray<f64> also has map, filter, reduce, forEach, some,
every, and findIndex (there is no find); each callback is a
non-capturing function. xs.avg(), xs.median(), and the other kScript
numeric reducers do not exist: they are loops, and the worked example
below is all of them.
A worked example: window statistics
The kScript "collections happy path" built an array from the bar's prices
and ran every reducer over it. The Indicator version is a Window class
over a ring buffer with the numeric reducers as methods: sum, avg,
min, max, range, variance, stdev (population, dividing by n),
and median through an allocation-free insertion sort into a scratch
buffer. Everything is allocated once, at module scope.
import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_avg, out_median, out_range, out_stdev } from "./gen/outputs";
import { p_bars } from "./gen/params";
param("bars", 20, { min: 2, max: 200, description: "Bars in the window" });
input("close", ohlcv.close);
output("avg", line, lower, { color: "#2563eb", description: "Window mean" });
output("median", line, lower, { color: "#16a34a", description: "Window median" });
output("stdev", line, lower, { color: "#f59e0b", description: "Population standard deviation" });
output("range", line, lower, { color: "#94a3b8", description: "Max minus min" });
class Window {
values: StaticArray<f64>;
scratch: StaticArray<f64>;
size: i32;
cursor: i32 = 0;
count: i32 = 0;
constructor(capacity: i32) {
this.values = new StaticArray<f64>(capacity);
this.scratch = new StaticArray<f64>(capacity);
this.size = capacity;
}
resize(n: i32): void {
this.size = n < 1 ? 1 : n > this.values.length ? this.values.length : n;
this.reset();
}
push(x: f64): void {
this.values[this.cursor] = x;
this.cursor = (this.cursor + 1) % this.size;
if (this.count < this.size) this.count += 1;
}
full(): bool {
return this.count == this.size;
}
sum(): f64 {
let s = 0.0;
for (let i = 0; i < this.count; i++) s += this.values[i];
return s;
}
avg(): f64 {
return this.count == 0 ? NaN : this.sum() / f64(this.count);
}
min(): f64 {
let m = Infinity;
for (let i = 0; i < this.count; i++) if (this.values[i] < m) m = this.values[i];
return this.count == 0 ? NaN : m;
}
max(): f64 {
let m = -Infinity;
for (let i = 0; i < this.count; i++) if (this.values[i] > m) m = this.values[i];
return this.count == 0 ? NaN : m;
}
range(): f64 {
return this.max() - this.min();
}
variance(): f64 {
if (this.count == 0) return NaN;
const mean = this.avg();
let sq = 0.0;
for (let i = 0; i < this.count; i++) {
const d = this.values[i] - mean;
sq += d * d;
}
return sq / f64(this.count); // population variance: divide by n
}
stdev(): f64 {
return Math.sqrt(this.variance());
}
median(): f64 {
if (this.count == 0) return NaN;
// Copy into the scratch buffer and insertion-sort the first count slots: no allocation.
for (let i = 0; i < this.count; i++) this.scratch[i] = this.values[i];
for (let i = 1; i < this.count; i++) {
const x = this.scratch[i];
let j = i - 1;
while (j >= 0 && this.scratch[j] > x) {
this.scratch[j + 1] = this.scratch[j];
j -= 1;
}
this.scratch[j + 1] = x;
}
const mid = this.count / 2;
return this.count % 2 == 1 ? this.scratch[mid] : (this.scratch[mid - 1] + this.scratch[mid]) / 2.0;
}
reset(): void {
this.cursor = 0;
this.count = 0;
}
}
const MAX_BARS = 200;
const window = new Window(MAX_BARS); // allocated once, sized from the param's max
export function init(): void {
window.resize(i32(p_bars()));
}
export function state(): i32 {
window.push(in_close());
return window.full() ? 1 : 0;
}
export function finalize(): void {
out_avg(window.avg());
out_median(window.median());
out_stdev(window.stdev());
out_range(window.range());
emitRow();
}
export function reset(): void {
window.reset();
}A few idioms worth lifting out:
this.values[this.cursor] = x; this.cursor = (this.cursor + 1) % this.size;is the ring buffer: the oldest slot is overwritten and nothing shifts.median()sorts a copy in a preallocated scratch buffer, so a per-bar median allocates nothing. Sortingvaluesin place would destroy the ring order.- Empty-window reducers return
NaN, the counterpart of kScript'sna, andstate()abstains until the window is full anyway. resize()clamps to the capacity allocated from the param'smax, so a setting change never asks for memory that was not reserved.
Maps
A Map is right for a small keyed table: weights per venue, a running
total per hour of day, a level per session name. Allocate the map at
module scope and insert its keys in init(); from then on set on an
existing key allocates nothing. Volume by hour of the day, from the time
source:
import { histogram, input, line, lower, ohlcv, output, time } from "./sdk/declare";
import { in_bar_t, in_volume } from "./gen/inputs";
import { emitRow, out_hour, out_hour_total } from "./gen/outputs";
input("volume", ohlcv.volume);
input("bar_t", time.bar_open_sec);
output("hour_total", histogram, lower, { color: "#2563eb", description: "Cumulative volume traded in this bar's UTC hour, over the loaded history" });
output("hour", line, lower, { color: "#94a3b8", description: "The bar's UTC hour, 0 to 23" });
const byHour = new Map<i32, f64>();
let hour: i32 = 0;
let total: f64 = 0.0;
export function init(): void {
for (let h = 0; h < 24; h++) byHour.set(h, 0.0); // every key exists before the first bar
}
export function state(): i32 {
hour = i32((i64(in_bar_t()) % 86400) / 3600); // epoch seconds to the UTC hour
total = byHour.get(hour) + in_volume();
byHour.set(hour, total); // an existing key: no allocation
return 1;
}
export function finalize(): void {
out_hour_total(total);
out_hour(f64(hour));
emitRow();
}
export function reset(): void {
for (let h = 0; h < 24; h++) byHour.set(h, 0.0);
hour = 0;
total = 0.0;
}get on a missing key traps at runtime, so guard with has when a key
might not exist, or insert every key up front as this file does.
Sorting
sort() orders an array in place and returns it, on Array<f64> and
StaticArray<f64> alike. With no argument the order is numeric
ascending: [30, 4, 100, 25] comes out [4, 25, 30, 100].
Comparators
sort takes one optional comparator, a function (a: T, b: T) => i32 in
the JavaScript convention: a negative result puts a first, a positive
result puts b first.
xs.sort((a: f64, b: f64): i32 => (a > b ? 1 : a < b ? -1 : 0)); // ascending
xs.sort((a: f64, b: f64): i32 => (a < b ? 1 : a > b ? -1 : 0)); // descendingThe comparator is an ordinary non-capturing function, so it may read
module-level state but not a local of the enclosing function
(lambdas-and-reducers.md). Sorting a copy is xs.slice(0, xs.length).sort(...), and slice allocates, so do it in init() or use
a scratch buffer as the window example does.
Structs, strings, and NaN
An array of class instances sorts by a comparator over a field
((a: Level, b: Level): i32 => (a.price > b.price ? 1 : a.price < b.price ? -1 : 0)); strings compare code unit by code unit, so a < b orders them
directly. NaN > x and NaN < x are both false, so a NaN element
compares equal to everything and lands wherever the algorithm leaves it:
drop NaN values before sorting, or map them to Infinity in the
comparator when they should sort last.
What sort rejects
The comparator's return type is i32. The JavaScript habit of returning a
boolean, (a, b) => a > b, is refused at compile time with Type 'bool' is not assignable to type 'i32'. Return the difference or the three-way
ternary. A comparator with untyped parameters, or without a return type,
is refused too (Type expected.): annotate both.
The two limits
Collections are bounded so a module cannot exhaust memory or silently mis-type a list.
Memory is allocated once
There is no garbage collector in the module. Every new takes memory that
is never returned, the host validates a 4 MiB ceiling before the module is
instantiated, and a module that grows on every bar reaches it. So:
- allocate at module scope or in
init(), sized from a param'smax; - never
pushwithout bound, neversliceormapper bar, never build a string with+per bar; - prefer a
StaticArrayyou overwrite to anArrayyou refill.
This is the Indicator counterpart of kScript's 100,000-element ceiling: not a count the runtime enforces, but a budget you allocate against, once.
Arrays are typed
An array has one element type, fixed when it is declared. Mixing types is
refused at compile time: pushing a string onto an Array<f64> fails with
Type 'String' is not assignable to type 'f64'. If you genuinely need
mixed data per row, declare a class with typed fields
(user-defined-types.md) and keep an array of those.