Conditionals and loops

Conditionals and loops control which code runs during each bar's state() call. Use them for decision logic, counted iteration over a buffer, and conditional…

Conditionals and loops control which code runs during each bar's state() call. Use them for decision logic, counted iteration over a buffer, and conditional searches, keeping in mind that the bar loop itself 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. The kScript (legacy) constructs carry over as AssemblyScript's: if/else, for, while, do/while, break, and continue.

ConstructUse it for
if / elsedecision logic; the condition must be a bool
fora known iteration count: a ring buffer, a cell block, a bounded scan
whilea condition-driven search with a bound you can prove
break, continueleaving a loop early, skipping an iteration

if / else

if (condition) { ... } else { ... }. The condition is a boolean expression; a number is not one, so if (value) is refused and if (value > 0.0) or if (!isNaN(value)) is the form. else if chains as usual.

let direction = 0.0;
if (close > prevClose) {
  direction = 1.0;
} else if (close < prevClose) {
  direction = -1.0;
}

The previous close is a module-level variable the module kept from the last call, not close[1] (Execution model).

for

for (let i = 0; i < end; i++) { ... }. The counter is an i32; i++ and i += 1 both work. Loop bodies work with locals and with the module's buffers; a StaticArray<f64> sized from a param's max in init() or at module start is the usual target, and unchecked(ring[i]) skips the bounds check once the index is provably inside.

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 (Volume profile).

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.

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

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.

Every construct 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 that finds the first bar back whose range exceeds the current one. The three snippets above appear verbatim inside state().

import { input, line, lower, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_bars_above, out_bars_since_above, out_close_sum, out_direction, out_wider_back } from "./gen/outputs";
import { p_period } from "./gen/params";
import { Sma } from "./sdk/ta";

param("period", 20, { min: 2, max: 200, description: "Average window and search depth" });
input("close", ohlcv.close);
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
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;
let directionValue: f64 = 0.0;
let closeSum: f64 = NaN;
let barsSinceAbove: f64 = NaN;
let barsAbove: f64 = 0.0;
let widerBack: f64 = NaN;

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

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

export function state(): i32 {
  const close = in_close();
  const open = in_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] = in_high() - in_low()));
  unchecked((above[cursor] = !isNaN(average) && close > average));
  cursor = (cursor + 1) % size;
  if (count < size) count += 1;
  directionValue = direction;
  if (isNaN(average)) return 0;

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

  // 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;
  }
  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;
  }
  barsAbove = f64(ups);

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

export function finalize(): void {
  out_direction(directionValue);
  out_close_sum(closeSum);
  out_bars_since_above(barsSinceAbove);
  out_bars_above(barsAbove);
  out_wider_back(widerBack);
  emitRow();
}

export function reset(): void {
  sma.reset();
  cursor = 0;
  count = 0;
  prevClose = NaN;
  directionValue = 0.0;
  closeSum = NaN;
  barsSinceAbove = NaN;
  barsAbove = 0.0;
  widerBack = NaN;
}

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.

Tips

  • Infinite loop protection. Make sure a while condition can become false; the host stops a module that overruns its execution timeout and discards the worker, so a stuck loop fails the run loudly rather than hanging the chart.
  • Variable scope. A let inside a loop or a branch is local to that block and gone when state() returns; module-level variables are the ones that persist across bars, and reset() must restore them.
  • Performance. Loops run once per bar over the loaded history and again on every tick of the forming bar. 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.
  • No history array. kScript forbade a timeseries inside a loop; an Indicator has no timeseries at all. The ring buffer is the window, and it is yours to size and index.