F13c: restructure the indicator catalogue into eight families
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.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user