The original taxonomy was four classical families plus a statistics group, with the F1-F12 expansion slotted in as sub-categories. This regroups the whole 71-indicator catalogue into eight top-level families, each with at least five members: Moving Averages (12), Momentum Oscillators (13), Trend & Directional (9), Price Oscillators (5), Volatility & Bands (12), Trailing Stops (5), Volume (9), Price Statistics (7). - Wiki: docs/wiki/indicators/ reorganised into eight family folders; all 71 indicator pages moved with `git mv`. Every internal cross-link is normalised to `../<family>/Indicator-X.md`, each page's `Family` field is set to its new family, and two pre-existing `../Indicator-Chaining.md` links (should have been `../../`) are corrected. A link check confirms every relative wiki link resolves. - Indicators-Overview.md fully rewritten around the eight families; Home.md indicator reference and the README family table follow suit. - Warmup-Periods.md gains the eight F13 indicators; CHANGELOG records the 46-indicator expansion (25 -> 71) and the eight-family taxonomy. - Tests: Node indicators.test.js and Python test_new_indicators.py cover all eight new indicators (Node 91/91, Python 117/117 green). cargo fmt + clippy (core/wickra/data/wasm/node) clean; 508 core tests, 25 data tests and 74 doctests green.
7.4 KiB
MacdIndicator
Moving Average Convergence Divergence — the difference of two EMAs, with a third EMA on top as the signal line.
The Rust struct is MacdIndicator (since Macd would collide with the
output struct on case-insensitive file systems and several existing trait
imports). The Python and Node bindings expose the same engine under the
shorter, conventional name MACD.
Quick reference
| Field | Value |
|---|---|
| Family | Trend & Directional |
| Input type | f64 (close) |
| Output type | MacdOutput { macd, signal, histogram } |
| Output range | unbounded (centred on 0) |
| Default parameters | fast = 12, slow = 26, signal = 9 (MacdIndicator::classic()) |
| Warmup period | slow + signal − 1 (34 for the classic configuration) |
| Interpretation | crossovers of macd and signal; zero-line crosses; histogram momentum |
Formula
EMA_n(x) = exponential moving average of x over n periods
(Wickra's EMA seeds from a simple average of the first n inputs)
macd_t = EMA_fast(close)_t − EMA_slow(close)_t
signal_t = EMA_signal(macd)_t
hist_t = macd_t − signal_t
The signal EMA does not start consuming inputs until macd_t becomes
defined (i.e. until both the fast and slow EMAs have seeded), which is
why the overall warmup is slow + signal − 1 rather than
max(slow, signal).
Parameters
| Name | Type | Default (Python) | Valid range | Description |
|---|---|---|---|---|
fast |
usize |
12 |
>= 1 and < slow |
Fast EMA period. |
slow |
usize |
26 |
>= 1 and > fast |
Slow EMA period. |
signal |
usize |
9 |
>= 1 |
EMA period applied to the raw MACD line. |
MacdIndicator::new returns Error::PeriodZero if any period is zero and
Error::InvalidPeriod if fast >= slow.
Inputs / Outputs
From impl Indicator for MacdIndicator:
type Input = f64;
type Output = MacdOutput;
fn update(&mut self, input: f64) -> Option<MacdOutput>;
MacdOutput carries three fields:
| Field | Description |
|---|---|
macd |
EMA(fast) − EMA(slow) of the input series. |
signal |
EMA(signal) of macd. |
histogram |
macd − signal. |
Python's MACD.batch(prices) returns a (n, 3) float64 array with
columns [macd, signal, histogram]; warmup rows are entirely NaN.
Node's MACD.batch(prices) returns a flat number[] of length n * 3
in the same interleaved order: index i*3 + 0 is macd, i*3 + 1 is
signal, i*3 + 2 is histogram. The streaming update(value) returns
a { macd, signal, histogram } object (or null during warmup).
Warmup
warmup_period() returns slow + signal − 1. The slow EMA seeds at
input slow; from that point onward the signal EMA starts receiving
macd values, and needs signal − 1 further inputs to seed itself.
For the classic (12, 26, 9) configuration this gives 26 + 9 − 1 = 34
inputs before the first complete MacdOutput is emitted, as pinned by
the unit test first_emission_matches_warmup_period.
Edge cases
- Constant input. Both EMAs converge to the constant value, so
macdapproaches0; with no movement inmacd, the signal EMA also approaches0, and so does the histogram. The Rust testconstant_series_yields_zero_macd_eventuallypins this. - Non-finite input.
update(NaN)orupdate(±∞)returns the previously emittedMacdOutputwithout advancing any internal EMA. - Reset.
reset()resets all three EMAs and clearslast. The nextwarmup_period()calls returnNoneagain.
Examples
Rust
use wickra::{BatchExt, Indicator, MacdIndicator};
let prices: Vec<f64> = (0..40).map(|i| 100.0 + i as f64 * (20.0 / 39.0)).collect();
let mut macd = MacdIndicator::classic();
let out = macd.batch(&prices);
let v = out[33].unwrap();
println!("row 33 macd={} signal={} hist={}", v.macd, v.signal, v.histogram);
let v = out[39].unwrap();
println!("row 39 macd={} signal={} hist={}", v.macd, v.signal, v.histogram);
Verified output:
row 33 macd=3.589743589743577 signal=3.5897435897435788 hist=-0.0000000000000017763568394002505
row 39 macd=3.589743589743591 signal=3.589743589743585 hist=0.000000000000006217248937900877
Python
import numpy as np
import wickra as ta
prices = np.linspace(100.0, 120.0, 40)
macd = ta.MACD(12, 26, 9)
out = macd.batch(prices)
print('shape :', out.shape)
print('warmup:', macd.warmup_period())
print('row 33:', out[33])
print('row 39:', out[39])
Verified output:
shape : (40, 3)
warmup: 34
row 33: [ 3.58974359e+00 3.58974359e+00 -1.77635684e-15]
row 39: [3.58974359e+00 3.58974359e+00 6.21724894e-15]
Node
const wickra = require('wickra');
const macd = new wickra.MACD(12, 26, 9);
const prices = Array.from({ length: 40 }, (_, i) => 100 + i * 20 / 39);
const flat = macd.batch(prices);
console.log('flat length:', flat.length);
console.log('row 33 macd :', flat[33 * 3]);
console.log('row 33 signal:', flat[33 * 3 + 1]);
console.log('row 33 hist :', flat[33 * 3 + 2]);
console.log('row 39 macd :', flat[39 * 3]);
console.log('row 39 signal:', flat[39 * 3 + 1]);
console.log('row 39 hist :', flat[39 * 3 + 2]);
Verified output:
flat length: 120
row 33 macd : 3.589743589743577
row 33 signal: 3.5897435897435788
row 33 hist : -1.7763568394002505e-15
row 39 macd : 3.589743589743591
row 39 signal: 3.589743589743585
row 39 hist : 6.217248937900877e-15
Interpretation
- Signal-line crossover.
macdcrossing abovesignalis the canonical bullish signal; the symmetric crossover below is bearish. Thehistogrammakes this explicit — it crosses zero on the same bar. - Zero-line crossover.
macdcrossing above zero says the fast EMA has overtaken the slow EMA; a longer-term trend confirmation, weaker than the signal-line cross. - Histogram momentum. Rising histogram bars (even while negative) indicate that bearish momentum is fading, and vice versa. Traders use this to anticipate signal-line crosses.
Common pitfalls
- The signal line lags the MACD line by
signal_periodbars. A crossover signal therefore arrives one full EMA-cycle after the underlying momentum turn, which is why MACD is a confirmation indicator, not a leading one. fast >= slowis rejected. A common bug when reading parameters from a config file is swapping the two — the constructor returnsError::InvalidPeriodrather than silently producing an inverted MACD line.- Don't slice a single column out of a warmup row. During the first
slow + signal − 1inputs every field isNaN(Python) or absent (Nonein Rust /nullin Node). Filter by checkingmacdfor finiteness before readingsignalorhistogram.
References
- Gerald Appel, Technical Analysis: Power Tools for Active Investors, Financial Times Prentice Hall, 2005 — the canonical modern treatment of the MACD line/signal-line/histogram trio Appel popularised in the late 1970s.
See also
- Indicator: Rsi — bounded sibling oscillator, useful as a confirmation filter on top of MACD signals.
- Indicator: Trix — another EMA-based momentum oscillator (triple-smoothed rate of change).
- Warmup Periods — table including the
slow + signal − 1rule. - Quickstart: Python — MACD multi-column NaN pattern explained.