User-defined types

Define your own typed classes with fields and methods, construct them, call methods, and mutate state through this. Model trading state in the shape of the…

Define your own typed classes with fields and methods, construct them, call methods, and mutate state through this. Model trading state in the shape of the problem. kScript (legacy) v3 called these type structs with .new(...) named-field construction; an Indicator uses an AssemblyScript class, with typed fields, a constructor, and the same methods, allocated once and reset in place.

Introduction

A class is a struct you define: a named bundle of typed fields, optionally with methods that operate on those fields. Instead of tracking a supply zone as three loose variables (zoneTop, zoneBottom, zoneTouches), you describe it once as a Zone and work with whole zones.

class Zone {
  top: f64 = NaN;
  bottom: f64 = NaN;
  touches: i32 = 0;
}

Each field is declared with a name, a type, and (best practice) an initializer. Once a class exists, you create instances of it, read and write their fields, and pass them around like any other value.

Construct with a constructor

You build an instance with new, and a constructor sets the fields from its arguments:

class Zone {
  top: f64;
  bottom: f64;
  touches: i32 = 0;

  constructor(top: f64, bottom: f64) {
    this.top = top;
    this.bottom = bottom;
  }
}

const z = new Zone(high, low);
const h = z.top - z.bottom;

Arguments are positional, so name the parameters well and keep the order obvious (top before bottom, open before close). A field the class does not declare cannot be set: z.label = ... is a compile error, so a typo is caught before the file runs. A field left without an initializer or a constructor assignment starts at zero (or null for a reference); give numeric fields an explicit NaN when "not set yet" must be distinguishable from 0.

Add behavior with methods

A class can carry methods. Declare them inside the class, and they read and mutate the instance through this:

class Zone {
  top: f64;
  bottom: f64;
  touches: i32 = 0;

  constructor(top: f64, bottom: f64) {
    this.top = top;
    this.bottom = bottom;
  }

  height(): f64 {
    return this.top - this.bottom;
  }

  registerTouch(price: f64): bool {
    if (price >= this.bottom && price <= this.top) {
      this.touches += 1;
      return true;
    }
    return false;
  }
}

Now the data and the logic that maintains it live together. height() derives a value from the fields. registerTouch(price) is a self-updating operation: ask the zone whether the current price touched it, and it answers while bumping its own touches counter. The zone manages its own state.

const inZone = z.registerTouch(close);   // true or false, and z.touches updates itself
const tall = z.height();

This is why classes matter for Indicators. A "supply zone that counts its own touches and retires after the third," an "order block that tracks whether price mitigated it," a "trailing-stop level that ratchets": each becomes one class whose methods enforce its rules, instead of bookkeeping smeared across the whole file. Hold one in a module-level variable to track one, or in a preallocated StaticArray<Zone> to manage a fixed set of them across bars (collections.md).

Two rules a class must follow here

Allocate once. new Zone(...) takes memory the module never gives back, so a class instance is built at module scope or in init(), never per bar. When a zone is replaced, mutate the instance you have (a set(top, bottom) method) rather than constructing a new one. A fixed set of zones is a StaticArray<Zone> filled in init(); a slot that may be empty is a StaticArray<Zone | null> and is tested with !== null before use.

Give it a reset(). The host replays the forming bar into a snapshot of the module, and the module's reset() must put every piece of state back. A class that carries state across bars gets its own reset() method, and the module's reset() calls it. A field it forgets is a stale value the replay reads (repainting.md).

A complete typed class

This file declares Zone with both methods and a set(), keeps one instance at module scope, re-anchors it to the previous bar's candle body whenever price leaves it, and lets the methods mutate touches through this. Three outputs show the zone's height, its running touch count, and whether this bar touched it.

import { input, line, lower, none, ohlcv, output, overlay, param, shape } from "./sdk/declare";
import { in_close, in_high, in_low, in_open } from "./gen/inputs";
import { emitRow, out_height, out_touch, out_touched, out_touches, out_zone_bottom, out_zone_top } from "./gen/outputs";
import { p_max_touches } from "./gen/params";

param("max_touches", 3, { min: 1, max: 20, description: "Touches after which the zone retires and re-anchors" });
input("open", ohlcv.open);
input("high", ohlcv.high);
input("low", ohlcv.low);
input("close", ohlcv.close);
output("zone_top", line, overlay, { color: "#7c3aed", width: 1, description: "Top of the tracked zone" });
output("zone_bottom", line, overlay, { color: "#7c3aed", width: 1, description: "Bottom of the tracked zone" });
output("height", line, lower, { color: "#2563eb", description: "Zone height (a method over the fields)" });
output("touches", line, lower, { color: "#16a34a", description: "Touches registered on the current zone" });
output("touch", shape, overlay, { color: "#16a34a", shape_where: "touched", description: "This bar's close touched the zone" });
output("touched", none);

class Zone {
  top: f64 = NaN;
  bottom: f64 = NaN;
  touches: i32 = 0;

  set(top: f64, bottom: f64): void {
    this.top = top > bottom ? top : bottom;
    this.bottom = top > bottom ? bottom : top;
    this.touches = 0;
  }

  height(): f64 {
    return this.top - this.bottom;
  }

  contains(price: f64): bool {
    return price >= this.bottom && price <= this.top;
  }

  registerTouch(price: f64): bool {
    if (this.contains(price)) {
      this.touches += 1;
      return true;
    }
    return false;
  }

  reset(): void {
    this.top = NaN;
    this.bottom = NaN;
    this.touches = 0;
  }
}

const zone = new Zone(); // one instance, allocated once at module scope
let maxTouches: i32 = 3;
let prevOpen: f64 = NaN;
let prevClose: f64 = NaN;
let close: f64 = NaN;
let touched: bool = false;

export function init(): void {
  maxTouches = i32(p_max_touches());
}

export function state(): i32 {
  const open = in_open();
  close = in_close();
  in_high();
  in_low();
  // Anchor the zone to the previous bar's body when there is none yet or the current one has retired.
  if (isNaN(zone.top) && !isNaN(prevOpen)) zone.set(prevOpen, prevClose);
  touched = !isNaN(zone.top) && zone.registerTouch(close);
  if (zone.touches >= maxTouches) zone.set(open, close); // retire: re-anchor to this bar's body
  prevOpen = open;
  prevClose = close;
  return isNaN(zone.top) ? 0 : 1;
}

export function finalize(): void {
  out_zone_top(zone.top);
  out_zone_bottom(zone.bottom);
  out_height(zone.height());
  out_touches(f64(zone.touches));
  out_touch(close);
  out_touched(touched ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  zone.reset();
  prevOpen = NaN;
  prevClose = NaN;
  close = NaN;
  touched = false;
}

What to expect: zone.height() returns the band width, registerTouch(close) reports whether the close fell inside the band and increments zone.touches when it did, and after max_touches touches the zone re-anchors to the current bar's body. The whole zone is one value carrying its own data and behavior, and zone.reset() is one call in the module's reset().

Rules and gotchas

A few constraints the compiler enforces, each at compile time with a precise line and column.

Fields are typed. top: f64; is required; an untyped field is a parse error (Type expected.), and assigning an i32 literal to an f64 field needs the point (0.0) or a cast.

Constructors are positional. There is no Zone.new(top=..., bottom=...); new Zone(top, bottom) takes the arguments in the constructor's order. A class without a constructor is built with new Zone() and its initializers.

this only works inside methods. It refers to the current instance and is meaningless at module scope or inside a free function.

Unknown fields are refused. z.label on a class without label fails with Property 'label' does not exist on type 'src/indicator/Zone' (the compiler prints a class by its file path).

A class is a reference. Assigning an instance to another variable aliases it; two zones need two instances, allocated once each.

Nullable slots need a test. A StaticArray<Zone | null> element is read into a local and tested with !== null before its fields are touched; using it as a Zone without the test fails with Type 'src/indicator/Zone | null' is not assignable to type 'src/indicator/Zone'.

Classes are part of the broader type system: type-system.md covers how f64, i32, bool, and string fields fit together, and collections.md covers arrays of instances.