Named streams

Multi-output indicators like Bollinger bands, MACD, and the stochastic produce several lines at once. In kScript (legacy) they returned one result whose…

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 class

The 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 builtinClassFields
bbBb.basis, .upper, .lower
keltnerKeltner.basis, .upper, .lower
donchianDonchian.basis, .upper, .lower
macdMacd.macd, .signal, .hist
stoch, stochasticStoch, Stochastic.k, .d
supertrendSupertrend.line, .direction
adxAdx.adx, .plusDi, .minusDi
ichimokuIchimoku.tenkan, .kijun, .senkouA, .senkouB, .chikou

Every class and its conventions: TA library.

A few worth calling out:

  • Stoch splits into the fast %K (.k) and its smoothed %D (.d). The classic crossover is Cross.update(stoch.k, stoch.d).
  • Supertrend carries the trailing stop level as .line and the trend side as .direction (1 or -1). Use .direction as a color_by index or a gate; draw .line.
  • Macd.hist is the MACD line minus the signal line, ready for a histogram output.

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.