Lambdas and reducers
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.
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); // 42Both 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:
ERROR AS100: Not implemented: ClosuresThe 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)));
}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):
| Method | Callback | Returns | Allocates? |
|---|---|---|---|
map<U>(fn) | (value: f64, index?: i32) => U | a new array | yes |
filter(fn) | (value: f64, index?: i32) => bool | a new array | yes |
reduce<U>(fn, initial) | (acc: U, value: f64, index?: i32) => U | the final accumulator | no |
forEach(fn) | (value: f64, index?: i32) => void | nothing | no |
findIndex(fn) | (value: f64, index?: i32) => bool | the first matching index, or -1 | no |
some(fn) | (value: f64, index?: i32) => bool | true if any match | no |
every(fn) | (value: f64, index?: i32) => bool | true if all match | no |
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.
// 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);
}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.