Trend indicators

Trend indicators identify direction and strength: the directional movement system, the Ichimoku cloud, MACD, the parabolic stop-and-reverse, the ATR trailing…

Trend indicators identify direction and strength: the directional movement system, the Ichimoku cloud, MACD, the parabolic stop-and-reverse, the ATR trailing stop, the band pair, and the volatility primitives under them. Every builtin of the kScript (legacy) roster on this page ships as a class in src/sdk/ta.ts (Adx, Ichimoku, Macd, Psar, Supertrend, Tr, Atr, Stdev, Variance), each one matching the kScript engine bar for bar, and the bands become outputs plus a range() declaration. The full catalog is on the TA library page.

SystemWhat it reads
Directional movement (Adx)trend strength as adx, with plusDi and minusDi; above 25 is a strong trend, below 20 often a range
Ichimoku cloud (Ichimoku)five components; the two leading spans are outputs displaced 26 bars ahead by declaration
MACD (Macd)momentum from the convergence and divergence of two Ema objects
Parabolic SAR (Psar)a dot that trails price and jumps to the other side when the trend flips
Supertrend (Supertrend)an ATR-banded trailing stop with a direction the chart colors by

Adx

new Adx(period = 14), .update(high, low, close) returns the ADX line; after each update the fields adx, plusDi, and minusDi hold the three streams the kScript tuple [ADX, DI+, DI-] carried. The class mirrors the engine's own arithmetic rather than a textbook Wilder average: the true range and the two directional movements are first summed over the first period bars (a plain sum, not a mean), then each running sum is smoothed as s = s - s / period + x; plusDi and minusDi are the smoothed movements over the smoothed true range times 100 (0 when that range is exactly 0); DX is the normalized DI difference, 0 when both DI lines are 0; and adx seeds on the plain average of the first period DX values before smoothing as (adx * (period - 1) + dx) / period. So the DI lines appear at bar period and adx at bar 2 * period - 1. Bar 0 has no previous bar and produces nothing; a non-finite bar before the seed clears the partial sums and restarts the run, and a non-finite bar after the seed leaves all three outputs NaN for the rest of the series, which is what the engine does too.

let adx = new Adx(14);
export function init(): void { adx = new Adx(i32(p_adx_period())); }

Read adx.plusDi > adx.minusDi for direction and adx.adx rising for conviction; a Cross object over the two DI lines (Series functions) turns the crossover into a signal.

Ichimoku

new Ichimoku(conversion = 9, base = 26, laggingSpan = 52, displacement = 26), .update(high, low, close) returns tenkan; fields tenkan, kijun, senkouA, senkouB, chikou. Each line is the midpoint of the highest high and lowest low over its period, and the engine's window is partial at the start: bar 0 already carries a value from its own bar, there is no NaN warm-up. senkouA and senkouB are the values from displacement bars ago (the engine falls back to the current bar's values on the first displacement bars), and every output that would be NaN is reported as 0, because the engine never returns na from ichimoku. chikou is the one field a streaming class cannot mirror: the engine reads the close displacement bars in the future, so the field holds the current close. Drawing the displacement stays declarative: the two spans are declared with displacement_bars: 26 so the chart draws them 26 bars ahead, and the lagging span with displacement_bars: -26, while the metric values stay on the bar they were computed on. The declarations and the two-color cloud (two boxes offset 26 bars ahead) are compiled on the Special indicators page.

Macd

new Macd(fast = 12, slow = 26, signal = 9), .update(close) returns the MACD line; fields macd, signal, hist. Three chained accumulator EMAs with the engine's seeding: the fast and slow legs each seed on the mean of their first period finite inputs (so macd appears at bar slow - 1), the signal leg is fed the MACD line and seeds on the mean of its first signal finite values (first at bar slow + signal - 2), and hist is macd - signal once both are finite. A non-finite input after a seed leaves that leg NaN for good. A compiled module is on the Oscillators page; the histogram draws as a histogram output around zero.

Psar

new Psar(start = 0.02, increment = 0.02, maxValue = 0.2), .update(high, low, close) returns the SAR price for the bar; there is no direction field, so read the side as close > sar (or track the flips with a Cross object over price and the SAR). The acceleration factor starts at start, grows by increment on every new extreme, and caps at maxValue; the SAR never crosses the two prior bars' lows in an up trend or highs in a down trend, and when price trades through it the trend flips and the SAR jumps to the last extreme. Engine conventions: bar 0 is NaN; bar 1 decides the opening trend from close[1] >= close[0] and returns low[0] for an up trend or high[0] for a down one; the close is only read on those two bars; a non-finite high or low (or close on bars 0 and 1) makes the SAR NaN for that bar and every bar after it, because the engine recomputes the whole series and stops at the first bad bar. Lower acceleration gives smoother, less twitchy stops; higher reacts faster but whipsaws in ranges.

let psar = new Psar(0.02, 0.02, 0.2);
let sar: f64 = NaN;
let side: f64 = 0.0;

export function state(): i32 {
  const close = in_close();
  sar = psar.update(in_high(), in_low(), close);
  side = isNaN(sar) ? 0.0 : close > sar ? 1.0 : -1.0;
  return isNaN(sar) ? 0 : 1;
}

Draw the SAR as a scatter output so it reads as dots, and color the dots by side through a color_by ladder (Styling).

Supertrend

new Supertrend(factor, atrPeriod), .update(high, low, close) returns the stop line and fills the fields line and direction (1 for an up trend, the line sits below price; -1 for a down trend, the line sits above). The bands sit factor ATRs either side of the bar midpoint (high + low) / 2; the upper band ratchets down (a lower basic band, or the previous close above the old band, replaces it) and the lower band ratchets up symmetrically; the direction flips to -1 when the close drops below the lower band in an up trend and to 1 when it rises above the upper band in a down trend; the line is the lower band in an up trend and the upper band in a down one. The ATR inside is the same Wilder smoothing as Atr, so the first finite bar is atrPeriod - 1, where the direction starts as 1 when the close is at or above the midpoint. While the ATR is NaN both fields are NaN and the band state is left untouched, so a gap neither resets nor advances the bands; a non-finite high, low, or close on a bar with a finite ATR gives NaN for that bar alone. Because a shape cannot read a bool, the direction is a number, ready to be written to a data-only output and used as a color_by index.

let st = new Supertrend(3.0, 10);

export function init(): void {
  st = new Supertrend(p_factor(), i32(p_atr_period()));
}

export function state(): i32 {
  st.update(in_high(), in_low(), in_close());
  return isNaN(st.line) ? 0 : 1;
}

export function finalize(): void {
  out_st_line(st.line);
  out_st_dir(st.direction > 0.0 ? 1.0 : 0.0);
  emitRow();
}

Bands as outputs

Bb and Keltner (both described on the Moving averages page) expose basis, upper, and lower fields; each becomes an output. kScript handed the two edges to fillBetween; an Indicator declares the band. range(upper, lower, options) records the pair in the sheet with a color, edge_width, edge_line_style, and smooth, or a colors + color_by ladder to tint it per bar.

Not in Indicators yet. The chart does not draw declared ranges or sheet fills today: range() rides the sheet for hosts that honor it and the chart lane ignores it. To shade the band on the chart, declare a box on every bar between the two edge outputs with from and to left at 0 (Drawing primitives); the slices tile into a channel.

Volatility primitives

ClassConstructPer barReturns
Trnew Tr().update(high, low, close)the true range of the current bar: the largest of high - low, abs(high - prevClose), abs(low - prevClose), taken pairwise left to right; on bar 0 just high - low
Atrnew Atr(period = 14).update(high, low, close)the Wilder-smoothed true range: the seed is the plain average of the first period true ranges (first value at bar period - 1), then (prev * (period - 1) + tr) / period
Stdevnew Stdev(period).update(x)the rolling population standard deviation (divide by period, not period - 1)
Variancenew Variance(period).update(x)the rolling population variance, the same window arithmetic as Stdev without the square root

All four ship in src/sdk/ta.ts and match the kScript engine bar for bar; they are the building blocks the band and stop classes use internally, exposed so custom volatility logic composes the same way. Their NaN rules differ, and the differences are the engine's: Tr returns NaN on a non-finite high or low and on the bar after a non-finite close; Atr restarts its seed when a non-finite true range arrives before the seed completes, and stays NaN for good after one arrives later (the engine never reseeds); Stdev and Variance are strict windows, NaN until period bars exist and whenever any value in the window is not finite, healing as soon as the bad bar leaves.

const tr = new Tr();
let atr = new Atr(14);
let stdev = new Stdev(20);
let variance = new Variance(20);
let trValue: f64 = NaN;
let atrValue: f64 = NaN;
let stdevValue: f64 = NaN;
let varianceValue: f64 = NaN;

export function init(): void {
  atr = new Atr(i32(p_atr_period()));
  stdev = new Stdev(i32(p_period()));
  variance = new Variance(i32(p_period()));
}

export function state(): i32 {
  const close = in_close();
  trValue = tr.update(in_high(), in_low(), close);
  atrValue = atr.update(in_high(), in_low(), close);
  stdevValue = stdev.update(close);
  varianceValue = variance.update(close);
  return isNaN(atrValue) ? 0 : 1;
}

Stdev is strict: a NaN sample anywhere in its window makes the result NaN until the sample leaves the window, which is the kScript stdev rule. kScript's stddev computes the same numbers in the same order on finite data, so Stdev stands in for both names; guard the input with isNaN when a sparse source feeds it.

Putting them together

The directional system, the SAR, the Supertrend stop, and the volatility primitives in one module. The stop line is colored by its regime through a color_by ladder over a data-only st_dir output, the SAR draws as dots, and the ADX trio and the four volatility lines share a lower pane.

import { input, line, lower, none, ohlcv, output, overlay, param, scatter } from "./sdk/declare";
import { in_close, in_high, in_low } from "./gen/inputs";
import {
  emitRow,
  out_adx,
  out_atr,
  out_di_minus,
  out_di_plus,
  out_psar,
  out_st_dir,
  out_st_line,
  out_stdev,
  out_tr,
  out_variance,
} from "./gen/outputs";
import { p_adx_period, p_atr_period, p_factor } from "./gen/params";
import { Adx, Atr, Psar, Stdev, Supertrend, Tr, Variance } from "./sdk/ta";

param("adx_period", 14, { min: 1, max: 200 });
param("factor", 3, { min: 0.5, max: 10, description: "Supertrend ATR multiplier" });
param("atr_period", 10, { min: 1, max: 200, description: "ATR window for Supertrend and the atr line" });
input("close", ohlcv.close);
input("high", ohlcv.high);
input("low", ohlcv.low);
output("st_line", line, overlay, { width: 2, color_by: "st_dir", colors: ["#dc2626", "#16a34a"], description: "Supertrend stop, red in a short regime, green in a long one" });
output("st_dir", none, overlay, { description: "0 short, 1 long: the palette index for st_line" });
output("psar", scatter, overlay, { color: "#9333ea", description: "Parabolic SAR dots" });
output("adx", line, lower, { color: "#111827", width: 2, description: "Average directional index" });
output("di_plus", line, lower, { color: "#2563eb", width: 1, description: "+DI" });
output("di_minus", line, lower, { color: "#dc2626", width: 1, description: "-DI" });
output("tr", line, lower, { color: "#94a3b8", width: 1, description: "True range" });
output("atr", line, lower, { color: "#f97316", width: 2, description: "Average true range" });
output("stdev", line, lower, { color: "#0891b2", width: 1, description: "Rolling standard deviation of the close" });
output("variance", line, lower, { color: "#7c3aed", width: 1, description: "Rolling variance of the close" });

let adx = new Adx(14);
let psar = new Psar(0.02, 0.02, 0.2);
let st = new Supertrend(3.0, 10);
let tr = new Tr();
let atr = new Atr(10);
let stdev = new Stdev(10);
let variance = new Variance(10);
let psarValue: f64 = NaN;
let trValue: f64 = NaN;
let atrValue: f64 = NaN;
let stdevValue: f64 = NaN;
let varianceValue: f64 = NaN;

export function init(): void {
  adx = new Adx(i32(p_adx_period()));
  psar = new Psar(0.02, 0.02, 0.2);
  st = new Supertrend(p_factor(), i32(p_atr_period()));
  tr = new Tr();
  atr = new Atr(i32(p_atr_period()));
  stdev = new Stdev(i32(p_atr_period()));
  variance = new Variance(i32(p_atr_period()));
}

export function state(): i32 {
  const close = in_close();
  const high = in_high();
  const low = in_low();
  adx.update(high, low, close);
  psarValue = psar.update(high, low, close);
  st.update(high, low, close);
  trValue = tr.update(high, low, close);
  atrValue = atr.update(high, low, close);
  stdevValue = stdev.update(close);
  varianceValue = variance.update(close);
  return 1;
}

export function finalize(): void {
  out_st_line(st.line);
  out_st_dir(st.direction > 0.0 ? 1.0 : 0.0);
  out_psar(psarValue);
  out_adx(adx.adx);
  out_di_plus(adx.plusDi);
  out_di_minus(adx.minusDi);
  out_tr(trValue);
  out_atr(atrValue);
  out_stdev(stdevValue);
  out_variance(varianceValue);
  emitRow();
}

export function reset(): void {
  adx.reset();
  psar.reset();
  st.reset();
  tr.reset();
  atr.reset();
  stdev.reset();
  variance.reset();
  psarValue = NaN;
  trValue = NaN;
  atrValue = NaN;
  stdevValue = NaN;
  varianceValue = NaN;
}

Bands: outputs, a range, and a shaded box

The Bollinger pair as three outputs, a range() declaration recording the band in the sheet, and a box on every bar shading the same pair on the chart, gated so the shade only shows while the basis is rising.

import { box, input, line, none, ohlcv, output, overlay, param, range } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_basis, out_lower_band, out_rising, out_upper_band } from "./gen/outputs";
import { p_mult, p_period } from "./gen/params";
import { Sma, Stdev } from "./sdk/ta";

param("period", 20, { min: 2, max: 400 });
param("mult", 2, { min: 0.5, max: 5, description: "Standard-deviation multiples" });
input("close", ohlcv.close);
const upper = output("upper_band", line, overlay, { color: "#64748b", width: 1, description: "Basis plus mult deviations" });
output("basis", line, overlay, { color: "#2563eb", width: 1, description: "20-bar simple average" });
const lowerBand = output("lower_band", line, overlay, { color: "#64748b", width: 1, description: "Basis minus mult deviations" });
const rising = output("rising", none, overlay, { description: "1 while the basis rises: the shade gate" });
// The sheet-level band: honored by hosts that draw ranges, ignored by the chart lane today.
range("upper_band", "lower_band", { color: "#2563eb", edge_width: 1, edge_line_style: "dotted" });
// The chart-drawn band: one slice per bar between the same two outputs, tiling into a channel.
box("band_shade", { top: upper, bottom: lowerBand, when: rising, color: "#2563eb", opacity: 0.12, borderWidth: 0 });

let sma = new Sma(20);
let stdev = new Stdev(20);
let mult: f64 = 2.0;
let mid: f64 = NaN;
let prevMid: f64 = NaN;
let sd: f64 = NaN;

export function init(): void {
  sma = new Sma(i32(p_period()));
  stdev = new Stdev(i32(p_period()));
  mult = p_mult();
}

export function state(): i32 {
  const close = in_close();
  prevMid = mid;
  mid = sma.update(close);
  sd = stdev.update(close);
  return isNaN(mid) || isNaN(sd) ? 0 : 1;
}

export function finalize(): void {
  out_upper_band(mid + mult * sd);
  out_basis(mid);
  out_lower_band(mid - mult * sd);
  out_rising(!isNaN(prevMid) && mid > prevMid ? 1.0 : 0.0);
  emitRow();
}

export function reset(): void {
  sma.reset();
  stdev.reset();
  mid = NaN;
  prevMid = NaN;
  sd = NaN;
}

Reading them

  • ADX. Above 25 is a strong trend. Use the DI crossover for direction, and trust it most while adx is rising.
  • MACD divergences. Price at a new high while macd.macd is not is a weakening trend; the hist output shrinking toward zero says the same thing earlier.
  • Ichimoku. Price above the cloud (both spans) is an uptrend, below it a downtrend; a tenkan over kijun cross that agrees with the cloud is the classic entry.
  • PSAR. Lower acceleration (0.01 to 0.02) gives smoother stops with fewer reversals; 0.05 and up reacts faster and flips more in ranges.
  • Supertrend. The flip of direction is the signal; the line itself is the trailing stop for the position the regime implies.