ChartnautDocs

ATR

The ATR indicator from the public library (atr), written against the current DSL.

What it plots

Average True Range in its own pane: volatility in price units, with a choice of smoothing.

Key decisions

Piece

Why it matters

True range

The largest of high minus low and the distance from each to the previous close, so gaps count.

smoothingType select

RMA (Wilder, the default), SMA, EMA or WMA once the first window is full.

Seed, then smooth

The first ATR is the plain average of period true ranges for every type.

Warmup per smoothing type

period for SMA and WMA, 30 times it for EMA, 50 times it for RMA.

Full script

meta({
  shortName: "ATR",
  kind: "pane",
  dataSource: "candles",
  category: "custom",
});

dialog({
  title: "ATR Configuration",
  description: "Configure Average True Range (ATR) settings",
  width: 400,
  height: 350,
  resizable: false,
  showEnabledCheckbox: true,
  resetToDefaults: {
    enabled: true,
    confirmationMessage: "Are you sure you want to reset all ATR settings to their default values?",
  },
});

const period = input.number({
  id: "period",
  label: "Period",
  default: 14,
  min: 1,
  step: 1,
});

const smoothingType = input.select({
  id: "smoothingType",
  label: "Smoothing",
  default: "RMA",
  options: [
    { value: "RMA", label: "RMA (Wilder's)" },
    { value: "SMA", label: "SMA" },
    { value: "EMA", label: "EMA" },
    { value: "WMA", label: "WMA" },
  ],
});

const lineColor = input.color({
  id: "lineColor",
  label: "Line Color",
  default: "#2962FF",
  allowAlpha: true,
});

const lineStyle = input.lineStyle({
  id: "lineStyle",
  label: "Line Style",
  default: LineStyle.Solid,
});

const labelsEnabled = input.boolean({
  id: "labelsEnabled",
  label: "Show Labels",
  default: true,
});

warmup((w) => w.when(smoothingType)
  .is(["SMA", "WMA"]).then(w.window(period))
  .is("EMA").then(w.ema(period))
  .otherwise(w.wilder(period)));

layout(
  tab({ id: "general", title: "General" },
    section({ id: "parameters", title: "Parameters" }, period, smoothingType),
    section({ id: "display", title: "Display Options" }, labelsEnabled),
  ),
  tab({ id: "appearance", title: "Appearance" },
    section({ id: "line", title: "Line", columns: 2 }, lineColor, lineStyle),
  ),
);

output.line({
  id: "atr",
  lineWidth: 2,
  colorFrom: lineColor,
  lineStyleFrom: lineStyle,
  labelFrom: labelsEnabled,
  titleWhenLabeled: "ATR",
});

// onBar runs again for the forming bar on every tick. The chart rolls ctx.accum back between
// ticks; foldBar does not depend on that, so the same code is right on every runtime.
// foldBar keeps two states: `done` (as of the last closed bar) and `tip` (this bar). `step`
// builds the tip from `done` without mutating it, so a repeat of the same bar recomputes the tip
// instead of stacking on top of it. A new bar time commits the tip.
function foldBar(ctx, id, init, step) {
  const s = ctx.accum(id, () => ({ t: null, done: init, tip: init }), (s) => {
    if (s.t !== ctx.time) {
      s.done = s.tip;
      s.t = ctx.time;
    }
    s.tip = step(s.done);
    return s;
  });
  return s.tip;
}

function mean(values) {
  let sum = 0;
  for (const v of values) sum += v;
  return sum / values.length;
}

/** @param {ScriptedCtx} ctx */
export function onBar(ctx) {
  const p = ctx.params.period;
  const type = ctx.params.smoothingType;

  const st = foldBar(ctx, "atr", { close: null, trs: [], atr: null }, (prev) => {
    if (prev.close == null) return { close: ctx.close, trs: [], atr: null };

    // True range counts the gap from the previous close.
    const tr = Math.max(
      ctx.high - ctx.low,
      Math.abs(ctx.high - prev.close),
      Math.abs(ctx.low - prev.close),
    );
    const trs = prev.trs.concat(tr).slice(-p);
    if (trs.length < p) return { close: ctx.close, trs, atr: null };

    let atr;
    if (prev.atr == null || type === "SMA") {
      atr = mean(trs); // the first full window seeds every type
    } else if (type === "WMA") {
      let sum = 0;
      for (let i = 0; i < p; i++) sum += trs[i] * (i + 1);
      atr = sum / (p * (p + 1) / 2);
    } else if (type === "EMA") {
      atr = prev.atr + (2 / (p + 1)) * (tr - prev.atr);
    } else {
      atr = (prev.atr * (p - 1) + tr) / p; // RMA (Wilder)
    }
    return { close: ctx.close, trs, atr };
  });

  if (st.atr != null) ctx.plot("atr", st.atr);
}

Warmup and memory for why Wilder smoothing declares 50x its period, and onBar for the forming-bar rule behind foldBar.