Mass Index (Donald Dorsey) — a running sum of how much a smoothed bar
range exceeds its own smoothing, which is a measure of range expansion
that ignores direction entirely:
E1=EMA(high − low, emaPeriod) E2=EMA(E1, emaPeriod) ${output} =Σ over sumPeriod of (E1/E2)
Appends one column. Reads high and low, each named by an option
defaulting to its DEFAULT_OHLCV name (the atr precedent).
Dorsey's reading: the ratio sits near 1 while ranges are steady and
climbs while they widen, so a 25-bar sum of it near 25 is a quiet market.
The signal he named is the reversal bulge — the index rising through
27 and then falling back below 26.5 — which warns that a trend is about to
turn without saying which way. The index has no direction of its own,
which is the whole point of it: pair it with a trend study.
Definition — Dorsey's 9 / 25, and what is pinned
TA-Lib has no Mass Index, so the oracle is a pandas replication with
the analytic first-valid bar asserted and a measured separation from the
plausible wrong turn (using the ratio's mean rather than its sum, which
is the same shape divided by 25 and would sit inside a chart's noise if it
were not asserted apart).
9 and 25 are Dorsey's own. Both are options because the two lengths
do different jobs, but the defaults are the published pair and the 27 /
26.5 bulge thresholds only mean anything at sumPeriod: 25 — a caller
who changes it has to rescale the thresholds too, which is said here
rather than left to be discovered.
A sum, not an average. The index is Σ ratio, so it lives on a
~sumPeriod scale (about 25 for a steady market). Dividing by
sumPeriod would be a perfectly good indicator and is not this one;
every published threshold is on the sum.
The EMAs are pond's, first-sample seed, α = 2/(emaPeriod + 1) —
the macd / trix convention, so the chain cannot disagree
with ema() inside the package.
Plain range, not true range (barRangeValues) — Dorsey's, and
the same fork chaikinVolatility takes. atr and
choppinessIndex take the other one.
Stage 2 steps over stage 1's warm-up rather than poisoning its seed with
it (that is the K2 array door's rule for derived inputs), so on gap-free
input E1 lands on bar emaPeriod − 1, E2 on 2·emaPeriod − 2, and
the summation sumPeriod − 1 bars after that: 2·emaPeriod + sumPeriod − 3, which is bar 40 at the defaults. Length-preserving.
Edges
Scale-invariant, and shift-invariant. The ratio of two linear filters
of the same non-negative array is unchanged by scaling every price, and
the range is a difference, so it is unchanged by shifting them too. Both
are pinned by property tests.
A steady market reads ≈ sumPeriod, never 0. The natural
comparison level is 25, not zero — this is not an oscillator around a
zero line.
A zero denominator reports undefined, and needs no guard at all.
Two ways to reach one, and the summation absorbs both. On real bars
E2 = 0 requires every range from the series' start to that bar to be
exactly zero (a first-sample-seeded EMA of non-negative values is zero
only if all of them are), which forces E1 = 0 too, so the ratio is
0/0 — already NaN. On crossing columns (high and low
redirected at two fields that swap order) the range changes sign, E2
can land exactly on zero with E1 non-zero beside it, and the ratio is
±Infinity — which rollingMeanValues counts as a missing
cell, exactly as it counts a NaN, so the summation over it is
undefined and nothing non-finite ever reaches withColumn.
An explicit d === 0 guard was written here first and mutation testing
deleted it: no input can tell it is there. That is the
commodityChannelIndex finding arriving by a new route — not "the
numerator is forced to zero" but "a rolling kernel downstream of the
division masks non-finite values" — and it is why
choppinessIndex's guards, which sit at its output, are live
while this one was not. A unit test reaches the crossing case and pins
the undefined.
A leading gap shifts the start; an interior gap blanks that bar
in both EMA stages and then every summation window holding it, after
which the study recovers — the ema family skips, so the hole does not
run to the end of the series the way atr's does.
Mass Index (Donald Dorsey) — a running sum of how much a smoothed bar range exceeds its own smoothing, which is a measure of range expansion that ignores direction entirely:
Appends one column. Reads high and low, each named by an option defaulting to its
DEFAULT_OHLCVname (the atr precedent).Dorsey's reading: the ratio sits near
1while ranges are steady and climbs while they widen, so a 25-bar sum of it near25is a quiet market. The signal he named is the reversal bulge — the index rising through 27 and then falling back below 26.5 — which warns that a trend is about to turn without saying which way. The index has no direction of its own, which is the whole point of it: pair it with a trend study.Definition — Dorsey's 9 / 25, and what is pinned
TA-Lib has no Mass Index, so the oracle is a pandas replication with the analytic first-valid bar asserted and a measured separation from the plausible wrong turn (using the ratio's mean rather than its sum, which is the same shape divided by 25 and would sit inside a chart's noise if it were not asserted apart).
sumPeriod: 25— a caller who changes it has to rescale the thresholds too, which is said here rather than left to be discovered.Σ ratio, so it lives on a~sumPeriodscale (about 25 for a steady market). Dividing bysumPeriodwould be a perfectly good indicator and is not this one; every published threshold is on the sum.α = 2/(emaPeriod + 1)— the macd / trix convention, so the chain cannot disagree withema()inside the package.Warm-up: the EMA∘EMA chain, on the trix rule
Stage 2 steps over stage 1's warm-up rather than poisoning its seed with it (that is the K2 array door's rule for derived inputs), so on gap-free input
E1lands on baremaPeriod − 1,E2on2·emaPeriod − 2, and the summationsumPeriod − 1bars after that:2·emaPeriod + sumPeriod − 3, which is bar 40 at the defaults. Length-preserving.Edges
≈ sumPeriod, never0. The natural comparison level is25, not zero — this is not an oscillator around a zero line.undefined, and needs no guard at all. Two ways to reach one, and the summation absorbs both. On real barsE2 = 0requires every range from the series' start to that bar to be exactly zero (a first-sample-seeded EMA of non-negative values is zero only if all of them are), which forcesE1 = 0too, so the ratio is0/0— alreadyNaN. On crossing columns (highandlowredirected at two fields that swap order) the range changes sign,E2can land exactly on zero withE1non-zero beside it, and the ratio is±Infinity— which rollingMeanValues counts as a missing cell, exactly as it counts aNaN, so the summation over it isundefinedand nothing non-finite ever reacheswithColumn. An explicitd === 0guard was written here first and mutation testing deleted it: no input can tell it is there. That is the commodityChannelIndex finding arriving by a new route — not "the numerator is forced to zero" but "a rolling kernel downstream of the division masks non-finite values" — and it is why choppinessIndex's guards, which sit at its output, are live while this one was not. A unit test reaches the crossing case and pins theundefined.emafamily skips, so the hole does not run to the end of the series the way atr's does.