MACD
The MACD indicator from the public library (macd), written against the current DSL.
What it plots
In its own pane: the MACD line, the signal line, and a histogram of their difference.
Key decisions
Piece | Why it matters |
|---|---|
Fast and slow EMAs of the close | MACD is fast minus slow. Each EMA seeds from the SMA of its first full window. |
Signal is an EMA of the MACD line | It starts once |
| The histogram output takes |
| The signal is an EMA of the MACD line, so its memory stacks on top of the slow EMA's. The fast EMA runs beside the slow one and needs less, so the slow one covers it. |
Full script
meta({
shortName: "MACD",
kind: "pane",
dataSource: "candles",
category: "custom",
});
dialog({
title: "MACD Configuration",
description: "Configure MACD settings",
width: 400,
height: 450,
resizable: false,
showEnabledCheckbox: true,
resetToDefaults: {
enabled: true,
confirmationMessage: "Are you sure you want to reset all MACD settings to their default values?",
},
});
const fastPeriod = input.number({
id: "fastPeriod",
label: "Fast Period",
default: 12,
min: 1,
step: 1,
});
const slowPeriod = input.number({
id: "slowPeriod",
label: "Slow Period",
default: 26,
min: 1,
step: 1,
});
const signalPeriod = input.number({
id: "signalPeriod",
label: "Signal Period",
default: 9,
min: 1,
step: 1,
});
const macdColor = input.color({
id: "macdColor",
label: "MACD Color",
default: "#2962FF",
allowAlpha: true,
});
const signalColor = input.color({
id: "signalColor",
label: "Signal Color",
default: "#FF6D00",
allowAlpha: true,
});
const histColor = input.color({
id: "histColor",
label: "Histogram Color",
default: "#26A69A",
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.sum(w.ema(slowPeriod), w.ema(signalPeriod)));
layout(
tab({ id: "general", title: "General" },
section({ id: "parameters", title: "Parameters" }, fastPeriod, slowPeriod, signalPeriod),
section({ id: "display", title: "Display Options" }, labelsEnabled),
),
tab({ id: "appearance", title: "Appearance" },
section({ id: "colors", title: "Colors", columns: 2 }, macdColor, signalColor, histColor, lineStyle),
),
);
output.line({
id: "macd",
lineWidth: 2,
colorFrom: macdColor,
lineStyleFrom: lineStyle,
labelFrom: labelsEnabled,
titleWhenLabeled: "MACD",
});
output.line({
id: "signal",
lineWidth: 1,
colorFrom: signalColor,
lineStyleFrom: lineStyle,
labelFrom: labelsEnabled,
titleWhenLabeled: "Signal",
});
output.histogram({
id: "histogram",
colorFrom: histColor,
labelFrom: labelsEnabled,
titleWhenLabeled: "Histogram",
});
// 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 fp = ctx.params.fastPeriod;
const sp = ctx.params.slowPeriod;
const sigP = ctx.params.signalPeriod;
// Each EMA seeds from the SMA of its first full window.
const ema = (prevValue, closes, n, x) => {
if (prevValue != null) return prevValue + (2 / (n + 1)) * (x - prevValue);
return closes.length >= n ? mean(closes.slice(-n)) : null;
};
const st = foldBar(ctx, "macd", { closes: [], fast: null, slow: null, macds: [], macd: null, signal: null }, (prev) => {
const closes = prev.closes.concat(ctx.close).slice(-Math.max(fp, sp));
const fast = ema(prev.fast, closes, fp, ctx.close);
const slow = ema(prev.slow, closes, sp, ctx.close);
if (fast == null || slow == null) return { closes, fast, slow, macds: [], macd: null, signal: null };
const macd = fast - slow;
// The signal line is an EMA of the MACD line, seeded the same way.
const macds = prev.signal == null ? prev.macds.concat(macd) : [];
const signal = ema(prev.signal, macds, sigP, macd);
return { closes, fast, slow, macds, macd, signal };
});
if (st.macd == null) return;
ctx.plot("macd", st.macd);
if (st.signal == null) return;
ctx.plot("signal", st.signal);
ctx.bar("histogram", st.macd - st.signal);
}Related
Warmup and memory for why each EMA needs 30x its period, and Series types for the histogram.
