Type system
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.
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: i32Once 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:
| Need | Form |
|---|---|
| the previous bar's close | a module-level prevClose assigned at the end of onBar() |
| a 20-bar average, a 14-bar RSI | new 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 back | a History per series, read with max() and ago(5) (Stats, history and lists); by hand, a Static ring buffer of 20 with a cursor |
| a running total | a 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:
bar.open();
bar.high();
bar.low();
bar.close();
bar.volume();
bar.time(); // the timestamp, epoch secondsA 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).
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:
f64toi32:i32(x)truncates toward zero. Periods, cursors, and loop bounds derived from a param need it.i32tof64:f64(n). Writing a counter to an output needs it, and so does mixing ani32into float arithmetic.boolto a number: a ternary,flag ? 1.0 : 0.0.- A number to a string:
.toString(), and only for module-internal text. - Never implicit:
f64 * i32is refused; cast one side.
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 moduleType mismatch errors name the rule. The ones you will meet:
| Message | Cause | Fix |
|---|---|---|
AS200: Conversion from type 'f64' to 'i32' requires an explicit cast. | a param or float where an integer goes | i32(...) |
TS2322: Type 'bool' is not assignable to type 'i32'. | a comparator returning a comparison | return a > b ? 1 : a < b ? -1 : 0 |
TS2322: Type 'String' is not assignable to type 'f64'. | a string pushed onto a numeric array | keep the array homogeneous |
TS2329: Index signature is missing in type 'f64'. | close[1] on a number | keep the previous value in a variable |
TS1110: Type expected. | a function without a return type | annotate: function f(x: f64): f64 |
AS100: Not implemented: Closures | an arrow function reading a local of the enclosing function | read module-level state instead (lambdas-and-reducers.) |
AS100: Not implemented: Iterators | for (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
}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.