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.
| 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.
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
| 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.
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
adxis rising. - MACD divergences. Price at a new high while
macd.macdis not is a weakening trend; thehistoutput shrinking toward zero says the same thing earlier. - Ichimoku. Price above the cloud (both spans) is an uptrend, below it
a downtrend; a
tenkanoverkijuncross that agrees with the cloud is the classic entry. - PSAR. Lower acceleration (
0.01to0.02) gives smoother stops with fewer reversals;0.05and up reacts faster and flips more in ranges. - Supertrend. The flip of
directionis the signal; the line itself is the trailing stop for the position the regime implies.