Strings in an Indicator live in string slots: byte-capped channels written
once per ready bar in finalize() and read by text renderers and drawing
labels. They never enter the module (params are numbers, inputs are
numbers) and they never become metrics. On the way in, AssemblyScript's
String has the JavaScript-style methods kScript (legacy) offered; on the
way out, the generated sb_* builder formats numbers into a slot without
allocating. This page is both halves and the one gotcha the declaration
name causes.
Where strings go
Declare a slot, then send text to it from finalize():
string("note", { max_bytes: 64 });
render.text("note_mark", { y: "average", text: "note" });The generated ./gen/strings module gives you, per slot, str_note(s)
(encode and send a whole string) and str_note_sb() (send the shared
line buffer), plus the builder that fills that buffer:
| Builder | Appends |
|---|---|
sb_clear() | nothing: starts a new line (call it first) |
sb_text(s) | a string's UTF-8 |
sb_int(n) | an integer (i64) |
sb_f64(x, decimals) | a number in fixed point with decimals (0..9) fraction digits, rounded half up; NaN, Inf, -Inf spelled out |
A line over the slot's max_bytes is refused host-side by name (strings
are never truncated); a slot not written that bar is ABSENT, which is
distinct from a written empty string, and the difference is what gates
render.text (an absent slot draws nothing). The senders trap by phase
outside finalize().
The string name
The declaration is called string, the same word as the type. In a file
that imports it, let label: string = ""; fails to compile (string is
now a function). Two ways out: let the type be inferred (let label = "";), or alias the import, which the extractor still recognizes:
import { string as slot } from "./sdk/declare";
slot("note", { max_bytes: 64 });
let label: string = "";Available methods
AssemblyScript's String matches the kScript table method for method.
They work on string values in the module, in init() or per bar, and
the result goes out through str_<slot>(s).
| Method | Signature | Description |
|---|---|---|
split | s.split(separator) | splits into an array of substrings (string[]) |
concat | s.concat(other) | joins two strings; + does the same |
substring | s.substring(start, end?) | the characters between two indexes |
toUpperCase | s.toUpperCase() | uppercase copy |
toLowerCase | s.toLowerCase() | lowercase copy |
trim | s.trim() | whitespace removed from both ends (trimStart, trimEnd too) |
replace | s.replace(search, replaceWith) | the first occurrence replaced (replaceAll for every one) |
indexOf | s.indexOf(search) | the index of the first occurrence, -1 if absent |
startsWith, endsWith | s.startsWith(prefix) | prefix and suffix tests |
includes | s.includes(search) | containment test |
length | s.length | the number of UTF-16 units; a property, not a call |
charCodeAt | s.charCodeAt(i) | the code unit at i |
padStart, padEnd | s.padStart(width, fill) | padding to a width |
Numbers do not stringify themselves cheaply: sb_f64 and sb_int are
the way to put a number in a line, and the builder is where per-bar text
belongs.
Allocation
Every String method that returns a new string allocates it, and the
module's runtime never frees: the sandbox compiles with a bump allocator
and a 4 MiB memory ceiling, and a module that grows memory after init()
is refused. Per-bar String work therefore accumulates for the whole
history. The sb_* builder is allocation-free by design (one shared
buffer sized to the largest slot at module start), so the rule is:
String methods for one-time work in init() or at module start (a
label, a market name, a template), sb_* for everything that happens
per bar.
Every method in one module
A label assembled once in init() with the String methods, and per-bar
text built with the sb_* builder into a mark and a live tag.
import { input, line, none, ohlcv, output, overlay, param, render, string as slot, time } from "./sdk/declare";
import { in_bar_t, in_close } from "./gen/inputs";
import { emitRow, out_average, out_bar_time, out_stretched } from "./gen/outputs";
import { p_period } from "./gen/params";
import { sb_clear, sb_f64, sb_int, sb_text, str_readout_sb, str_tag_sb } from "./gen/strings";
import { Sma } from "./sdk/ta";
param("period", 20, { min: 1, max: 200 });
input("close", ohlcv.close);
input("bar_t", time.bar_open_sec);
output("average", line, overlay, { color: "#38bdf8", width: 2, description: "Simple average" });
output("bar_time", none, overlay, { description: "Bar open in epoch seconds" });
output("stretched", none, overlay, { description: "1 while the close is over two percent from the average" });
slot("readout", { max_bytes: 48 });
slot("tag", { max_bytes: 48 });
render.text("stretch_mark", { y: "average", text: "readout", size: 10 });
render.label("average_tag", { x: "bar_time", y: "average", text: "tag", size: 11 });
let sma = new Sma(20);
let label: string = "";
let period: i32 = 20;
let value: f64 = NaN;
let close: f64 = NaN;
let barTime: f64 = NaN;
let barIndex: i32 = 0;
export function init(): void {
period = i32(p_period());
sma = new Sma(period);
// One-time string work: every method from the table, on a market label.
const raw = " btc-usdt:perp ";
const parts = raw.trim().split(":");
let name = parts[0].toUpperCase().replace("-", "/");
if (name.startsWith("BTC") && name.endsWith("USDT") && name.indexOf("/") == 3 && name.includes("USD")) {
name = name.concat(" ").concat(parts[1].toLowerCase());
}
label = name.substring(0, name.length).padEnd(12, ".");
}
export function state(): i32 {
close = in_close();
barTime = in_bar_t();
value = sma.update(close);
barIndex += 1;
return isNaN(value) ? 0 : 1;
}
export function finalize(): void {
const stretch = ((close - value) / value) * 100.0;
const stretched = Math.abs(stretch) >= 2.0;
out_average(value);
out_bar_time(barTime);
out_stretched(stretched ? 1.0 : 0.0);
if (stretched) {
// Per-bar text: the builder, no allocation.
sb_clear();
sb_f64(stretch, 1);
sb_text("% at bar ");
sb_int(barIndex);
str_readout_sb();
}
sb_clear();
sb_text(label);
sb_text(" SMA ");
sb_f64(value, 2);
str_tag_sb();
emitRow();
}
export function reset(): void {
sma.reset();
value = NaN;
close = NaN;
barTime = NaN;
barIndex = 0;
}label is built once and reused on every bar; reset() leaves it alone
because init() owns it. The readout is written only on stretched bars,
so the mark appears only there.
Caps
Per slot max_bytes is at most 4096; 64 slots per package; 64 KiB of
string bytes per row; 8 MiB per run; each a named refusal
(Limits).