Lambdas and reducers

View as MarkdownOpen the editor

Arrow functions, function values, and the loops that do the work of the reducer methods (map, filter, reduce, forEach, some, every).

A wrun indicator has arrow functions, with one rule that changes how you use them: a function cannot capture a local variable, so the "closure" is module-level state and a reducer is a plain loop over a buffer.

Lambda syntax

An arrow function has typed parameters and a typed return, in either the expression or the block form. It is a value: store it in a variable typed (x: f64) => f64, pass it to a function, call it.

text
const double = (x: f64): f64 => x * 2.0;
const label = (side: f64): string => {
  return side > 0.0 ? "bid" : "ask";
};

let pick: (x: f64) => bool = above;   // a named function is a value too
const y = double(21.0);               // 42

Both forms need every type written out: an untyped parameter or a missing return type is a parse error (Type expected.).

The one rule: no captured locals

The JavaScript habit is a lambda that closes over the surrounding scope: const threshold = high * 0.99; prices.filter((p) => p > threshold). In an indicator that exact shape, an arrow function inside onBar() reading a const of onBar(), is refused at compile time:

text
ERROR AS100: Not implemented: Closures

The compiler cannot build a function that carries a copy of another function's locals. What a function can read is module-level state, which is where an indicator keeps everything that matters anyway. So the pattern is: put the threshold in a module-level let, write the predicate as a named function (or an arrow at module scope), and pass it.

param("bars", 20, { min: 1, max: 200, description: "Bars in the window" });
output("above_count", line, lower, { color: "#16a34a", description: "Bars in the window closing above the window mean" });
output("near_high_count", line, lower, { color: "#f59e0b", description: "Bars in the window within 1% of the window high" });

const MAX_BARS = 200;
const closes = new StaticArray<f64>(MAX_BARS);
let n: i32 = 20;
let cursor: i32 = 0;
let count: i32 = 0;
let mean: f64 = NaN; // module-level: the "closure" every predicate reads
let nearHighLevel: f64 = NaN;

// Predicates are named functions over module-level state, never over a local.
function aboveMean(x: f64): bool {
  return x > mean;
}

function nearHigh(x: f64): bool {
  return x >= nearHighLevel;
}

// A reducer is a loop that takes the predicate as a value.
function countWhere(xs: StaticArray<f64>, len: i32, pred: (x: f64) => bool): i32 {
  let hits = 0;
  for (let i = 0; i < len; i++) if (pred(xs[i])) hits += 1;
  return hits;
}

function windowMax(xs: StaticArray<f64>, len: i32): f64 {
  let m = -Infinity;
  for (let i = 0; i < len; i++) if (xs[i] > m) m = xs[i];
  return m;
}

function windowMean(xs: StaticArray<f64>, len: i32): f64 {
  let s = 0.0;
  for (let i = 0; i < len; i++) s += xs[i];
  return s / f64(len);
}

function onStart(): void {
  n = i32(p_bars());
}

function onBar(): void {
  closes[cursor] = bar.close();
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  if (count < n) return;
  mean = windowMean(closes, n);
  nearHighLevel = windowMax(closes, n) * 0.99;
  out_above_count(f64(countWhere(closes, n, aboveMean)));
  out_near_high_count(f64(countWhere(closes, n, nearHigh)));
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

countWhere is the reducer; aboveMean and nearHigh are the lambdas, reading mean and nearHighLevel from module scope instead of capturing them. The same function value can be passed to any loop that takes a (x: f64) => bool.

The reducer methods

Array<f64> has the familiar methods, and each callback is a non-capturing function with typed parameters (trailing parameters may be omitted):

MethodCallbackReturnsAllocates?
map<U>(fn)(value: f64, index?: i32) => Ua new arrayyes
filter(fn)(value: f64, index?: i32) => boola new arrayyes
reduce<U>(fn, initial)(acc: U, value: f64, index?: i32) => Uthe final accumulatorno
forEach(fn)(value: f64, index?: i32) => voidnothingno
findIndex(fn)(value: f64, index?: i32) => boolthe first matching index, or -1no
some(fn)(value: f64, index?: i32) => booltrue if any matchno
every(fn)(value: f64, index?: i32) => booltrue if all matchno

There is no find: use findIndex and read the element. map and filter return a new array every call, and the module never frees memory, so they belong in onStart() (building a lookup table once) and not in onBar(). On the per-bar path, write the loop over a StaticArray you allocated once; the window example above is the template, and collections.md has every numeric reducer as a method.

text
// onStart(): fine, once
const doubled = periods.map<f64>((p: f64): f64 => p * 2.0);

// onBar(): a loop over a preallocated buffer, no allocation
let sum = 0.0;
for (let i = 0; i < n; i++) sum += window[i];

The microstructure idiom

Where you would reach for a map and a reduce over order-flow rows (buy minus sell per bucket, summed), an indicator reads the bar's cells (one [low, high, buy, sell] row per price bucket) through the input's view and loops over them: the net delta of the bar, and the price of its largest-volume bucket (the point of control).

input("close", ohlcv.close);
input("profile", volume_profile.cells, { max_cells: 8192 });
output("delta", line, lower, { color: "#2563eb", description: "Net per-bar delta: buy minus sell volume across every price bucket" });
output("poc", line, overlay, { color: "#f59e0b", description: "Price of the bucket that traded the most volume" });

function onBar(): void {
  const n = in_profile_cells();
  if (n <= 0) return; // no block this bar
  const cells = in_profile_view(); // max_cells tuples of 4 f64; only the first n values are this bar's
  let net = 0.0;
  let best = -1.0;
  let bestPrice = NaN;
  for (let i = 0; i + 3 < n; i += 4) {
    const buy = cells[i + 2];
    const sell = cells[i + 3];
    net += buy - sell; // the map + reduce, as one pass
    if (buy + sell > best) {
      best = buy + sell;
      bestPrice = (cells[i] + cells[i + 1]) / 2.0; // the bucket's midpoint
    }
  }
  out_delta(net);
  out_poc(bestPrice);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Every windowed class in the TA kit accepts any number, so rsi.update(delta) is a delta-RSI in one line. The celled input, its accessors, and the max_cells contract are in data-sources.md.

Loops still exist, and you bound them

for, while, and do are the iteration tools (for...of is not implemented: Not implemented: Iterators, so index loops it is). There is no per-loop ceiling inside the module; size every loop by a param with a declared max. A loop that runs away is stopped by the host's execution timeout and the evaluation is refused: it cannot hang the chart, and it cannot produce a value either.