Extra indicators

View as MarkdownOpen the editor

The TA library is the core catalog of 52 classes. The ./sdk/ta-plus module adds 30 more with the same shape: construct the class in onStart(), call update() once per bar.

Twenty-six are textbook indicators the catalog never had; four are Pine's own spellings of classes the library has in another shape.

The 26 are the mean and percent-rank deviations, Bollinger and Keltner widths, the volume lines (accumulation and distribution, the Williams pair, the volume indices, price-volume trend, intraday intensity), the running extremes, mode, range and centre of gravity, the double and triple EMAs, the zero-lag EMA, TRIX, Kaufman's efficiency ratio and adaptive average, the ultimate oscillator, Vortex, Aroon and choppiness. The four are Dmi with its two lengths, SourceStoch and SourceVwap over any source series, and HeikinAshi. Every class composes the library's own primitives and follows its three rules: NaN until the class is warm, NaN while a non-finite input sits in its window (the two volume indices carry instead), and no allocation after the constructor. The window arithmetic under them (History, List, the stats.* functions, rounding and a seeded random) is on Stats, history and lists.

Every class in ./sdk/ta-plus

update() returns the primary stream; the extra streams are public fields you read after the call. "First value" is the bar index of the first non-NaN result on a clean series (bar 0 is the first bar). Periods below 1 are clamped to 1.

ClassConstructupdate(...)FieldsDefinitionFirst value
Dev(period)(x)mean absolute deviation of the last period values from their Sma: sum(abs(x - sma)) / periodbar period - 1
PercentRank(period)(x)100 * count(previous period values <= x) / period, the current bar excluded from the countbar period
Bbw(period, mult)(x)basis, upper, lower(upper - lower) / basis over Bb(period, mult); NaN when the basis is 0bar period - 1
Kcw(period, mult, atrPeriod)(x, high, low, close)basis, upper, lower(upper - lower) / basis over Keltner(period, mult, atrPeriod); NaN when the basis is 0bar max(period, atrPeriod) - 1
AccDist()(high, low, close, volume)running sum of ((2 * close - high - low) / (high - low)) * volume, the term 0 when high == lowbar 0
Wad()(high, low, close)running sum of: close - min(low, prevClose) when the close rose, close - max(high, prevClose) when it fell, else 0; bar 0 returns 0bar 0
Wvad()(open, high, low, close, volume)((close - open) / (high - low)) * volume for this bar, never a running sum; NaN when high == lowbar 0
Nvi()(close, volume)starts at 1; when volume < prevVolume: nvi = prev + ((close - prevClose) / prevClose) * prev, else prev; a close of 0 or NaN, on this bar or the previous one, keeps prevbar 0
Pvi()(close, volume)the same with volume > prevVolumebar 0
Pvt()(close, volume)running sum of ((close - prevClose) / prevClose) * volume; bar 0 returns 0bar 0
RunningMax()(x)the largest finite x seen so far (a non-finite x is skipped)the first finite bar
RunningMin()(x)the smallest finite x seen so far (a non-finite x is skipped)the first finite bar
Mode(period)(x)the most frequent value of the window (exact equality); ties go to the smallestbar period - 1
Range(period)(x)Highest - Lowest of the last period valuesbar period - 1
Cog(period)(x)-(sum of x[i] * (i + 1)) / sum(x[i]) for i = 0 (the newest) to period - 1; NaN when the sum is 0bar period - 1
Iii()(high, low, close, volume)(2 * close - high - low) / ((high - low) * volume) for this bar; NaN when the denominator is 0bar 0
Dema(period)(x)2 * e1 - e2, e1 = Ema(period) of x, e2 = Ema(period) of e1bar 2 * period - 2
Tema(period)(x)3 * e1 - 3 * e2 + e3bar 3 * period - 3
Zlema(period)(x)Ema(period) of x + (x - x[lag]), lag = floor((period - 1) / 2)bar lag + period - 1
Trix(period)(x)10000 * the one-bar change of Ema(Ema(Ema(ln x))); NaN when x <= 0bar 3 * period - 2
Ultimate(p1 = 7, p2 = 14, p3 = 28)(high, low, close)BP = close - min(low, prevClose), TR = max(high, prevClose) - min(low, prevClose), avg_n = Sum(BP, n) / Sum(TR, n), result 100 * (4 * avg1 + 2 * avg2 + avg3) / 7bar p3
Vortex(period)(high, low, close)plus, minusVM+ = abs(high - prevLow), VM- = abs(low - prevHigh); plus = Sum(VM+) / Sum(TR), minus = Sum(VM-) / Sum(TR) over period; returns plusbar period
Aroon(period)(high, low)up, down, oscup = 100 * (Highest(period + 1).bars + period) / period, down likewise with Lowest, osc = up - down; returns oscbar period
Choppiness(period)(high, low, close)100 * log10(Sum(TR, period) / (Highest(high, period) - Lowest(low, period))) / log10(period); NaN when the range is 0bar period - 1
Efficiency(period)(x)abs(x - x[period]) / Sum(abs(x - x[1]), period); NaN when the sum is 0bar period
Kama(period, fast = 2, slow = 30)(x)sc = (er * (2 / (fast + 1) - 2 / (slow + 1)) + 2 / (slow + 1))^2 with er = Efficiency(period); kama = prev + sc * (x - prev), seeded with x on the first bar er existsbar period
Dmi(diLength, adxSmoothing)(high, low, close)adx, plusDi, minusDiPine's ta.dmi: +DI = 100 * Rma(+DM, diLength) / Rma(TR, diLength), -DI likewise, DX = abs(+DI - -DI) / (+DI + -DI) (over 1 when the sum is 0), adx = 100 * Rma(DX, adxSmoothing); a zero smoothed range holds +DI and -DI (Pine's fixnan); a missing bar is skipped the way TradingView skips it (see below); equal lengths give the library's Adx on a series without a hole; returns adxplusDi, minusDi at bar diLength; adx at bar diLength + adxSmoothing - 1
SourceStoch(periodK, smoothK = 1, periodD = 1)(source, high, low)k, dPine's ta.stoch(source, high, low, length): raw %K is 100 * (source - Lowest(low)) / (Highest(high) - Lowest(low)) over periodK, k = Sma(smoothK) of it, d = Sma(periodD) of k; a flat window follows TradingView (the previous raw %K when the source sits on the window's value, NaN otherwise) where the library's Stoch reads 0, so fed the close it is Stoch on every window with a range; returns kk at bar periodK + smoothK - 2, d at bar periodK + smoothK + periodD - 3
SourceVwap(anchor = "")(source, volume, tsMs = NaN)Pine's ta.vwap(source): the library's Vwap sums, sum(source * volume) / sum(volume) from the anchor ("", "day", "week", "month", "quarter", "year" or a bucket width in milliseconds; tsMs is bar.time() * 1000.0, read only with an anchor), over any source; fed (high + low + close) / 3 it is Vwap(anchor, "hlc3")bar 0
HeikinAshi()(open, high, low, close)open, high, low, closePine's ticker.heikinashi: close = (open + high + low + close) / 4, open = (previous ha open + previous ha close) / 2 (the bar's own (open + close) / 2 on the first bar), high = max(high, ha open, ha close), low = min(low, ha open, ha close); returns the smoothed closebar 0

A non-finite input (NaN or infinite) is handled by shape. A windowed class (Dev, PercentRank, Bbw, Mode, Range, Cog, Ultimate, Vortex, Aroon, Choppiness, Efficiency, SourceStoch) returns NaN while the bad bar sits inside its window and heals when it leaves; the running sums (AccDist, Wad, Pvt) stay NaN until reset(); the per-bar readings (Wvad, Iii) are NaN on that bar alone; the classes built on Ema (Dema, Tema, Zlema, Trix, and Kcw through Keltner) follow the Ema rule: a bad bar before the seed restarts the seed, a bad bar after it makes the value NaN for good. Dmi keeps TradingView's own rule for ta.dmi instead: ta.rma skips a na bar with its state untouched, ta.tr and the two changes are na on that bar and on the next one, so each smoother is fed only when its own input is a value, fixnan holds plusDi and minusDi meanwhile, adx keeps smoothing the DX of the held values, and two bars after the hole every stream continues from the state it had (the library's Adx goes NaN for good there). Kama re-seeds with the next x once its efficiency window is clean again, the running extremes skip the bar, and Nvi and Pvi never turn NaN: they carry their value across a bar whose close, or previous close, is 0 or not a number. SourceVwap keeps the Vwap rule (the current bucket is NaN until the next one starts), and HeikinAshi reads NaN on the bad bar and restarts its chain on the next finite one, seeding the open as the first bar does.

Deviation, rank and width

Seven classes read one series. Dev and PercentRank say how far and how high the newest value sits in its window; Bbw and Kcw are the band widths as one number, with the bands in fields; Range, Mode and Cog are the window's shape.

param("period", 20, { min: 2, max: 400, description: "Window for every class" });
param("mult", 2, { min: 0.5, max: 5, description: "Band multiplier" });
output("dev", line, lower, { color: "#2563eb", description: "Mean absolute deviation of the close" });
output("prank", line, lower, { color: "#7c3aed", description: "Percent rank of the close" });
output("bbw", line, lower, { color: "#16a34a", description: "Bollinger band width" });
output("bb_upper", line, lower, { color: "#15803d", description: "Upper Bollinger band" });
output("kcw", line, lower, { color: "#ea580c", description: "Keltner channel width" });
output("range", line, lower, { color: "#0891b2", description: "Highest minus lowest close" });
output("mode", line, lower, { color: "#4b5563", description: "Most frequent close" });
output("cog", line, lower, { color: "#be123c", description: "Centre of gravity" });

let dev = new Dev(20);
let prank = new PercentRank(20);
let bbw = new Bbw(20, 2.0);
let kcw = new Kcw(20, 2.0, 10);
let closeRange = new Range(20);
let mode = new Mode(20);
let cog = new Cog(20);

function onStart(): void {
  const period = i32(p_period());
  dev = new Dev(period);
  prank = new PercentRank(period);
  bbw = new Bbw(period, p_mult());
  kcw = new Kcw(period, p_mult(), period / 2);
  closeRange = new Range(period);
  mode = new Mode(period);
  cog = new Cog(period);
}

function onBar(): void {
  const close = bar.close();
  const devValue = dev.update(close);
  const prankValue = prank.update(close);
  const bbwValue = bbw.update(close);
  const kcwValue = kcw.update(close, bar.high(), bar.low(), close);
  const rangeValue = closeRange.update(close);
  const modeValue = mode.update(close);
  const cogValue = cog.update(close);
  if (isNaN(devValue)) return;
  out_dev(devValue);
  out_prank(prankValue);
  out_bbw(bbwValue);
  out_bb_upper(bbw.upper);
  out_kcw(kcwValue);
  out_range(rangeValue);
  out_mode(modeValue);
  out_cog(cogValue);
}

PercentRank counts the previous period values, never the current bar, so its first value lands one bar after Dev's: onBar() gates on Dev and the first ready row draws a gap for the rank. Kcw reports its basis once the EMA is seeded and the width once the ATR is warm too.

Volume and money flow

AccDist, Wad and Pvt are running sums: the line so far, from the first bar. Nvi and Pvi are indices that start at 1 and move only on a bar whose volume fell (Nvi) or rose (Pvi). Wvad and Iii are per-bar readings with no memory; for a running line feed one to the library's Cum, as the sample does for Wvad.

input("open", ohlcv.open);
output("accdist", line, lower, { color: "#2563eb", description: "Accumulation/distribution line" });
output("wad", line, lower, { color: "#7c3aed", description: "Williams accumulation/distribution" });
output("wvad", line, lower, { color: "#16a34a", description: "Williams variable A/D of this bar" });
output("wvad_line", line, lower, { color: "#65a30d", description: "Running total of wvad" });
output("nvi", line, lower, { color: "#dc2626", description: "Negative volume index, from 1" });
output("pvi", line, lower, { color: "#ea580c", description: "Positive volume index, from 1" });
output("pvt", line, lower, { color: "#0891b2", description: "Price-volume trend" });
output("iii", line, lower, { color: "#4b5563", description: "Intraday intensity of this bar" });
output("running_max", line, lower, { color: "#15803d", description: "Highest close so far" });
output("running_min", line, lower, { color: "#be123c", description: "Lowest close so far" });

let accdist = new AccDist();
let wad = new Wad();
let wvad = new Wvad();
let wvadLine = new Cum();
let nvi = new Nvi();
let pvi = new Pvi();
let pvt = new Pvt();
let iii = new Iii();
let runningMax = new RunningMax();
let runningMin = new RunningMin();

function onStart(): void {
  accdist = new AccDist();
  wad = new Wad();
  wvad = new Wvad();
  wvadLine = new Cum();
  nvi = new Nvi();
  pvi = new Pvi();
  pvt = new Pvt();
  iii = new Iii();
  runningMax = new RunningMax();
  runningMin = new RunningMin();
}

function onBar(): void {
  const open = bar.open();
  const high = bar.high();
  const low = bar.low();
  const close = bar.close();
  const volume = bar.volume();
  const accdistValue = accdist.update(high, low, close, volume);
  const wadValue = wad.update(high, low, close);
  const wvadValue = wvad.update(open, high, low, close, volume);
  // A flat bar has no reading: add 0, Cum keeps a NaN.
  const wvadTotal = wvadLine.update(isNaN(wvadValue) ? 0.0 : wvadValue);
  const nviValue = nvi.update(close, volume);
  const pviValue = pvi.update(close, volume);
  const pvtValue = pvt.update(close, volume);
  const iiiValue = iii.update(high, low, close, volume);
  const maxValue = runningMax.update(close);
  const minValue = runningMin.update(close);
  if (isNaN(accdistValue)) return;
  out_accdist(accdistValue);
  out_wad(wadValue);
  out_wvad(wvadValue);
  out_wvad_line(wvadTotal);
  out_nvi(nviValue);
  out_pvi(pviValue);
  out_pvt(pvtValue);
  out_iii(iiiValue);
  out_running_max(maxValue);
  out_running_min(minValue);
}

A running line depends on where the history starts: the chart feeds the module the bars it fetched, so the newest bar's value moves when that window does. Read AccDist, Wad, Pvt and the two indices as shapes, not as levels to alert on.

Smoothers on the EMA

Dema, Tema and Zlema stack the library's Ema; Trix stacks three of them over the logarithm of the price and reads the one-bar change; Efficiency and Kama are Kaufman's pair, the ratio of net move to total move and the average whose smoothing follows it.

param("period", 20, { min: 2, max: 400 });
param("fast", 2, { min: 1, max: 100, description: "Kama fast length" });
param("slow", 30, { min: 1, max: 400, description: "Kama slow length" });
output("dema", line, overlay, { color: "#2563eb", description: "Double EMA" });
output("tema", line, overlay, { color: "#7c3aed", description: "Triple EMA" });
output("zlema", line, overlay, { color: "#16a34a", description: "Zero-lag EMA" });
output("kama", line, overlay, { color: "#ea580c", width: 2, description: "Kaufman adaptive average" });
output("trix", line, lower, { color: "#dc2626", description: "TRIX" });
output("efficiency", line, lower, { color: "#0891b2", description: "Efficiency ratio, 0..1" });

let dema = new Dema(20);
let tema = new Tema(20);
let zlema = new Zlema(20);
let trix = new Trix(20);
let efficiency = new Efficiency(20);
let kama = new Kama(20, 2, 30);

function onStart(): void {
  const period = i32(p_period());
  dema = new Dema(period);
  tema = new Tema(period);
  zlema = new Zlema(period);
  trix = new Trix(period);
  efficiency = new Efficiency(period);
  kama = new Kama(period, i32(p_fast()), i32(p_slow()));
}

function onBar(): void {
  const close = bar.close();
  const demaValue = dema.update(close);
  const temaValue = tema.update(close);
  const zlemaValue = zlema.update(close);
  const trixValue = trix.update(close);
  const erValue = efficiency.update(close);
  const kamaValue = kama.update(close);
  if (isNaN(kamaValue)) return;
  out_dema(demaValue);
  out_tema(temaValue);
  out_zlema(zlemaValue);
  out_kama(kamaValue);
  out_trix(trixValue);
  out_efficiency(erValue);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

The warm-ups differ: Kama has its first value at bar period while Tema(20) waits until bar 57 and Trix(20) until bar 58. Gate readiness on the line you plot as primary, not on the slowest class.

Two oscillators

Ultimate blends buying pressure over three windows; Vortex reads the two movement sums against the true range and exposes both lines.

param("period", 14, { min: 2, max: 400, description: "Vortex window" });
input("high", ohlcv.high);
output("ultimate", line, lower, { color: "#2563eb", description: "Ultimate oscillator (7, 14, 28)" });
output("vortex_plus", line, lower, { color: "#16a34a", description: "VI+" });
output("vortex_minus", line, lower, { color: "#dc2626", description: "VI-" });

let ultimate = new Ultimate(7, 14, 28);
let vortex = new Vortex(14);

function onStart(): void {
  ultimate = new Ultimate(7, 14, 28);
  vortex = new Vortex(i32(p_period()));
}

function onBar(): void {
  const high = bar.high();
  const low = bar.low();
  const close = bar.close();
  const ultimateValue = ultimate.update(high, low, close);
  const plus = vortex.update(high, low, close);
  const minus = vortex.minus;
  if (isNaN(plus)) return;
  out_ultimate(ultimateValue);
  out_vortex_plus(plus);
  out_vortex_minus(minus);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

A regime from Aroon and choppiness

Aroon says which way the recent extreme sits, Choppiness says whether the window went anywhere at all. Together they make a three-way regime: trending up, trending down, or ranging. The regime is a data-only output that tints a shape through color_by (Colors).

param("period", 25, { min: 2, max: 400, description: "Aroon and choppiness window" });
param("chop_ceiling", 61.8, { min: 0, max: 100, description: "Choppiness above this reads as ranging" });
input("high", ohlcv.high);
output("aroon_up", line, lower, { color: "#16a34a", description: "Aroon up" });
output("aroon_down", line, lower, { color: "#dc2626", description: "Aroon down" });
output("chop", line, lower, { color: "#4b5563", description: "Choppiness index, 0..100" });
output("regime", none, overlay, { description: "0 ranging, 1 trending up, 2 trending down" });
output("regime_mark", shape, overlay, {
  color: "#94a3b8",
  color_by: "regime",
  colors: ["#94a3b8", "#16a34a", "#dc2626"],
  description: "The close, tinted by regime",
});

let aroon = new Aroon(25);
let chop = new Choppiness(25);
let chopCeiling: f64 = 61.8;

function onStart(): void {
  const period = i32(p_period());
  aroon = new Aroon(period);
  chop = new Choppiness(period);
  chopCeiling = p_chop_ceiling();
}

function onBar(): void {
  const high = bar.high();
  const low = bar.low();
  const close = bar.close();
  const osc = aroon.update(high, low);
  const chopValue = chop.update(high, low, close);
  if (isNaN(osc) || isNaN(chopValue)) return;
  let regime: f64 = 0.0;
  if (chopValue > chopCeiling) regime = 0.0;
  else regime = osc > 0.0 ? 1.0 : 2.0;
  out_aroon_up(aroon.up);
  out_aroon_down(aroon.down);
  out_chop(chopValue);
  out_regime(regime);
  out_regime_mark(close);
}
BTCUSDT perpetual on Binance, 1 hour bars, Aug 10 to Aug 18, 2026Real output from OpenMarket's engine

Aroon runs a Highest and a Lowest over period + 1 bars and reads their bars offsets, so up is 100 on the bar of a fresh high and falls by 100 / period on every bar the high ages; Choppiness is the log ratio of the true-range sum to the window's range, near 100 when the bars overlap and near 0 when they march in one direction. onBar() returns before writing until both are warm: Aroon needs period + 1 bars, Choppiness needs period.

Pine's spellings of four library classes

The library's Adx, Stoch and Vwap are the engine's forms: one length, the close as the source, a named bar price. Four classes here carry the Pine spellings a ported script writes. Dmi(diLength, adxSmoothing) is ta.dmi with its two lengths (+DI and -DI smoothed over diLength, ADX over adxSmoothing, both with the library's Rma); with equal lengths it is Adx to the last bit of the arithmetic, and across a missing bar it keeps TradingView's state rules (nothing restarts, nothing is poisoned). SourceStoch is ta.stoch(source, high, low, length) over any source, smoothed like Stoch; fed the close it is Stoch on every window with a range (a flat window follows TradingView: the previous raw %K when the source sits on the window's value, NaN otherwise, where Stoch reads 0). SourceVwap is ta.vwap(source) over any source with Vwap's anchors; fed (high + low + close) / 3 it is Vwap(anchor, "hlc3"). HeikinAshi is ticker.heikinashi as a class over the bars you feed it (this chart's, or another market's candles); the recursion forgets its start by half every bar, so after about 50 bars the values no longer depend on where the loaded history begins.

wrun
param("di_length", 14, { min: 1, max: 200 });
param("adx_smoothing", 7, { min: 1, max: 200 });
param("stoch_length", 14, { min: 1, max: 200 });
output("adx", line, lower, { color: "#111827", description: "ADX over its own smoothing" });
output("plus_di", line, lower, { color: "#16a34a", description: "+DI" });
output("minus_di", line, lower, { color: "#dc2626", description: "-DI" });
output("stoch_hl2", line, lower, { color: "#2563eb", description: "Stochastic of the bar midpoint" });
output("vwap_ohlc4", line, overlay, { color: "#f59e0b", description: "Session VWAP over ohlc4" });
output("ha_close", line, overlay, { color: "#7c3aed", description: "Heikin Ashi close" });

let dmi = new Dmi(14, 7);
let stoch = new SourceStoch(14);
const vwap = new SourceVwap("day");
const ha = new HeikinAshi();

function onStart(): void {
  dmi = new Dmi(i32(p_di_length()), i32(p_adx_smoothing()));
  stoch = new SourceStoch(i32(p_stoch_length()));
}

function onBar(): void {
  const o = bar.open();
  const h = bar.high();
  const l = bar.low();
  const c = bar.close();
  const adx = dmi.update(h, l, c);
  const k = stoch.update((h + l) / 2.0, h, l);
  const vwapValue = vwap.update((o + h + l + c) / 4.0, bar.volume(), bar.time() * 1000.0);
  const haClose = ha.update(o, h, l, c);
  if (isNaN(adx)) return;
  out_adx(adx);
  out_plus_di(dmi.plusDi);
  out_minus_di(dmi.minusDi);
  out_stoch_hl2(k);
  out_vwap_ohlc4(vwapValue);
  out_ha_close(haClose);
}

Dmi warms up last (diLength + adxSmoothing - 1 bars), so the sample gates on it. SourceStoch(length) is the raw %K; pass smoothK and periodD for the ta.sma smoothing a Pine script applies afterwards. SourceVwap takes the bar's open time in milliseconds like Vwap (Volume and VWAP); HeikinAshi exposes the four smoothed values as fields, read after update().

History and lists

History (Pine's close[1] as history.ago(1)), List, the stats.* window functions, HandleRing, roundTo, roundToTick and Random have their own page: Stats, history and lists.

From Pine

One to one: the class returns Pine's value on the same bars. The second table holds the three that share Pine's formula with one difference. The array.*, x[n], math.round and math.random rows are on Stats, history and lists.

PineExtra indicators
ta.dev(source, length)new Dev(length), .update(x)
ta.percentrank(source, length)new PercentRank(length), .update(x)
ta.bbw(source, length, mult)new Bbw(length, mult), .update(x)
ta.accdistnew AccDist(), .update(high, low, close, volume)
ta.wadnew Wad(), .update(high, low, close)
ta.wvadnew Wvad(), .update(open, high, low, close, volume)
ta.nvinew Nvi(), .update(close, volume)
ta.pvinew Pvi(), .update(close, volume)
ta.max(source)new RunningMax(), .update(x)
ta.min(source)new RunningMin(), .update(x)
ta.mode(source, length)new Mode(length), .update(x)
ta.range(source, length)new Range(length), .update(x)
ta.cog(source, length)new Cog(length), .update(x)
ta.iiinew Iii(), .update(high, low, close, volume)
PineExtra indicatorsThe difference
ta.stoch(source, high, low, length) with a na source, high or lownew SourceStoch(length), .update(source, high, low)TradingView carries the previous %K over a bar whose input is na; here the class reads NaN while that bar sits in its windows (the library's windowed rule) and heals when it leaves
ta.pvtnew Pvt(), .update(close, volume)the same line from the second bar on; the first bar, with no previous close, reads 0 here
ta.kcw(source, length, mult, true)new Kcw(length, mult, length), .update(x, high, low, close)the range average is the library's Wilder Atr; Pine's is an EMA of the true range, so the widths track each other without being equal