Trend and volatility

View as MarkdownOpen the editor

Trend tools read direction and strength, volatility tools read range: the directional movement system, the Ichimoku cloud, the parabolic stop-and-reverse, the ATR trailing stop, the band pair, and the volatility primitives under them.

Every one on this page ships as a class in ./sdk/ta (Adx, Ichimoku, Psar, Supertrend, Tr, Atr, Stdev, Variance), and the bands are outputs plus a range() declaration. MACD is an oscillator and lives on the Oscillators page. 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 come out of the class already shifted, the lagging span is drawn back by declaration
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 class follows 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.

wrun
let adx = new Adx(14);
function onStart(): 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.

Pine's ta.dmi(diLength, adxSmoothing) takes two lengths: +DI and -DI over diLength, ADX over adxSmoothing. Adx smooths all three over one period, so when the two differ reach for Dmi in ./sdk/ta-plus (Extra indicators), which keeps Pine's form (Wilder's Rma on both) and the same three fields; with equal lengths the two classes agree on a series without a hole (Dmi skips a missing bar the way TradingView's ta.dmi does, where Adx reads NaN for good).

wrun
let dmi = new Dmi(14, 14);
function onStart(): void { dmi = new Dmi(i32(p_di_length()), i32(p_adx_smoothing())); }
// after dmi.update(high, low, close): dmi.adx, dmi.plusDi, dmi.minusDi

Ichimoku

new Ichimoku(conversionPeriod = 9, basePeriod = 26, laggingSpanPeriod = 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. The engine conventions the class keeps:

  • Partial windows, no warm-up. Bar 0 already has a value from its own bar; there is no NaN lead-in. A NaN high or low inside the window is skipped, but the current bar's own NaN poisons the value, and every output that comes out NaN is reported as 0.
  • The displacement is inside the math. senkouA on bar i is (tenkan + kijun) / 2 as it stood displacement bars earlier, and senkouB is the 52-bar midpoint from displacement bars earlier; on the first displacement bars both fall back to the current bar's values. The two fields are therefore already the cloud that belongs on the current bar, so write them to plain outputs with no displacement_bars. The projection past the newest loaded bar is not emitted, exactly as in the engine.
  • chikou is the current close. The engine reads the close of bar i + displacement, a future bar, and only falls back to the current close on the last displacement bars of the series; a class that sees one bar at a time cannot read ahead, so the field is the current close. Declaring displacement_bars: -26 on its output makes the chart draw that close 26 bars back, which is the lagging span as the engine's chart shows it. That shift is drawing only: the value stays on the bar that computed it.
wrun
const cloud = new Ichimoku(9, 26, 52, 26);

// In onBar(): one update per bar, then read the five lines by name.
cloud.update(bar.high(), bar.low(), bar.close());
const bullish = cloud.senkouA >= cloud.senkouB; // the cloud on this bar, already shifted

The cloud itself is two boxes: box("cloud_up", { top: senkouA, bottom: senkouB, when: aAbove }) draws, for each bar, a one-bar slice between the two spans the class reports for that bar, and a second box gated the other way draws the bearish slices in the other color. The declarations are compiled in Cloud, stop, and dots below.

Psar

new Psar(start = 0.02, increment = 0.02, maxValue = 0.2), .update(high, low, close) returns the SAR price for the bar, which jumps to the other side of price when the trend flips; there is no direction field, so read the side as close > sar (or the SAR against the low), 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 (0.01 to 0.02) gives smoother stops with fewer reversals; 0.05 and up reacts faster and flips more in ranges.

wrun
let psar = new Psar(0.02, 0.02, 0.2);

function onBar(): void {
  const close = bar.close();
  const sar = psar.update(bar.high(), bar.low(), close);
  if (isNaN(sar)) return;
  const side = close > sar ? 1.0 : -1.0;
  out_psar(sar);
  out_side(side);
}

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) (no defaults; 3 and 10 are the usual values), .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.

wrun
let st = new Supertrend(3.0, 10);

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

function onBar(): void {
  st.update(bar.high(), bar.low(), bar.close());
  if (isNaN(st.line)) return;
  out_st_line(st.line);
  out_st_dir(st.direction > 0.0 ? 1.0 : 0.0);
}

Bands as outputs

Bb and Keltner (both described on the Moving averages page) expose basis, upper, and lower fields; each becomes an output. A wrun indicator declares the band between them. range(upper, lower, options) names the two edge outputs (both must be drawn) and the chart draws the band: both edge lines at edge_width and edge_line_style with a tinted interior in color, a colors + color_by ladder to tint it per bar, or a vertical gradient (Styling). A box on every bar between the two edge outputs, with from and to left at 0, is the other way to shade (Cards, frames and panels); the slices tile into a channel, and either edge may stay data-only.

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 ./sdk/ta; 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.

wrun
const tr = new Tr();
let atr = new Atr(14);
let stdev = new Stdev(20);
let variance = new Variance(20);

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

function onBar(): void {
  const close = bar.close();
  const trValue = tr.update(bar.high(), bar.low(), close);
  const atrValue = atr.update(bar.high(), bar.low(), close);
  const stdevValue = stdev.update(close);
  const varianceValue = variance.update(close);
  if (isNaN(atrValue)) return;
  out_tr(trValue);
  out_atr(atrValue);
  out_stdev(stdevValue);
  out_variance(varianceValue);
}

Stdev is strict: a NaN sample anywhere in its window makes the result NaN until the sample leaves the window; 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.

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

function onStart(): 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()));
}

function onBar(): void {
  const close = bar.close();
  const high = bar.high();
  const low = bar.low();
  adx.update(high, low, close);
  const psarValue = psar.update(high, low, close);
  st.update(high, low, close);
  const trValue = tr.update(high, low, close);
  const atrValue = atr.update(high, low, close);
  const stdevValue = stdev.update(close);
  const varianceValue = variance.update(close);
  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);
}

Cloud, stop, and dots

Ichimoku with its two-color cloud and the lagging span drawn back, the Supertrend stop colored by regime, and the SAR as dots, in one overlay.

param("factor", 3, { min: 0.5, max: 10, description: "Supertrend ATR multiplier" });
param("atr_period", 10, { min: 1, max: 200, description: "Supertrend ATR window" });
output("tenkan", line, overlay, { color: "#0891b2", width: 1, description: "Conversion line, 9-bar midpoint" });
output("kijun", line, overlay, { color: "#be123c", width: 1, description: "Base line, 26-bar midpoint" });
// The spans are already shifted inside the class: the value on a bar is the cloud for that bar.
const senkouA = output("senkou_a", line, overlay, { color: "#0f766e", width: 1, description: "Leading span A, the class shifts it 26 bars ahead" });
const senkouB = output("senkou_b", line, overlay, { color: "#b45309", width: 1, description: "Leading span B, the class shifts it 26 bars ahead" });
output("chikou", line, overlay, { color: "#64748b", width: 1, displacement_bars: -26, description: "Lagging span, the close drawn 26 bars back" });
const aAbove = output("a_above", none, overlay, { description: "1 where span A is above span B: the bullish cloud gate" });
const bAbove = output("b_above", none, overlay, { description: "1 where span B is above span A: the bearish cloud gate" });
// The cloud: one slice per bar between the two spans, tinted by which span is on top.
box("cloud_up", { top: senkouA, bottom: senkouB, when: aAbove, color: "#16a34a", opacity: 0.12, borderWidth: 0 });
box("cloud_down", { top: senkouB, bottom: senkouA, when: bAbove, color: "#dc2626", opacity: 0.12, borderWidth: 0 });
output("st_line", line, overlay, { width: 2, color_by: "st_dir", colors: ["#dc2626", "#16a34a"], description: "Supertrend stop, colored by regime" });
output("st_dir", none, overlay, { description: "0 short, 1 long" });
output("psar", scatter, overlay, { color: "#9333ea", description: "Parabolic SAR" });

let cloud = new Ichimoku(9, 26, 52, 26);
let st = new Supertrend(3.0, 10);
let psar = new Psar(0.02, 0.02, 0.2);

function onStart(): void {
  cloud = new Ichimoku(9, 26, 52, 26);
  st = new Supertrend(p_factor(), i32(p_atr_period()));
  psar = new Psar(0.02, 0.02, 0.2);
}

function onBar(): void {
  const close = bar.close();
  const high = bar.high();
  const low = bar.low();
  cloud.update(high, low, close);
  st.update(high, low, close);
  const psarValue = psar.update(high, low, close);
  out_tenkan(cloud.tenkan);
  out_kijun(cloud.kijun);
  out_senkou_a(cloud.senkouA);
  out_senkou_b(cloud.senkouB);
  out_chikou(cloud.chikou);
  out_a_above(cloud.senkouA >= cloud.senkouB ? 1.0 : 0.0);
  out_b_above(cloud.senkouB > cloud.senkouA ? 1.0 : 0.0);
  out_st_line(st.line);
  out_st_dir(st.direction > 0.0 ? 1.0 : 0.0);
  out_psar(psarValue);
}

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.

param("period", 20, { min: 2, max: 400 });
param("mult", 2, { min: 0.5, max: 5, description: "Standard-deviation multiples" });
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;

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

function onBar(): void {
  const close = bar.close();
  prevMid = mid;
  mid = sma.update(close);
  const sd = stdev.update(close);
  if (isNaN(mid) || isNaN(sd)) return;
  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);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Reading them

  • 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.
  • Supertrend. The flip of direction is the signal; the line itself is the trailing stop for the position the regime implies.