Named streams

View as MarkdownOpen the editor

Multi-output indicators like Bollinger bands, MACD, and the stochastic produce several lines at once.

In a wrun 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(): bands.upper, macd.signal, stoch.k. No positional guessing: every stream is read by name.

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. A wrun indicator declares one output per line, and a class computes all of them from one input per bar:

text
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 onBar(): bands.update(close)
out_bb_upper(bands.upper);              // then 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

Named streams: one Bb object, updated once per bar, holds three fields; the file writes each to an output of its own, and the chart draws three lines: upper, basis and lower.let bands = new Bb(20, 2);bands.update(bar.close());one update per barout_upper(bands.upper);out_basis(bands.basis);out_lower(bands.lower);

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:

text
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, and a file uses them by name with nothing to import. Two rules keep a module honest: construct in onStart() (allocation once), and update() once per bar.

The named streams

The multi-stream classes, and the fields each one carries:

ClassFields
Bb.basis, .upper, .lower
Keltner.basis, .upper, .lower
Donchian.basis, .upper, .lower
Macd.macd, .signal, .hist
Stoch, Stochastic.k, .d
Supertrend.line, .direction
Adx.adx, .plusDi, .minusDi
Ichimoku.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 an output of its own, so once you publish the indicator an alert set from the chart can follow any of them (Alerts).

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" });
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);

function onStart(): void {
  macd = new Macd(i32(p_fast()), i32(p_slow()), i32(p_signal()));
  bands = new Bb(i32(p_bb_period()), p_bb_mult());
}

function onBar(): void {
  const close = bar.close();
  macd.update(close);
  bands.update(close);
  // Nothing is written until the slowest stream is warm: the MACD signal line.
  if (isNaN(macd.signal) || isNaN(bands.basis)) return;
  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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

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 nothing is drawn until the 34th bar and all six lines then start in lockstep. Bars where one stream is finite and another is not are handled in the classes (NaN stays NaN), never in onBar().

A composite class as one value

The class instance is one 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.