Control flow

View as MarkdownOpen the editor

Control flow decides what runs on each bar. A wrun indicator is AssemblyScript, so the constructs are the typed C-style ones: if/else, the ? : ternary, switch, for, while, do/while, break and continue.

Everything here runs inside onBar(), which the host calls once per bar as it walks the loaded history. The bar loop belongs to the host: your code sees one bar per call, and a loop inside it walks memory you own, never history you do not have. Module-level variables are where state lives between calls.

ConstructUse it for
if / elsedecision logic; the condition must be a bool
? :one inline choice; both branches share a type
switchmapping a discrete code (a mode param) to a value or a branch
fora known iteration count: a ring buffer, a cell block, a bounded scan
while, do/whilea condition-driven search with a bound you can prove
break, continueleaving a loop early, skipping an iteration

if / else

Branch on any boolean condition; the else is optional and else if chains as usual. The condition must be a real bool: if (value) on a number is a compile error (AS200), so compare (if (value > 0.0), if (!isNaN(value))).

wrun
if (close > open) {
  direction = 1.0; // up bar
} else if (close < open) {
  direction = -1.0; // down bar
} else {
  direction = 0.0; // doji
}

A value from the last bar, such as the previous close, is a module-level variable the module kept from the last call, not close[1] (Execution model).

Ternary expressions

For a single inline choice, condition ? a : b is cleaner than a full if. Both branches must have the same type: write 1.0 : 0.0 for an f64, not 1 : 0, or the compiler infers an integer and refuses the assignment. It is the idiomatic way to pick a value or to suppress a draw, since NaN written to an output draws nothing:

wrun
// Pick a price based on the bar's direction.
const anchor = close > open ? high : low;
// Write the low only on signal bars; NaN draws nothing (a shape_where gate is the declared form).
const signal = crossed == 1 ? low : NaN;

switch

switch matches an integer against case labels with a default fallback, and falls through without break exactly as JavaScript does. Use it to map a discrete code (a mode param) to a value or a branch.

wrun
switch (mode) {
  case 0:
    picked = close;
    break;
  case 1:
    picked = high;
    break;
  default:
    picked = low;
}

for loops

Use a C-style for when you need a fixed number of iterations: summing a window, scanning buckets, accumulating a value. The form is for (let i = 0; i < end; i++) { ... }: the counter is an i32 (i++ and i += 1 both work), so cast it when it meets an f64. Loop bodies work with locals and with the module's buffers. A StaticArray<f64> sized from a param's max in onStart() or at module start is the usual target, and unchecked(ring[i]) skips the bounds check once the index is provably inside.

wrun
// Sum the last closes by hand, from a ring buffer the module keeps; count is how many slots are filled.
let total = 0.0;
for (let i = 0; i < count; i++) {
  total += unchecked(ring[i]);
}

A celled input's block is the other common loop target: for (let i = 0; i + 3 < n; i += 4) walks [low, high, buy, sell] tuples (Order flow).

There is no history array to loop over (close[n] does not exist): a wrun indicator has no timeseries. A window is a History you push() once per bar and read with ago(n) (Stats, history and lists), or by hand a StaticArray<f64> ring the module fills one bar at a time, sized once from the param's max (Collections). The ring is the window, and it is yours to size and index.

while and do/while

while (condition) { ... } and do { ... } while (condition). Use them when the iteration count depends on what you find, and make the bound explicit: a search back through a ring stops at the ring's size whatever the data says. Here above is a ring of booleans (was the close above its average on that bar) and slot(back) maps "bars back" to a ring index.

wrun
let back = 0;
while (back < count && !unchecked(above[slot(back)])) {
  back += 1;
}

A counted for is clearer and safer when you know the bound. Reach for while only for genuine search-until-found logic, and make sure the condition can end.

break and continue

break leaves the loop; continue skips to the next iteration. Both work in for, while, and do/while, and both are the idiomatic way to stop a scan the moment it has its answer.

Branches in one module

A for loop, an if/else branch, a ternary, and a switch, folded into one value, with a bar counter the module keeps itself (there is no barIndex global). The if/else, ternary, switch and for snippets above appear verbatim inside onBar().

param("mode", 0, { min: 0, max: 2, description: "0 rotate by bar, 1 high, 2 low: which price the switch picks" });
param("window", 5, { min: 2, max: 50, description: "Closes summed by the for loop" });
output("direction", line, lower, { description: "+1 up bar, -1 down bar, 0 doji" });
output("anchor", line, lower, { description: "The high on up bars, the low otherwise" });
output("signal", none, lower, { description: "The low on bullish-cross bars, NaN elsewhere" });
output("total", line, lower, { description: "The last closes summed by hand" });
output("picked", line, lower, { description: "The price the switch picked" });
output("folded", line, lower, { description: "All four results in one number" });

const MAX_WINDOW = 50;
const ring = new StaticArray<f64>(MAX_WINDOW);
let n: i32 = 5;
let cursor: i32 = 0;
let count: i32 = 0;
let modeParam: i32 = 0;
let barIndex: i32 = 0;
let sma = new Sma(5);
const cross = new Cross();

function onStart(): void {
  modeParam = i32(p_mode());
  n = i32(p_window());
  sma = new Sma(n);
}

function onBar(): void {
  const close = bar.close();
  const open = bar.open();
  const high = bar.high();
  const low = bar.low();
  unchecked((ring[cursor] = close));
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  const crossed = cross.update(close, sma.update(close));

  let direction = 0.0;
  if (close > open) {
    direction = 1.0; // up bar
  } else if (close < open) {
    direction = -1.0; // down bar
  } else {
    direction = 0.0; // doji
  }

  // Pick a price based on the bar's direction.
  const anchor = close > open ? high : low;
  // Write the low only on signal bars; NaN draws nothing (a shape_where gate is the declared form).
  const signal = crossed == 1 ? low : NaN;

  // Sum the last closes by hand, from a ring buffer the module keeps; count is how many slots are filled.
  let total = 0.0;
  for (let i = 0; i < count; i++) {
    total += unchecked(ring[i]);
  }

  // A per-bar mode: the param, or the bar index modulo 3 when the param is 0.
  const mode = modeParam == 0 ? barIndex % 3 : modeParam;
  let picked = NaN;
  switch (mode) {
    case 0:
      picked = close;
      break;
    case 1:
      picked = high;
      break;
    default:
      picked = low;
  }

  barIndex += 1;
  out_direction(direction);
  out_anchor(anchor);
  out_signal(signal);
  out_total(total);
  out_picked(picked);
  out_folded(anchor + picked / 100.0 + total);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Loops in one module

A search-until-found: how many bars back the close last traded above its average, found with a while over a ring buffer, plus a for that sums the ring, a for with continue that counts up bars while skipping doji bars, and a do/while with break that finds the first bar back whose range exceeds the current one. The for and while snippets above appear verbatim inside onBar().

param("period", 20, { min: 2, max: 200, description: "Average window and search depth" });
output("direction", line, lower, { color: "#94a3b8", description: "+1 when the close rose, -1 when it fell, 0 flat" });
output("close_sum", line, lower, { color: "#7c3aed", description: "The window's closes summed by the for loop" });
output("bars_since_above", line, lower, { color: "#2563eb", description: "Bars back to the last close above the average (0 = this bar), NaN if none in the window" });
output("bars_above", line, lower, { color: "#16a34a", description: "Up bars in the window, doji bars skipped" });
output("wider_back", line, lower, { color: "#f97316", description: "Bars back to the first bar with a wider range than this one, NaN if none" });

const MAX_PERIOD = 200;
const ring = new StaticArray<f64>(MAX_PERIOD);
const opens = new StaticArray<f64>(MAX_PERIOD);
const ranges = new StaticArray<f64>(MAX_PERIOD);
const above = new StaticArray<bool>(MAX_PERIOD);
let size: i32 = 20;
let sma = new Sma(20);
let cursor: i32 = 0;
let count: i32 = 0;
let prevClose: f64 = NaN;

// The ring slot `back` bars behind the newest write.
function slot(back: i32): i32 {
  return (cursor - 1 - back + size) % size;
}

function onStart(): void {
  size = i32(p_period());
  sma = new Sma(size);
}

function onBar(): void {
  const close = bar.close();
  const open = bar.open();
  const average = sma.update(close);

  // if / else: the bar's direction against the previous close the module kept.
  let direction = 0.0;
  if (close > prevClose) {
    direction = 1.0;
  } else if (close < prevClose) {
    direction = -1.0;
  }
  prevClose = close;

  unchecked((ring[cursor] = close));
  unchecked((opens[cursor] = open));
  unchecked((ranges[cursor] = bar.high() - bar.low()));
  unchecked((above[cursor] = !isNaN(average) && close > average));
  cursor = (cursor + 1) % size;
  if (count < size) count += 1;
  if (isNaN(average)) return;

  // for: sum the filled slots of the ring.
  let total = 0.0;
  for (let i = 0; i < count; i++) {
    total += unchecked(ring[i]);
  }

  // while: search back until a bar above the average is found, or the window runs out.
  let back = 0;
  while (back < count && !unchecked(above[slot(back)])) {
    back += 1;
  }
  const barsSinceAbove = back < count ? f64(back) : NaN;

  // for with continue: count up bars, skipping doji bars.
  let ups = 0;
  for (let i = 0; i < count; i++) {
    const c = unchecked(ring[i]);
    const o = unchecked(opens[i]);
    if (c == o) continue;
    if (c > o) ups += 1;
  }

  // do/while with break: the first bar back whose range exceeds this one.
  const current = unchecked(ranges[slot(0)]);
  let probe = 1;
  let widerBack = NaN;
  if (count > 1) {
    do {
      if (unchecked(ranges[slot(probe)]) > current) {
        widerBack = f64(probe);
        break;
      }
      probe += 1;
    } while (probe < count);
  }

  out_direction(direction);
  out_close_sum(total);
  out_bars_since_above(barsSinceAbove);
  out_bars_above(f64(ups));
  out_wider_back(widerBack);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Every loop here is bounded by count, which is bounded by size, which is bounded by the param's declared max: the module allocates its rings once for MAX_PERIOD and never grows.

Loop limits

A wrun indicator has no iteration counter: the chart bounds the whole evaluation with an execution timeout (20 s per run). A module that overruns it is stopped and its worker replaced, so a runaway loop, or a while whose condition never becomes false, fails the run loudly rather than hanging the chart. In practice a loop bounded by a param's declared max never approaches the timeout; keep loop bounds tied to declared ranges and the module stays cheap on a long history (Execution model).

Loops run once per bar over the loaded history and again each time a live update re-evaluates the forming bar (at most about once a second). A scan of a 200-slot ring is cheap; a scan inside a scan is not, so cache a statistic once per bar and reuse it.

What to keep in mind

  • Per-bar execution. onBar() re-runs on every bar. To carry a value across bars, keep it in a module-level let. A let inside onBar(), a loop or a branch is local to that block and gone when the call returns.
  • Typed branches. Every branch of a ternary and every assignment agrees on a type; f64 values get float literals (0.0, 1.0), counts and indexes are i32.