ChartnautDocs

Bollinger Bands

The Bollinger Bands indicator from the public library (bollinger), written against the current DSL.

What it plots

A middle line on price (the SMA of the close) with upper and lower bands standardDeviations standard deviations away.

Key decisions

Piece

Why it matters

SMA and standard deviation of one window

The bands widen and tighten with recent volatility.

Three output.line ids

Middle, upper and lower are styled separately and readable separately as dependency outputs.

warmup((w) => w.window(period))

Both numbers come from the same period closes, so the warmup is period.

Full script

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

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

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

const standardDeviations = input.number({
  id: "standardDeviations",
  label: "Standard Deviations",
  default: 2,
  min: 0.1,
  step: 0.1,
});

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

const bandColor = input.color({
  id: "bandColor",
  label: "Band Color",
  default: "#FF6D00",
  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.window(period));

layout(
  tab({ id: "general", title: "General" },
    section({ id: "parameters", title: "Parameters" }, period, standardDeviations),
    section({ id: "display", title: "Display Options" }, labelsEnabled),
  ),
  tab({ id: "appearance", title: "Appearance" },
    section({ id: "lines", title: "Lines", columns: 2 }, middleColor, bandColor, lineStyle),
  ),
);

output.line({
  id: "middle",
  lineWidth: 2,
  colorFrom: middleColor,
  lineStyleFrom: lineStyle,
  labelFrom: labelsEnabled,
  titleWhenLabeled: "BB Mid",
});

output.line({
  id: "upper",
  lineWidth: 1,
  colorFrom: bandColor,
  lineStyleFrom: lineStyle,
  labelFrom: labelsEnabled,
  titleWhenLabeled: "BB Upper",
});

output.line({
  id: "lower",
  lineWidth: 1,
  colorFrom: bandColor,
  lineStyleFrom: lineStyle,
  labelFrom: labelsEnabled,
  titleWhenLabeled: "BB Lower",
});

// 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 mult = ctx.params.standardDeviations;

  const closes = foldBar(ctx, "closes", [], (prev) => prev.concat(ctx.close).slice(-p));
  if (closes.length < p) return;

  const sma = mean(closes);
  let sq = 0;
  for (const c of closes) sq += (c - sma) * (c - sma);
  const sd = Math.sqrt(sq / p); // population standard deviation of the same window

  ctx.plot("middle", sma);
  ctx.plot("upper", sma + sd * mult);
  ctx.plot("lower", sma - sd * mult);
}

Series types for bands, and onBar for the forming-bar rule behind foldBar.