Collections
Arrays and maps in a wrun indicator, the loops that do the work of the reducer methods, 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.
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 inonStart(), 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 inonStart(), 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 onStart(), 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. There is no xs.avg() or xs.median(): the
numeric reducers are loops, and the worked example below is all of them.
A worked example: window statistics
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.
param("bars", 20, { min: 2, max: 200, description: "Bars in the window" });
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
function onStart(): void {
window.resize(i32(p_bars()));
}
function onBar(): void {
window.push(bar.close());
if (!window.full()) return;
out_avg(window.avg());
out_median(window.median());
out_stdev(window.stdev());
out_range(window.range());
}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, andonBar()returns before writing 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 onStart(); from then on set on an
existing key allocates nothing. Volume by hour of the day, from the time
source:
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>();
function onStart(): void {
for (let h = 0; h < 24; h++) byHour.set(h, 0.0); // every key exists before the first bar
}
function onBar(): void {
const hour = i32((i64(bar.time()) % 86400) / 3600); // epoch seconds to the UTC hour
const total = byHour.get(hour) + bar.volume();
byHour.set(hour, total); // an existing key: no allocation
out_hour_total(total);
out_hour(f64(hour));
}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 onStart() 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 chart caps a module at 4 MiB, and it refuses a run
whose memory grows once the bars start ("The Indicator allocated memory
after init() ...: the sandbox forbids growth once the bars start."), which
a module that allocates on every bar does sooner or later. So:
- allocate at module scope or in
onStart(), sized from a param'smax; - never
pushwithout bound, neversliceormapper bar, never build a string with+per bar; - prefer a
StaticArrayyou overwrite to anArrayyou refill.
The limit is 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.