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.