- Rust core: cmf.rs (Chaikin Money Flow — summed money-flow volume over summed volume, bounded to [-1, +1]), chaikin_oscillator.rs (Chaikin Oscillator — the MACD of the ADL, EMA(ADL, fast) - EMA(ADL, slow)), force_index.rs (Elder's Force Index — EMA of price change scaled by volume), ease_of_movement.rs (Arms' Ease of Movement — SMA of distance travelled per unit of volume). Each with a full Indicator impl, runnable doctest and reference / property / warmup / reset / batch==streaming tests. - Python: PyChaikinMoneyFlow / PyChaikinOscillator / PyForceIndex / PyEaseOfMovement PyO3 classes + module registration + .pyi stubs. - Node: explicit ChaikinMoneyFlowNode / ChaikinOscillatorNode / ForceIndexNode / EaseOfMovementNode; index.d.ts and index.js updated. - WASM: WasmChaikinMoneyFlow / WasmChaikinOscillator / WasmForceIndex / WasmEaseOfMovement. - Wiki: Indicator-ChaikinMoneyFlow/ChaikinOscillator/ForceIndex/ EaseOfMovement.md plus a new "Oscillators" sub-table in Indicators-Overview.md and entries in Home.md. cargo fmt + clippy (core/wickra/data/wasm/node) clean; 402 core tests, 25 data tests and 57 doctests green.
4.7 KiB
EaseOfMovement
Ease of Movement (EOM) — Richard Arms' measure of how far price travels per unit of volume, averaged over a window.
Quick reference
| Field | Value |
|---|---|
| Family | Volume |
| Sub-category | Oscillators |
| Input type | Candle (uses high, low, volume) |
| Output type | f64 |
| Output range | unbounded around zero (scaled by divisor) |
| Default parameters | period = 14, divisor = 1e8 (Python) |
| Warmup period | period + 1 |
| Interpretation | Light-volume moves push it away from zero; sign tracks direction. |
Formula
distance_t = (high_t + low_t)/2 − (high_{t−1} + low_{t−1})/2
EMV_t = distance_t · (high_t − low_t) · divisor / volume_t
EOM_t = SMA(EMV, period)_t
Arms' question is how easily did price move? A bar whose midpoint jumped a
long way on a wide range but light volume gets a large EMV; a bar that
needed heavy volume to budge gets a small one. The divisor is a pure
output-scaling constant — the conventional 1e8 keeps EMV readable for
typical share volumes; smaller markets want a smaller divisor. The window SMA
smooths the noisy per-bar EMV into a tradeable line.
Parameters
period— the SMA averaging window (Python default14).divisor— the volume-scaling constant (Python default1e8). Rust exposesEaseOfMovement::new(period)for the1e8default andEaseOfMovement::with_divisor(period, divisor)for an explicit value.
Inputs / Outputs
From crates/wickra-core/src/indicators/ease_of_movement.rs:
impl Indicator for EaseOfMovement {
type Input = Candle;
type Output = f64;
// update(&mut self, input: Candle) -> Option<f64>
}
EaseOfMovement is a candle-input indicator that reads high, low and
volume. In Python the streaming update accepts a 6-tuple or a dict; the
batch helper takes high, low, volume numpy arrays. Node and WASM expose
update(high, low, volume) and the matching batch.
Warmup
EaseOfMovement::new(14).warmup_period() == 15. The first candle only seeds
the previous midpoint, so the first EMV appears on candle 2 and the first
averaged value on candle period + 1.
Edge cases
- Zero-volume bar. Contributes
EMV = 0instead of dividing by zero (zero_volume_contributes_zeropins this). - Zero-range bar.
high == lowmakes the(high − low)factor zero, soEMV = 0. - Constant series. Unchanging midpoints give zero distance, so EOM stays
at
0.0(constant_series_yields_zeropins this). - Reset.
eom.reset()clears the previous midpoint and the SMA window.
Examples
Rust
use wickra::{BatchExt, Candle, Indicator, EaseOfMovement};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// EOM(period = 1, divisor = 1): one EMV value is its own average.
let mut eom = EaseOfMovement::with_divisor(1, 1.0)?;
let out = eom.batch(&[
Candle::new(9.0, 10.0, 8.0, 9.0, 50.0, 0)?, // seeds the previous midpoint (9)
Candle::new(12.0, 14.0, 10.0, 12.0, 100.0, 1)?, // mid 12, distance 3, range 4
]);
println!("{:?}", out);
Ok(())
}
Output:
[None, Some(0.12)]
Bar 2: EMV = distance · range · divisor / volume = 3 · 4 · 1 / 100 = 0.12.
This matches the reference_values test in
crates/wickra-core/src/indicators/ease_of_movement.rs.
Python
import numpy as np
import wickra as ta
eom = ta.EaseOfMovement(1, 1.0)
high = np.array([10.0, 14.0])
low = np.array([8.0, 10.0])
volume = np.array([50.0, 100.0])
print(eom.batch(high, low, volume))
Output:
[ nan 0.12]
Node
const ta = require('wickra');
const eom = new ta.EaseOfMovement(1, 1.0);
console.log(eom.batch([10, 14], [8, 10], [50, 100]));
Output:
[ NaN, 0.12 ]
Interpretation
EOM crossing above zero says price is drifting up without needing much volume — an easy, low-resistance advance; below zero is the same for a decline. A reading hovering near zero means volume is heavy relative to the distance covered, i.e. price is grinding. The sign tracks direction; the distance from zero tracks how freely the move is happening.
Common pitfalls
- Reading the raw magnitude. It depends entirely on the
divisoryou chose — only the sign and relative size are portable. - Feeding it scalar prices. It needs
high,lowandvolume.
References
Richard W. Arms Jr.'s Ease of Movement; the box-ratio formulation here matches the standard definition.
See also
- Indicator-ForceIndex.md — a different price-change-vs-volume gauge.
- Indicator-ChaikinMoneyFlow.md — bounded money-flow balance.
- Indicators-Overview.md — the full taxonomy.