Type system

View as MarkdownOpen the editor

A comprehensive guide to the wrun type system: static typing, how types are inferred from literals, explicit casts, the per-bar value (there is no series type), and the messages the compiler prints when a type does not fit.

A wrun indicator is AssemblyScript: every value has a fixed width, nothing converts implicitly, and there is no any.

Introduction

The compiler type-checks the whole file before it runs. There are no implicit conversions between numeric widths, no coercion between strings and numbers, and no union types. That strictness is the point: a mismatch is a compile error with a line and column, never a wrong number on bar 1,400.

At its core the system has two categories of values: primitives (f64, i32, bool, string) and references (arrays, maps, class instances). What it does not have is a series type, a value with a whole history behind it; an indicator's central idea is one value per bar, read through a function, with history kept explicitly. This page covers declarations and inference, the primitives, the per-bar model, collections, conversion rules, and a complete example.

Variable declarations and type inference

let and const declare variables, as in TypeScript. The type comes from the annotation if there is one, otherwise from the initializer, and the rule to remember is the literal rule: a whole-number literal is an i32, a literal with a decimal point is an f64.

text
let price = 45000.5;     // f64
let count = 0;           // i32, because the literal has no point
let flag = true;         // bool
let name = "BTCUSDT";    // string

let value: f64 = 0;      // annotated: f64 (the literal widens)
let cursor: i32 = 0;     // annotated: i32

Once inferred, the type is fixed. let count = 0; count = 2.5; is refused with Conversion from type 'f64' to 'i32' requires an explicit cast, which is also the message you get when a param (always f64) meets a class constructor (always i32): write new Sma(i32(p_period())).

const fixes the binding, not the contents: a const window = new StaticArray<f64>(50) is filled and overwritten freely; only window = ... is refused. Module-level TA objects are let, because onStart() rebuilds them at the chosen period.

The three lifetimes (a local, a module-level variable, a value with history) are in core-variables.md; this page is about what the values are.

Primitive types

f64 is the 64-bit float and the type of everything that crosses the host boundary. Arithmetic on two f64 values is an f64; dividing by zero gives Infinity or NaN, never an error, so guard denominators. NaN is the missing value: isNaN(x) tests it, x == x is false when x is NaN, and every comparison with NaN is false.

i32 is the 32-bit integer: periods, counters, cursors, loop bounds, Cross.update()'s answer. Integer division truncates (7 / 2 is 3). i64 exists for epoch-second math (time-and-sessions.md).

bool comes from comparisons and logical operators. A bare number in an if compiles (nonzero is true), but write the comparison out: x > 0.0 says what you mean, and !isNaN(x) is the only reliable test for a missing value.

string is immutable text with length, charCodeAt, indexOf, startsWith, + between strings, and == / < by code unit. Strings stay inside the module and reach the chart only through a declared string slot; building one allocates, so per-bar text goes through the line builder (sb_clear, sb_text, sb_f64). There is no number-to-string coercion: "close " + close is refused with Type 'f64' is not assignable to type 'String'; write close.toString().

The per-bar value

An indicator input is a function: bar.close() returns this bar's close (and in_<name>() a declared input's), inside onBar(). The host calls onBar() once per bar, oldest first, so a line you write once still produces a value at every point in time, but the past is not addressable.

What holds history:

NeedForm
the previous bar's closea module-level prevClose assigned at the end of onBar()
a 20-bar average, a 14-bar RSInew Sma(20), new Rsi(14): the class keeps the window, update(x) folds one value
the highest high of 20 bars, the close 5 bars backa History per series, read with max() and ago(5) (Stats, history and lists); by hand, a StaticArray<f64> ring buffer of 20 with a cursor
a running totala module-level accumulator

Immutability is built in (an input has no setter), and module scope is where history lives: anything that must survive a call is declared there, because a local is gone when onBar() returns.

OHLCV data structure

A candle is not one value with six columns. The chart's own candle is read one field at a time through bar, with no declaration:

text
bar.open();
bar.high();
bar.low();
bar.close();
bar.volume();
bar.time();   // the timestamp, epoch seconds

A file with no input line runs on the chart's own candles. When a file declares inputs (another feed, a pinned market or interval), the first input is the primary: it defines the grid (market and interval) every other input aligns to (Data sources). There is no priceIndex: a class takes whichever number you hand it, so sma.update(bar.close()) and sma.update((bar.high() + bar.low()) / 2.0) are the same call over different sources.

Arrays and collections

Arrays are typed and homogeneous. StaticArray<f64> is a fixed-size block allocated once; Array<f64> grows with push; f64[] and string[] literals work; Map<K, V> is a key-value store. The element type is fixed at declaration and enforced: pushing a string onto an Array<f64> is refused with Type 'String' is not assignable to type 'f64'. Arrays are references: passing one to a function passes the same storage, so a function that writes into it writes into yours. The allocation rule applies to all of them: build them in onStart() or at module scope, never per bar (collections.md).

text
const periods: i32[] = [10, 20, 50];
const window = new StaticArray<f64>(200);
const weights = new Map<string, f64>();

Data sources

An indicator reads every feed through one form, input(name, source.field, options), where source is a source namespace and field one of its members (the chart's own candle needs no declaration: bar.close() and the other bar fields read it); every input is an f64, and the source decides what the number means. The catalog, the missing policy for sparse feeds, and the named refusals for feeds a host cannot fetch are in data-sources.md.

Type conversion and casting

Nothing converts on its own. The rules:

  • f64 to i32: i32(x) truncates toward zero. Periods, cursors, and loop bounds derived from a param need it.
  • i32 to f64: f64(n). Writing a counter to an output needs it, and so does mixing an i32 into float arithmetic.
  • bool to a number: a ternary, flag ? 1.0 : 0.0.
  • A number to a string: .toString(), and only for module-internal text.
  • Never implicit: f64 * i32 is refused; cast one side.
text
const period = i32(p_period());          // f64 to i32
out_count(f64(hits));                    // i32 to f64
out_is_long(fast > slow ? 1.0 : 0.0);    // bool to f64
const label = "n=" + hits.toString();    // i32 to string, inside the module

Type mismatch errors name the rule. The ones you will meet:

MessageCauseFix
AS200: Conversion from type 'f64' to 'i32' requires an explicit cast.a param or float where an integer goesi32(...)
TS2322: Type 'bool' is not assignable to type 'i32'.a comparator returning a comparisonreturn a > b ? 1 : a < b ? -1 : 0
TS2322: Type 'String' is not assignable to type 'f64'.a string pushed onto a numeric arraykeep the array homogeneous
TS2329: Index signature is missing in type 'f64'.close[1] on a numberkeep the previous value in a variable
TS1110: Type expected.a function without a return typeannotate: function f(x: f64): f64
AS100: Not implemented: Closuresan arrow function reading a local of the enclosing functionread module-level state instead (lambdas-and-reducers.md)
AS100: Not implemented: Iteratorsfor (const x of xs)an index loop

Best practices

Annotate every module-level let and every function signature (the annotation is documentation, and it stops the literal rule from making a float an integer); feed inputs straight into the classes that keep the windows; keep params, inputs, and outputs at the top as top-level statements, state under them, onStart() and onBar() last.

Complete example: multi-source momentum

A momentum indicator over three sources: the close for an RSI, the funding rate averaged over 24 bars, and liquidations (sparse, so missing: "zero" makes a quiet bar a 0). The extreme condition is a data-only gate and a mark drawn only where it holds.

// Configuration: the RSI length, the RSI level, and the funding level.
param("rsi_period", 14, { min: 2, max: 100, description: "RSI period" });
param("extreme_rsi", 30, { min: 1, max: 99, description: "RSI at or below this is oversold" });
param("funding_threshold", 0.01, { min: 0, max: 100, description: "Funding below minus this means shorts are paying, in the feed's own units" });
// Data sources: price, funding, liquidations.
input("close", ohlcv.close);
input("funding", funding.rate_close, { description: "Funding rate at the bar's close" });
input("liqs", liquidations.liquidations, { missing: "zero", description: "Liquidation volume, 0 on quiet bars" });
// Visualization: the RSI and its level in one pane, a mark only on extreme bars.
output("rsi", line, lower, { color: "#2962FF", width: 2, description: "Relative Strength Index" });
output("extreme_rsi", line, lower, { color: "#FF6B35", width: 1, description: "Extreme RSI level" });
output("extreme", shape, lower, { color: "#00BA88", shape_where: "is_extreme", description: "Oversold, shorts paying, liquidations printing" });
output("is_extreme", none, lower);

let rsi = new Rsi(14);
let avgFunding = new Sma(24); // 24 bars of funding, one day on a 1h chart
let liqAvg = new Sma(6); // six bars of liquidation volume
let extremeLevel: f64 = 30.0;
let fundingThreshold: f64 = 0.01;

function onStart(): void {
  rsi = new Rsi(i32(p_rsi_period()));
  extremeLevel = p_extreme_rsi();
  fundingThreshold = p_funding_threshold();
}

function onBar(): void {
  const rsiValue = rsi.update(bar.close()); // f64 in, f64 out
  const fundingAvg = avgFunding.update(in_funding());
  const liqRecent = liqAvg.update(in_liqs());
  // Three bools, combined; NaN on any side makes its comparison false, so a warming source never fires.
  const oversold: bool = rsiValue <= extremeLevel;
  const shortsPaying: bool = fundingAvg < -fundingThreshold;
  const liquidating: bool = liqRecent > 0.0;
  const extreme: bool = oversold && shortsPaying && liquidating;
  if (isNaN(rsiValue)) return;
  out_rsi(rsiValue);
  out_extreme_rsi(extremeLevel);
  out_extreme(rsiValue); // the mark sits on the RSI line
  out_is_extreme(extreme ? 1.0 : 0.0); // bool to f64
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Watch the types flow: three f64 inputs feed three classes that return f64, those numbers feed comparisons that return bool, the bools combine into one bool, and the ternary turns it into the f64 gate output that the shape output reads through shape_where. Nothing converts on its own, and every step is checked before the first bar runs.