Multi-output indicators like Bollinger bands, MACD, and the stochastic
produce several lines at once. In kScript (legacy) they returned one
result whose components you read by name: bb.upper, macd.signal,
stoch.k. In an Indicator each stream is an output of its own, and the
computation behind them is a class whose fields you read by name after one
update(). No positional guessing, and every stream is a metric id.
Introduction
Some indicators produce more than one line. Bollinger bands give you an upper band, a basis, and a lower band. MACD gives a MACD line, a signal line, and a histogram. An Indicator declares one output per line, and a class computes all of them from one input per bar:
output("bb_upper", line, overlay, { color: "#2563eb" });
output("bb_basis", line, overlay, { color: "#64748b" });
output("bb_lower", line, overlay, { color: "#2563eb" });
const bands = new Bb(20, 2.0); // in state(): bands.update(close)
out_bb_upper(bands.upper); // in finalize(): read the fields by name
out_bb_basis(bands.basis);
out_bb_lower(bands.lower);bands.update(close) folds one bar; bands.upper, bands.basis, and
bands.lower are its three lines afterwards. You name what you want. There
is no "is the upper band index 0 or index 2?" guesswork, and a typo like
bands.upperr is a compile error rather than a silently wrong line.
How it works
A multi-output computation is a class with one update(x) method and one
typed field per stream. update returns the primary stream (the MACD line,
the basis) so the class also reads like a single-output one, and the other
streams sit on the instance. Each field is an ordinary f64, so it flows
straight into math, other classes, conditions, and outputs:
const m = new Macd(12, 26, 9);
m.update(close);
const risingMomentum = m.hist > prevHist; // compare bars (prevHist is remembered)
const smoothSignal = signalEma.update(m.signal); // feed a stream into another classThe kit ships every class, single-stream and multi-stream alike, in
src/sdk/ta.ts (import { Bb, Macd } from "./sdk/ta";), each one
matching its kScript builtin bar for bar. Three rules keep a module
honest: construct in init() (allocation once), update() once per bar,
and a reset() that calls .reset() on every class, because the host's
replay of the forming bar calls the module's reset() and expects every
stream back at its start.
The named streams
The multi-output builtins kScript exposed, and the class and fields each one becomes:
| kScript builtin | Class | Fields |
|---|---|---|
bb | Bb | .basis, .upper, .lower |
keltner | Keltner | .basis, .upper, .lower |
donchian | Donchian | .basis, .upper, .lower |
macd | Macd | .macd, .signal, .hist |
stoch, stochastic | Stoch, Stochastic | .k, .d |
supertrend | Supertrend | .line, .direction |
adx | Adx | .adx, .plusDi, .minusDi |
ichimoku | Ichimoku | .tenkan, .kijun, .senkouA, .senkouB, .chikou |
Every class and its conventions: TA library.
A few worth calling out:
Stochsplits into the fast%K(.k) and its smoothed%D(.d). The classic crossover isCross.update(stoch.k, stoch.d).Supertrendcarries the trailing stop level as.lineand the trend side as.direction(1or-1). Use.directionas acolor_byindex or a gate; draw.line.Macd.histis the MACD line minus the signal line, ready for ahistogramoutput.
Reading every stream at once
Two shipped classes and all six of their streams as outputs. Macd is
the fast, slow and signal EMAs folded into one object; Bb is the window
mean and its population standard deviation. Every stream is a metric once
the package is installed
(wrun/@you/macd-bb/signal, wrun/@you/macd-bb/bb_upper), so an alert
can cross any of them.
import { histogram, input, line, lower, ohlcv, output, overlay, param } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_bb_basis, out_bb_lower, out_bb_upper, out_hist, out_macd, out_signal } from "./gen/outputs";
import { p_bb_mult, p_bb_period, p_fast, p_signal, p_slow } from "./gen/params";
import { Bb, Macd } from "./sdk/ta";
param("fast", 12, { min: 1, max: 200, description: "MACD fast EMA" });
param("slow", 26, { min: 2, max: 400, description: "MACD slow EMA" });
param("signal", 9, { min: 1, max: 200, description: "MACD signal EMA" });
param("bb_period", 20, { min: 2, max: 400, description: "Bollinger window" });
param("bb_mult", 2, { min: 0.5, max: 4, description: "Bollinger width in standard deviations" });
input("close", ohlcv.close);
output("bb_upper", line, overlay, { color: "#2563eb", width: 2, description: "Upper Bollinger band" });
output("bb_basis", line, overlay, { color: "#64748b", width: 2, description: "Bollinger basis (the moving average)" });
output("bb_lower", line, overlay, { color: "#dc2626", width: 2, description: "Lower Bollinger band" });
output("macd", line, lower, { color: "#1d4ed8", width: 2, description: "MACD line" });
output("signal", line, lower, { color: "#ea580c", width: 2, description: "Signal line" });
output("hist", histogram, lower, { color: "#15803d", description: "MACD minus signal" });
// One update() per bar; the streams are fields you read by name afterwards.
let macd = new Macd(12, 26, 9);
let bands = new Bb(20, 2.0);
export function init(): void {
macd = new Macd(i32(p_fast()), i32(p_slow()), i32(p_signal()));
bands = new Bb(i32(p_bb_period()), p_bb_mult());
}
export function state(): i32 {
const close = in_close();
macd.update(close);
bands.update(close);
// The row is ready once the slowest stream is: the MACD signal line.
return isNaN(macd.signal) || isNaN(bands.basis) ? 0 : 1;
}
export function finalize(): void {
out_bb_upper(bands.upper);
out_bb_basis(bands.basis);
out_bb_lower(bands.lower);
out_macd(macd.macd);
out_signal(macd.signal);
out_hist(macd.hist);
emitRow();
}
export function reset(): void {
macd.reset();
bands.reset();
}What to expect: the three bands start drawing once the 20-bar window
fills and track together. The MACD streams warm later: the MACD line needs
the slow EMA (26 bars), the signal line nine MACD values beyond that, so
the row abstains until the 34th bar and all six lines then draw in
lockstep.
Bars where one stream is finite and another is not are handled in the
classes (NaN stays NaN), never in finalize().
A composite class as one value
kScript let you keep the whole result in one var and pull fields as
needed. The class instance is that value: pass macd to a helper that
takes a Macd, keep an array of them for several settings, or read a
single field inline. What you cannot do is index a stream's history
(m.hist[1]): remember the previous value in a module-level variable,
exactly as for any other number (core-variables.md).
Single-output classes like Rsi, Ema, and Sma return their one value
from update() and have no stream fields, so you write them straight to an
output: out_rsi(rsi.update(close)). Named streams are only for the
multi-line indicators above. The full catalog is the
TA library.