ChartnautDocs

Moving Average

The Moving Average indicator from the public library (ma), written against the current DSL.

What it plots

One line on price: the SMA, EMA, WMA or RMA of the close over period.

Key decisions

Piece

Why it matters

input.select for maType

One script covers four averages.

warmup((w) => w.when(maType)...)

SMA and WMA need period bars of history; EMA about 30 times period; RMA (Wilder) about 50 times. The warmup follows the selected type.

Window of the newest period closes

EMA and RMA seed from the SMA of the first full window, then smooth.

No plot until the window is full

A half-filled average is not drawn.

The script keeps its state with a small foldBar helper, the same in every example from here on. onBar runs again for the forming bar on every tick. foldBar builds each bar's state from the last closed bar's state and commits it only when the next bar starts, so a repeat of the same bar never stacks on itself. The chart also rolls back ctx.accum between ticks: see onBar.

Full script

meta({
  shortName: "MA",
  kind: "overlay",
  dataSource: "candles",
  category: "custom",
});

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

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

const maType = input.select({
  id: "maType",
  label: "Type",
  default: "SMA",
  options: [
    { value: "SMA", label: "SMA" },
    { value: "EMA", label: "EMA" },
    { value: "WMA", label: "WMA" },
    { value: "RMA", label: "RMA" },
  ],
});

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(maType)
  .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, maType),
    section({ id: "display", title: "Display Options" }, labelsEnabled),
  ),
  tab({ id: "appearance", title: "Appearance" },
    section({ id: "line", title: "Line", columns: 2 }, lineColor, lineStyle),
  ),
);

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

// 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.maType;

  const st = foldBar(ctx, "ma", { closes: [], value: null }, (prev) => {
    const closes = prev.closes.concat(ctx.close).slice(-p);
    if (closes.length < p) return { closes, value: null };

    let value;
    if (type === "SMA") {
      value = mean(closes);
    } else if (type === "WMA") {
      let sum = 0;
      for (let i = 0; i < p; i++) sum += closes[i] * (i + 1);
      value = sum / (p * (p + 1) / 2);
    } else {
      // EMA and RMA seed from the SMA of the first full window, then smooth.
      const alpha = type === "EMA" ? 2 / (p + 1) : 1 / p;
      value = prev.value == null ? mean(closes) : prev.value + alpha * (ctx.close - prev.value);
    }
    return { closes, value };
  });

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