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.
173 lines
5.4 KiB
Markdown
173 lines
5.4 KiB
Markdown
# MassIndex
|
||
|
||
> Mass Index — Donald Dorsey's range-expansion indicator: it watches the
|
||
> high–low range widen and contract to anticipate reversals.
|
||
|
||
## Quick reference
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| Family | Trend & Directional |
|
||
| Input type | `Candle` (uses `high`, `low`) |
|
||
| Output type | `f64` |
|
||
| Output range | `> 0`, oscillates around `sum_period` |
|
||
| Default parameters | `(ema_period = 9, sum_period = 25)` (Python) |
|
||
| Warmup period | `2·ema_period + sum_period − 2` |
|
||
| Interpretation | A rise above `27` then fall below `26.5` flags a reversal. |
|
||
|
||
## Formula
|
||
|
||
```
|
||
range_t = high_t − low_t
|
||
single_t = EMA(range, ema_period)_t
|
||
double_t = EMA(single, ema_period)_t
|
||
ratio_t = single_t / double_t
|
||
MassIndex = Σ ratio over sum_period
|
||
```
|
||
|
||
The Mass Index ignores direction entirely — it tracks **volatility shape**.
|
||
When the high–low range widens, the single EMA pulls ahead of the double
|
||
EMA, the ratio climbs above `1`, and the windowed sum rises. Dorsey's
|
||
"reversal bulge" is the classic pattern: the Mass Index rising above `27`
|
||
and then falling back below `26.5` warns that a range expansion is about
|
||
to resolve — often into a trend reversal.
|
||
|
||
## Parameters
|
||
|
||
| Name | Type | Default | Valid range | Description |
|
||
|--------------|---------|---------------|-------------|-------------|
|
||
| `ema_period` | `usize` | `9` (Python) | `>= 1` | Period of both EMAs in the cascade. `0` errors with `Error::PeriodZero`. |
|
||
| `sum_period` | `usize` | `25` (Python) | `>= 1` | Length of the summation window. |
|
||
|
||
The Python binding defaults the pair to `(9, 25)`. The `periods` property
|
||
returns `(ema_period, sum_period)`.
|
||
|
||
## Inputs / Outputs
|
||
|
||
From `crates/wickra-core/src/indicators/mass_index.rs`:
|
||
|
||
```rust
|
||
impl Indicator for MassIndex {
|
||
type Input = Candle;
|
||
type Output = f64;
|
||
// update(&mut self, input: Candle) -> Option<f64>
|
||
}
|
||
```
|
||
|
||
`MassIndex` is a **candle-input** indicator: it reads `high` and `low`. In
|
||
Python the streaming `update` accepts a 6-tuple or a dict; the batch
|
||
helper takes `high` and `low` numpy arrays. Node and WASM expose
|
||
`update(high, low)` and `batch(high, low)`.
|
||
|
||
## Warmup
|
||
|
||
`warmup_period() == 2·ema_period + sum_period − 2`. The first EMA seeds at
|
||
input `ema_period`; the second EMA, stacked on it, seeds at
|
||
`2·ema_period − 1`; the summation window then needs `sum_period` ratios.
|
||
For the default `(9, 25)` that is `41` bars.
|
||
|
||
## Edge cases
|
||
|
||
- **Constant range.** When every bar has the same high–low range, both
|
||
EMAs converge to the same value, every ratio is `1`, and the Mass Index
|
||
equals `sum_period` (`constant_range_sums_to_sum_period` pins this).
|
||
- **Zero-range market.** A flat market (`high == low`) drives both EMAs to
|
||
`0`; the `0 / 0` is guarded with the neutral ratio `1`, so the Mass
|
||
Index again equals `sum_period`
|
||
(`zero_range_market_sums_to_sum_period` pins this).
|
||
- **Candle validation.** `Candle::new` rejects invalid bars upstream.
|
||
- **Reset.** `mi.reset()` clears both EMAs, the window and the sum.
|
||
|
||
## Examples
|
||
|
||
### Rust
|
||
|
||
```rust
|
||
use wickra::{BatchExt, Candle, Indicator, MassIndex};
|
||
|
||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||
let mut mi = MassIndex::new(3, 4)?;
|
||
// Constant high-low range of 2.0; the Mass Index settles at sum_period.
|
||
let candles: Vec<Candle> = (0..40)
|
||
.map(|i| {
|
||
let mid = 100.0 + f64::from(i);
|
||
Candle::new(mid, mid + 1.0, mid - 1.0, mid, 1.0, i64::from(i)).unwrap()
|
||
})
|
||
.collect();
|
||
let out = mi.batch(&candles);
|
||
println!("warmup_period = {}", mi.warmup_period());
|
||
println!("last = {:?}", out.last().unwrap());
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
Output:
|
||
|
||
```
|
||
warmup_period = 8
|
||
last = Some(4.0)
|
||
```
|
||
|
||
A constant range makes every ratio `1`, so the sum equals `sum_period`
|
||
(`4`). This matches the `constant_range_sums_to_sum_period` test in
|
||
`crates/wickra-core/src/indicators/mass_index.rs`.
|
||
|
||
### Python
|
||
|
||
```python
|
||
import numpy as np
|
||
import wickra as ta
|
||
|
||
mi = ta.MassIndex() # (ema_period=9, sum_period=25)
|
||
mid = np.arange(100.0, 160.0)
|
||
high = mid + 1.0
|
||
low = mid - 1.0
|
||
print(mi.batch(high, low)[-1]) # constant range -> 25
|
||
```
|
||
|
||
Output:
|
||
|
||
```
|
||
25.0
|
||
```
|
||
|
||
### Node
|
||
|
||
```javascript
|
||
const ta = require('wickra');
|
||
const mi = new ta.MassIndex(9, 25);
|
||
const mid = Array.from({ length: 60 }, (_, i) => 100 + i);
|
||
const high = mid.map((m) => m + 1);
|
||
const low = mid.map((m) => m - 1);
|
||
console.log(mi.batch(high, low).at(-1)); // 25
|
||
```
|
||
|
||
## Interpretation
|
||
|
||
`MassIndex` is a *reversal-warning* tool, not a direction tool — it never
|
||
tells you which way price will go, only that a turn is likely. The textbook
|
||
use is the "reversal bulge" on the default `(9, 25)` settings: watch for
|
||
the index to push above `27`, then act when it drops back under `26.5`,
|
||
using a directional indicator (a moving average, ADX) to pick the side.
|
||
|
||
## Common pitfalls
|
||
|
||
- **Expecting a direction.** The Mass Index is direction-blind; always
|
||
pair it with a trend indicator.
|
||
- **Feeding it scalar prices.** It needs `high`/`low`; it takes a
|
||
`Candle`, not an `f64`.
|
||
|
||
## References
|
||
|
||
Donald Dorsey, "The Mass Index", *Technical Analysis of Stocks &
|
||
Commodities* (1992). The double-EMA-of-range construction and the `(9,
|
||
25)` defaults follow Dorsey's original.
|
||
|
||
## See also
|
||
|
||
- [Indicator-Atr.md](../volatility-bands/Indicator-Atr.md) — directional-free
|
||
volatility in price units.
|
||
- [Indicator-BollingerBands.md](../volatility-bands/Indicator-BollingerBands.md)
|
||
— another range-expansion lens.
|
||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|