---
title: "Collections"
description: "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…"
order: 15
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 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 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 `init()`, 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 `init()`, 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

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

```typescript
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. Sorting `values` in place would destroy
  the ring order.
- Empty-window reducers return `NaN`, the counterpart of kScript's `na`,
  and `state()` abstains 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 `init()`; from then on `set` on an
existing key allocates nothing. Volume by hour of the day, from the `time`
source:

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

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

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.
