---
title: "Collections"
description: "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…"
order: 18
section: "core-concepts"
---

<!-- source: docs/indicators/core-concepts/collections.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# 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 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.

```typescript sample=cc-collections-window
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. 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:

```typescript sample=cc-collections-map
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.

```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.
