---
title: "Trend indicators"
description: "Trend indicators identify direction and strength: the directional movement system, the Ichimoku cloud, MACD, the parabolic stop-and-reverse, the ATR trailing…"
order: 34
section: "functions"
---

<!-- source: docs/indicators/functions/trend-indicators.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Trend indicators

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](ta-library.md) page.

| System | What 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.

```typescript
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](series-functions.md)) 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](special-indicators.md) 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](oscillators.md) 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.

```typescript
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](styling.md)).

## 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.

```typescript
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](moving-averages.md)
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](drawing-primitives.md)); the slices tile into a
channel.

## Volatility primitives

| Class | Construct | Per bar | Returns |
| --- | --- | --- | --- |
| `Tr` | `new 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` |
| `Atr` | `new 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` |
| `Stdev` | `new Stdev(period)` | `.update(x)` | the rolling population standard deviation (divide by `period`, not `period - 1`) |
| `Variance` | `new 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.

```typescript
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.

```typescript
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.

```typescript
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.
