Control flow

Control flow decides what runs on each bar. An Indicator is AssemblyScript, so the toolkit is the familiar typed C-style one: if/else branches, for loops, the…

Control flow decides what runs on each bar. An Indicator is AssemblyScript, so the toolkit is the familiar typed C-style one: if/else branches, for loops, the ? : ternary, and switch. Everything here runs inside state() (or finalize()), which the host calls once per bar as it walks the loaded history; that is the whole per-bar execution model, and it is why a kScript (legacy) script and an Indicator read almost the same in their bodies while differing in where the state lives.

if / else

Branch on any boolean condition; the else is optional. Conditions are real bool values: if (value) on a number is a compile error (AS200), so compare (if (close > open), if (!isNaN(value))).

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

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:

// 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;

for loops

Use a C-style for when you need a fixed number of iterations: summing a window, scanning buckets, accumulating a value. The loop index is an i32; cast it when it meets an f64.

// 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]);
}

There is no history array to loop over (close[n] does not exist): a window is a StaticArray<f64> ring the module fills one bar at a time, sized once from the param's max (Collections).

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.

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

Everything together

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 four snippets above appear verbatim inside state().

import { input, line, lower, none, ohlcv, output, param } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_anchor, out_direction, out_folded, out_picked, out_signal, out_total } from "./gen/outputs";
import { p_mode, p_window } from "./gen/params";
import { Cross, Sma } from "./sdk/ta";

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" });
input("close", ohlcv.close);
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
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();
let direction: f64 = 0.0;
let anchorValue: f64 = NaN;
let signalValue: f64 = NaN;
let totalValue: f64 = 0.0;
let picked: f64 = NaN;

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

export function state(): i32 {
  const close = in_close();
  const open = in_open();
  const high = in_high();
  const low = in_low();
  unchecked((ring[cursor] = close));
  cursor = (cursor + 1) % n;
  if (count < n) count += 1;
  const crossed = cross.update(close, sma.update(close));

  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;
  switch (mode) {
    case 0:
      picked = close;
      break;
    case 1:
      picked = high;
      break;
    default:
      picked = low;
  }

  anchorValue = anchor;
  signalValue = signal;
  totalValue = total;
  barIndex += 1;
  return 1;
}

export function finalize(): void {
  out_direction(direction);
  out_anchor(anchorValue);
  out_signal(signalValue);
  out_total(totalValue);
  out_picked(picked);
  out_folded(anchorValue + picked / 100.0 + totalValue);
  emitRow();
}

export function reset(): void {
  cursor = 0;
  count = 0;
  barIndex = 0;
  sma.reset();
  cross.reset();
  direction = 0.0;
  anchorValue = NaN;
  signalValue = NaN;
  totalValue = 0.0;
  picked = NaN;
}

Loop limits

kScript capped a script at one million loop iterations. An Indicator has no iteration counter: the host bounds the WHOLE evaluation with an execution timeout, and a module that overruns it is stopped and its worker discarded, so a runaway loop fails the run rather than hanging the chart. In practice a loop bounded by a param's declared max never approaches either limit; keep loop bounds tied to declared ranges and the module stays cheap on a long history (Execution model).

What to keep in mind

  • Per-bar execution. state() re-runs on every bar. To carry a value across bars, keep it in a module-level let (the port of persist); a local declared inside state() is gone when the call returns, the port of a plain var.
  • while exists but use it sparingly. A counted for is clearer and safer when you know the bound. Reach for while only for genuine search-until-found logic (Loops), and make sure the condition can end.
  • 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.
  • reset() restores what the branches touched. The host replays the forming bar through reset(); a module-level variable a branch set on a previous tick and reset() forgot is a stale read.