Collections

View as MarkdownOpen the editor

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 declared max, index it with a cursor, and it never grows.
  • Array<T> is a growable list with push, pop, length, and the familiar methods. Grow it in onStart(), not per bar.
  • Map<K, V> is a key-value store with set, get, has, delete, size, keys(), and values().
  • Loops (for, while, do) are how you iterate. The reducer methods exist on Array, but each map or filter allocates a new array, so they belong in onStart(), 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

text
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 array

Array<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());
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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. Sorting values in place would destroy the ring order.
  • Empty-window reducers return NaN, and onBar() returns before writing until the window is full anyway.
  • resize() clamps to the capacity allocated from the param's max, 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));
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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.

text
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));   // descending

The 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's max;
  • never push without bound, never slice or map per bar, never build a string with + per bar;
  • prefer a StaticArray you overwrite to an Array you 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.