User-defined functions

View as MarkdownOpen the editor

Create custom, reusable functions with the function keyword: typed parameters, a typed return, default values, functions as values, and the rules that keep them honest in a per-bar module.

A wrun indicator's functions are plain TypeScript syntax, with every type written out and module-level state in place of closures.

Overview

FeatureDescription
functionThe keyword; declares a named function with typed parameters and return
Any logicEncapsulate a calculation, a predicate, a candle pattern, a session test
Module stateFunctions read and write module-level variables; that is how state reaches them
ValuesA non-capturing function is a value you can store and pass

Function declaration

Basic syntax

text
function functionName(parameter1: f64, parameter2: f64): f64 {
  // body
  return result;
}

Every parameter carries a type and the return type is written after the parameter list. Leaving the return type off is a parse error (Type expected.), so annotate even void.

Calling

Calls are positional, in the declared order. There are no keyword arguments; readable names and a stable order do the same job, and a function with many settings takes a class instance instead of a long argument list. Parameters may carry defaults:

text
function calculate(base: f64, multiplier: f64, offset: f64 = 0.0): f64 {
  return base * multiplier + offset;
}

const a = calculate(10.0, 2.0, 5.0);   // 25
const b = calculate(10.0, 2.0);        // 20, offset defaulted

Syntax details

Function name: standard identifier rules (letters, digits, underscore; not starting with a digit). The two names onStart and onBar are the hooks the build calls; everything else is yours, and nothing needs an export.

Parameters: typed, positional, optionally defaulted. A parameter is an f64 for a price or a value, an i32 for a count, a StaticArray<f64> for a window, a class for a struct, or a function type for a callback.

Return statement: every path returns a value of the declared type (void returns nothing). The compiler refuses a missing return.

Function examples

Safe division

text
function safeDiv(a: f64, b: f64): f64 {
  return b == 0.0 ? 0.0 : a / b;
}

const ratio = safeDiv(10.0, 2.0);   // 5
const safe = safeDiv(10.0, 0.0);    // 0

Average of two values

text
function average(a: f64, b: f64): f64 {
  return (a + b) / 2.0;
}

const mid = average(close, prevClose);

Custom pattern logic

A candle-pattern test takes this bar's and the previous bar's open and close. An indicator remembers the previous bar's values in module-level variables that onBar() updates on every bar:

input("open", ohlcv.open);
input("close", ohlcv.close);
input("low", ohlcv.low);
output("engulfing", shape, overlay, { color: "#16a34a", shape_where: "is_engulfing", description: "Bullish engulfing candle" });
output("is_engulfing", none);
output("body_ratio", line, lower, { color: "#94a3b8", description: "This body divided by the previous body" });

function safeDiv(a: f64, b: f64): f64 {
  return b == 0.0 ? 0.0 : a / b;
}

function isGreenCandle(openPrice: f64, closePrice: f64): bool {
  return closePrice > openPrice;
}

function isBullishEngulfing(prevOpen: f64, prevClose: f64, currOpen: f64, currClose: f64): bool {
  const prevWasRed = prevClose < prevOpen;
  const currIsGreen = isGreenCandle(currOpen, currClose);
  const engulfs = currOpen < prevClose && currClose > prevOpen;
  return prevWasRed && currIsGreen && engulfs;
}

let prevOpen: f64 = NaN;
let prevClose: f64 = NaN;

function onBar(): void {
  const open = bar.open();
  const close = bar.close();
  const ready = !isNaN(prevOpen);
  const engulfing = ready && isBullishEngulfing(prevOpen, prevClose, open, close);
  const bodyRatio = ready ? safeDiv(Math.abs(close - open), Math.abs(prevClose - prevOpen)) : NaN;
  prevOpen = open;
  prevClose = close;
  if (!ready) return;
  out_engulfing(bar.low()); // the mark sits under the candle
  out_is_engulfing(engulfing ? 1.0 : 0.0);
  out_body_ratio(bodyRatio);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

The three helpers are pure: they take numbers and return a number or a bool, and onBar() supplies the remembered previous bar. That keeps the pattern testable in isolation and the per-bar function short.

Functions as values

A function that captures no local variable is a value. Store it in a variable typed with its signature, pass it to a loop, keep a small table of them:

text
function aboveMean(x: f64): bool { return x > mean; }   // mean is module-level

let pred: (x: f64) => bool = aboveMean;
const hits = countWhere(window, n, pred);

Arrow functions work the same way, and nested functions (declared inside another function) are allowed as long as they read nothing from the enclosing function's locals. The full treatment, with the loop that takes a predicate, is lambdas-and-reducers.md.

Constraints and rules

No declarations inside functions

param(...), input(...), output(...), box(...), and the rest are top-level statements of the file; the build reads them without running the code. One inside a function is a named build error:

text
output(...) declarations must be top-level statements, not inside a function, class, or expression

Pass an input's value in as an argument instead:

text
// Invalid
function bad(): f64 {
  input("close", ohlcv.close);   // a declaration cannot live here
  return 0.0;
}

// Valid: declare at the top level, pass the value
function good(close: f64, avg: f64): f64 {
  return close - avg;
}

No locals from the outside

A function reads its parameters and module-level state. An inner function or arrow that reads a local of the function around it is refused with AS100: Not implemented: Closures. Move the value to module scope or pass it as a parameter.

Allocation is once, not per call

A function called in onBar() runs on every bar. A new inside it (a class, an array, a string built with +) allocates memory the module never frees. Helpers on the per-bar path take and return numbers, or write into buffers allocated at module scope.

Types everywhere

Parameter types and the return type are required; there is no inference from usage. A function meant for both a price and a count is written for f64, and the caller casts (f64(count)).

Phase rules still apply inside functions

p_<param>() reads its value from onStart() on (at module start it returns NaN), in_<input>() and bar.*() from onBar() on (NaN before the first bar), and an out_<output>() write reaches the row only from onBar(); a helper inherits the phase of its caller, so keep helpers pure and let the hooks read and write.

Best practices

  • Keep functions focused. One thing per function: rsiZone(rsiValue: f64): f64 returns 0, 1, or 2; the output write stays in onBar().
  • Use descriptive names. isOverbought(rsiValue), percentChange(oldValue, newValue); not calc(a, b) or check(x).
  • Prefer parameters to module state for pure math. A function that takes everything it needs is reusable across indicators by pasting; a function that reaches into module state is tied to this file. Reserve module-state reads for predicates you pass as values.
  • Turn a family of helpers into a class when they share state: a Window with sum(), avg(), and median() (collections.md), a Macd with update() (named-streams.md). Helpers that share nothing stay functions.