F10: add Chaikin Money Flow, Chaikin Oscillator, Force Index and Ease of Movement
- 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.
This commit is contained in:
@@ -131,6 +131,10 @@ Rust / Python / Node examples. They are grouped by family, mirroring the
|
||||
- [Indicator-Vwap.md](indicators/volume/Indicator-Vwap.md)
|
||||
- [Indicator-Adl.md](indicators/volume/Indicator-Adl.md)
|
||||
- [Indicator-VolumePriceTrend.md](indicators/volume/Indicator-VolumePriceTrend.md)
|
||||
- [Indicator-ChaikinMoneyFlow.md](indicators/volume/Indicator-ChaikinMoneyFlow.md)
|
||||
- [Indicator-ChaikinOscillator.md](indicators/volume/Indicator-ChaikinOscillator.md)
|
||||
- [Indicator-ForceIndex.md](indicators/volume/Indicator-ForceIndex.md)
|
||||
- [Indicator-EaseOfMovement.md](indicators/volume/Indicator-EaseOfMovement.md)
|
||||
|
||||
## See also
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Indicators Overview
|
||||
|
||||
Wickra ships 50 indicators, organised in source under the four classical
|
||||
Wickra ships 54 indicators, organised in source under the four classical
|
||||
families — trend, momentum, volatility, volume — that map directly to the
|
||||
directory structure of `crates/wickra-core/src/indicators/`. The same family
|
||||
labels are used here, plus a second-level grouping that reflects how the
|
||||
@@ -169,6 +169,18 @@ Volume indicators all take `Candle` input because they need `close` and
|
||||
|---------------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `RollingVwap` | VWAP over a sliding window instead of since-start; useful for session-independent VWAP. | `Candle` | `f64` | unbounded (price scale) | `period` | `period` | [Indicator-Vwap.md → RollingVwap](indicators/volume/Indicator-Vwap.md#rollingvwap-finite-window) |
|
||||
|
||||
### Oscillators
|
||||
|
||||
Volume-flow oscillators: bounded or zero-centred readings derived from where
|
||||
price closes within each bar and how much volume backed the move.
|
||||
|
||||
| Indicator | One-liner | Input | Output | Range | Defaults | Warmup | Deep dive |
|
||||
|-----------|-----------|-------|--------|-------|----------|--------|-----------|
|
||||
| `ChaikinMoneyFlow` | Summed money-flow volume divided by summed volume over `period` bars. | `Candle` | `f64` | `[−1, +1]` | `period = 20` (Python) | `period` | [Indicator-ChaikinMoneyFlow.md](indicators/volume/Indicator-ChaikinMoneyFlow.md) |
|
||||
| `ChaikinOscillator` | `EMA(ADL, fast) − EMA(ADL, slow)`; the MACD of the ADL. | `Candle` | `f64` | unbounded around zero | `(fast=3, slow=10)` (Python) | `slow` | [Indicator-ChaikinOscillator.md](indicators/volume/Indicator-ChaikinOscillator.md) |
|
||||
| `ForceIndex` | `EMA((close − prev_close) · volume, period)`; the conviction behind a move. | `Candle` | `f64` | unbounded around zero | `period = 13` (Python) | `period + 1` | [Indicator-ForceIndex.md](indicators/volume/Indicator-ForceIndex.md) |
|
||||
| `EaseOfMovement` | `SMA` of distance travelled per unit of volume. | `Candle` | `f64` | unbounded around zero | `(period=14, divisor=1e8)` (Python) | `period + 1` | [Indicator-EaseOfMovement.md](indicators/volume/Indicator-EaseOfMovement.md) |
|
||||
|
||||
## Pick the right indicator for…
|
||||
|
||||
A short cheat-sheet of "I want X, which indicator?" answers, grounded in
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
# ChaikinMoneyFlow
|
||||
|
||||
> Chaikin Money Flow (CMF) — the ratio of money-flow volume to total
|
||||
> volume over a rolling window, bounded to `[−1, +1]`.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Sub-category | Oscillators |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | `[−1, +1]` |
|
||||
| Default parameters | `period = 20` (Python) |
|
||||
| Warmup period | `period` |
|
||||
| Interpretation | Window accumulation/distribution balance; sign and magnitude both matter. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
MFM_t = ((close − low) − (high − close)) / (high − low) (money-flow multiplier, −1..+1)
|
||||
MFV_t = MFM_t · volume_t (money-flow volume)
|
||||
CMF_t = Σ(MFV, period) / Σ(volume, period)
|
||||
```
|
||||
|
||||
CMF is the [`Adl`](Indicator-Adl.md) increment averaged the way RSI averages
|
||||
gains: rather than a running total, it divides the *summed* money-flow volume
|
||||
of the last `period` bars by the *summed* volume of those bars. The result is
|
||||
volume-normalised, so it lives in `[−1, +1]` regardless of how heavily the
|
||||
instrument trades. A bar with `high == low` carries no positional information
|
||||
and contributes a money-flow volume of `0`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the lookback window. The Python binding defaults it to `20`; the
|
||||
Rust and Node constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/cmf.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChaikinMoneyFlow {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ChaikinMoneyFlow` is a **candle-input** indicator: it reads `high`, `low`,
|
||||
`close` and `volume`. In Python the streaming `update` accepts a 6-tuple or a
|
||||
dict; the batch helper takes `high`, `low`, `close`, `volume` numpy arrays.
|
||||
Node and WASM expose `update(high, low, close, volume)` and the matching
|
||||
`batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChaikinMoneyFlow::new(20).warmup_period() == 20`. The first value lands once
|
||||
the window holds a full `period` bars — on input index `period − 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Zero-range bar.** A bar with `high == low` contributes `MFV = 0`.
|
||||
- **Empty-volume window.** If the whole window traded zero volume, the
|
||||
`0/0` ratio is defined as `0.0` (`zero_volume_window_yields_zero` pins this).
|
||||
- **Saturated flow.** Every bar closing on its high gives `MFM = +1`, so CMF
|
||||
saturates at `+1` (`closes_at_high_yield_cmf_one` pins this).
|
||||
- **Reset.** `cmf.reset()` clears the window and both running sums.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChaikinMoneyFlow};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut cmf = ChaikinMoneyFlow::new(2)?;
|
||||
let out = cmf.batch(&[
|
||||
Candle::new(8.0, 10.0, 8.0, 10.0, 100.0, 0)?, // close at high -> MFV +100
|
||||
Candle::new(10.0, 12.0, 8.0, 10.0, 100.0, 1)?, // close mid-range -> MFV 0
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, Some(0.5)]
|
||||
```
|
||||
|
||||
Bar 1 closes at its high (`MFM = +1`, `MFV = +100`); bar 2 closes mid-range
|
||||
(`MFM = 0`, `MFV = 0`). `CMF(2) = (100 + 0) / (100 + 100) = 0.5`. This matches
|
||||
the `reference_values` test in `crates/wickra-core/src/indicators/cmf.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
cmf = ta.ChaikinMoneyFlow(2)
|
||||
high = np.array([10.0, 12.0])
|
||||
low = np.array([8.0, 8.0])
|
||||
close = np.array([10.0, 10.0])
|
||||
volume = np.array([100.0, 100.0])
|
||||
print(cmf.batch(high, low, close, volume))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[nan 0.5]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const cmf = new ta.ChaikinMoneyFlow(2);
|
||||
console.log(cmf.batch([10, 12], [8, 8], [10, 10], [100, 100]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 0.5 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
CMF reads as a balance: sustained positive values mean closes are clustering
|
||||
near bar highs on real volume (accumulation), sustained negative values mean
|
||||
the opposite (distribution). Crosses of the zero line are the textbook signal;
|
||||
the `±0.05` band is often treated as a neutral zone. Because CMF is
|
||||
volume-normalised it is comparable across instruments — unlike the raw
|
||||
[`Adl`](Indicator-Adl.md), whose level is arbitrary.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Confusing it with the ADL.** CMF is a *bounded ratio*; the ADL is an
|
||||
*unbounded running total*. They share the money-flow multiplier and nothing
|
||||
else.
|
||||
- **Feeding it scalar prices.** It needs the full OHLCV bar.
|
||||
|
||||
## References
|
||||
|
||||
Marc Chaikin's Chaikin Money Flow; the money-flow-multiplier formulation here
|
||||
matches the standard definition (StockCharts).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Adl.md](Indicator-Adl.md) — the cumulative line CMF is built on.
|
||||
- [Indicator-ChaikinOscillator.md](Indicator-ChaikinOscillator.md) — the
|
||||
EMA-difference oscillator on the ADL.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,161 @@
|
||||
# ChaikinOscillator
|
||||
|
||||
> Chaikin Oscillator — the MACD of the Accumulation/Distribution Line:
|
||||
> a fast EMA of the ADL minus a slow EMA of the ADL.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Sub-category | Oscillators |
|
||||
| Input type | `Candle` (uses `high`, `low`, `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero |
|
||||
| Default parameters | `fast = 3`, `slow = 10` (Python) |
|
||||
| Warmup period | `slow` |
|
||||
| Interpretation | Momentum of accumulation/distribution; zero-line crossings are the signal. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
ChaikinOsc_t = EMA(ADL, fast)_t − EMA(ADL, slow)_t
|
||||
```
|
||||
|
||||
The [`Adl`](Indicator-Adl.md) is an unbounded line that drifts with cumulative
|
||||
volume — useful for its slope but awkward to trade directly. The Chaikin
|
||||
Oscillator applies the MACD construction to it: difference a fast and a slow
|
||||
EMA of the ADL to get a zero-centred momentum reading. Positive values mean
|
||||
short-term accumulation is outrunning the longer trend; negative values mean
|
||||
distribution leads.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `fast` — period of the fast EMA on the ADL (classic `3`).
|
||||
- `slow` — period of the slow EMA on the ADL (classic `10`).
|
||||
|
||||
`fast` must be strictly less than `slow`. `ChaikinOscillator::classic()`
|
||||
returns the `(3, 10)` configuration.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/chaikin_oscillator.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ChaikinOscillator {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
It is a **candle-input** indicator (the ADL inside it needs `high`, `low`,
|
||||
`close`, `volume`). Python's streaming `update` accepts a 6-tuple or a dict;
|
||||
the batch helper takes `high`, `low`, `close`, `volume` numpy arrays. Node and
|
||||
WASM expose `update(high, low, close, volume)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ChaikinOscillator::classic().warmup_period() == 10`. The ADL emits a value
|
||||
from the very first candle, so both EMAs are fed every bar and the slow EMA
|
||||
gates the first output — the warmup is exactly `slow`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **Flat market.** A flat candle has zero money-flow volume, so the ADL never
|
||||
moves and both EMAs of the constant-zero series stay at zero — the
|
||||
oscillator sits at `0.0` (`flat_market_yields_zero` pins this).
|
||||
- **`fast >= slow`.** Rejected at construction with an error.
|
||||
- **Reset.** `osc.reset()` clears the ADL and both EMAs.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ChaikinOscillator};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut osc = ChaikinOscillator::classic(); // EMA(ADL, 3) − EMA(ADL, 10)
|
||||
// A flat market: the ADL never moves, so the oscillator sits at zero.
|
||||
let candles: Vec<Candle> = (0..20)
|
||||
.map(|i| Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, i).unwrap())
|
||||
.collect();
|
||||
let out = osc.batch(&candles);
|
||||
println!("{:?}", out.last().unwrap());
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Some(0.0)
|
||||
```
|
||||
|
||||
A flat series produces a flat ADL and therefore a zero oscillator. This
|
||||
matches the `flat_market_yields_zero` test in
|
||||
`crates/wickra-core/src/indicators/chaikin_oscillator.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
osc = ta.ChaikinOscillator(3, 10)
|
||||
n = 20
|
||||
flat = np.full(n, 10.0)
|
||||
print(osc.batch(flat, flat, flat, np.full(n, 100.0))[-1])
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0.0
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const osc = new ta.ChaikinOscillator(3, 10);
|
||||
const flat = Array(20).fill(10);
|
||||
const vol = Array(20).fill(100);
|
||||
const out = osc.batch(flat, flat, flat, vol);
|
||||
console.log(out[out.length - 1]);
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
0
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Trade the Chaikin Oscillator like any MACD-style line: a cross above zero is a
|
||||
bullish accumulation signal, a cross below is bearish. Divergence between the
|
||||
oscillator and price is the higher-conviction setup — for example, price
|
||||
making a new high while the oscillator does not is the same warning the raw
|
||||
ADL gives, but packaged as a bounded, zero-centred series.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Treating the level as meaningful.** Only the sign and the slope carry
|
||||
information; the magnitude scales with the instrument's volume.
|
||||
- **Feeding it scalar prices.** It needs the full OHLCV bar.
|
||||
|
||||
## References
|
||||
|
||||
Marc Chaikin's Chaikin Oscillator — the MACD construction applied to his
|
||||
Accumulation/Distribution Line (StockCharts).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Adl.md](Indicator-Adl.md) — the cumulative line this oscillates.
|
||||
- [Indicator-ChaikinMoneyFlow.md](Indicator-ChaikinMoneyFlow.md) — a bounded
|
||||
ratio built from the same money-flow volume.
|
||||
- [Indicator-MacdIndicator.md](../momentum/Indicator-MacdIndicator.md) — the
|
||||
same fast/slow EMA-difference construction on price.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,161 @@
|
||||
# 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 default `14`).
|
||||
- `divisor` — the volume-scaling constant (Python default `1e8`). Rust exposes
|
||||
`EaseOfMovement::new(period)` for the `1e8` default and
|
||||
`EaseOfMovement::with_divisor(period, divisor)` for an explicit value.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/ease_of_movement.rs`:
|
||||
|
||||
```rust
|
||||
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 = 0` instead of dividing by zero
|
||||
(`zero_volume_contributes_zero` pins this).
|
||||
- **Zero-range bar.** `high == low` makes the `(high − low)` factor zero, so
|
||||
`EMV = 0`.
|
||||
- **Constant series.** Unchanging midpoints give zero distance, so EOM stays
|
||||
at `0.0` (`constant_series_yields_zero` pins this).
|
||||
- **Reset.** `eom.reset()` clears the previous midpoint and the SMA window.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```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
|
||||
|
||||
```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
|
||||
|
||||
```javascript
|
||||
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 `divisor` you
|
||||
chose — only the sign and relative size are portable.
|
||||
- **Feeding it scalar prices.** It needs `high`, `low` *and* `volume`.
|
||||
|
||||
## References
|
||||
|
||||
Richard W. Arms Jr.'s Ease of Movement; the box-ratio formulation here matches
|
||||
the standard definition.
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-ForceIndex.md](Indicator-ForceIndex.md) — a different
|
||||
price-change-vs-volume gauge.
|
||||
- [Indicator-ChaikinMoneyFlow.md](Indicator-ChaikinMoneyFlow.md) — bounded
|
||||
money-flow balance.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
@@ -0,0 +1,155 @@
|
||||
# ForceIndex
|
||||
|
||||
> Force Index — Alexander Elder's price change scaled by volume, then
|
||||
> smoothed with an EMA.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Family | Volume |
|
||||
| Sub-category | Oscillators |
|
||||
| Input type | `Candle` (uses `close`, `volume`) |
|
||||
| Output type | `f64` |
|
||||
| Output range | unbounded around zero |
|
||||
| Default parameters | `period = 13` (Python) |
|
||||
| Warmup period | `period + 1` |
|
||||
| Interpretation | Conviction behind a move; sign and zero-crossings are the signal. |
|
||||
|
||||
## Formula
|
||||
|
||||
```
|
||||
raw_t = (close_t − close_{t−1}) · volume_t
|
||||
Force_t = EMA(raw, period)_t
|
||||
```
|
||||
|
||||
The raw force is positive on an up-close and negative on a down-close, with a
|
||||
magnitude that grows with the volume backing the move — a large move on heavy
|
||||
volume registers a large force, a large move on thin volume does not.
|
||||
Smoothing the raw series with an EMA turns the noisy per-bar reading into a
|
||||
tradeable line; Elder's classic period is `13`.
|
||||
|
||||
## Parameters
|
||||
|
||||
`period` — the EMA smoothing period. The Python binding defaults it to `13`;
|
||||
the Rust and Node constructors require it explicitly.
|
||||
|
||||
## Inputs / Outputs
|
||||
|
||||
From `crates/wickra-core/src/indicators/force_index.rs`:
|
||||
|
||||
```rust
|
||||
impl Indicator for ForceIndex {
|
||||
type Input = Candle;
|
||||
type Output = f64;
|
||||
// update(&mut self, input: Candle) -> Option<f64>
|
||||
}
|
||||
```
|
||||
|
||||
`ForceIndex` is a **candle-input** indicator that reads `close` and `volume`.
|
||||
In Python the streaming `update` accepts a 6-tuple or a dict; the batch helper
|
||||
takes `close`, `volume` numpy arrays. Node and WASM expose
|
||||
`update(close, volume)` and the matching `batch`.
|
||||
|
||||
## Warmup
|
||||
|
||||
`ForceIndex::new(13).warmup_period() == 14`. The first candle only establishes
|
||||
the previous close, so the first raw force appears on candle 2 and the first
|
||||
smoothed value on candle `period + 1`.
|
||||
|
||||
## Edge cases
|
||||
|
||||
- **First candle.** Establishes the previous close only; emits `None`.
|
||||
- **Up- vs down-trend.** A strictly rising series gives a positive force, a
|
||||
strictly falling series a negative one (`pure_uptrend_is_positive` and
|
||||
`pure_downtrend_is_negative` pin this).
|
||||
- **`period = 1`.** `EMA(1)` has `alpha = 1`, so the Force Index passes the
|
||||
raw force through unsmoothed.
|
||||
- **Reset.** `fi.reset()` clears the previous close and the EMA.
|
||||
|
||||
## Examples
|
||||
|
||||
### Rust
|
||||
|
||||
```rust
|
||||
use wickra::{BatchExt, Candle, Indicator, ForceIndex};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// ForceIndex(1): EMA(1) passes the raw force through.
|
||||
let mut fi = ForceIndex::new(1)?;
|
||||
let out = fi.batch(&[
|
||||
Candle::new(10.0, 10.0, 10.0, 10.0, 100.0, 0)?, // seeds the previous close
|
||||
Candle::new(12.0, 12.0, 12.0, 12.0, 100.0, 1)?, // raw = (12-10)·100
|
||||
Candle::new(11.0, 11.0, 11.0, 11.0, 200.0, 2)?, // raw = (11-12)·200
|
||||
]);
|
||||
println!("{:?}", out);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[None, Some(200.0), Some(-200.0)]
|
||||
```
|
||||
|
||||
This matches the `reference_values` test in
|
||||
`crates/wickra-core/src/indicators/force_index.rs`.
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import wickra as ta
|
||||
|
||||
fi = ta.ForceIndex(1)
|
||||
close = np.array([10.0, 12.0, 11.0])
|
||||
volume = np.array([100.0, 100.0, 200.0])
|
||||
print(fi.batch(close, volume))
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ nan 200. -200.]
|
||||
```
|
||||
|
||||
### Node
|
||||
|
||||
```javascript
|
||||
const ta = require('wickra');
|
||||
const fi = new ta.ForceIndex(1);
|
||||
console.log(fi.batch([10, 12, 11], [100, 100, 200]));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
[ NaN, 200, -200 ]
|
||||
```
|
||||
|
||||
## Interpretation
|
||||
|
||||
Elder reads the Force Index on two horizons. A short period (the classic `2`)
|
||||
is a sensitive entry timer — it crosses zero often. A longer period (`13`)
|
||||
tracks the conviction behind the prevailing trend: it staying above zero
|
||||
confirms buyers are in control. Divergence between a `13`-period Force Index
|
||||
and price flags an exhausting move.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **Comparing levels across instruments.** The force scales with raw volume,
|
||||
so a value of `200` means nothing without knowing the instrument.
|
||||
- **Feeding it scalar prices.** It needs `close` *and* `volume`.
|
||||
|
||||
## References
|
||||
|
||||
Alexander Elder's Force Index, introduced in *Trading for a Living* (1993).
|
||||
|
||||
## See also
|
||||
|
||||
- [Indicator-Obv.md](Indicator-Obv.md) — cumulative signed volume, a coarser
|
||||
volume-conviction gauge.
|
||||
- [Indicator-VolumePriceTrend.md](Indicator-VolumePriceTrend.md) — cumulative
|
||||
volume scaled by percentage move.
|
||||
- [Indicators-Overview.md](../../Indicators-Overview.md) — the full taxonomy.
|
||||
Reference in New Issue
Block a user