Files
wickra/docs/wiki/indicators/momentum/Indicator-Vortex.md
T
kingchenc 16c0639f0c F6: add Aroon Oscillator, Vortex and Mass Index
Completes the F6 family (Trend strength) end to end:

- Rust core: aroon_oscillator.rs (AroonUp - AroonDown, one-line trend
  gauge), vortex.rs (Vortex Indicator VI+/VI- with the VortexOutput
  struct), mass_index.rs (Dorsey's range-expansion sum of the
  EMA-of-range ratio). Each with a full Indicator impl, runnable doctest
  and reference / saturation / warmup / reset / batch==streaming tests.
- Python: PyAroonOscillator / PyVortex / PyMassIndex PyO3 classes +
  module registration + .pyi stubs (defaults Aroon=14, Vortex=14,
  MassIndex=(9,25)).
- Node: explicit AroonOscillatorNode, VortexNode (with VortexValue
  object) and MassIndexNode; index.d.ts and index.js updated.
- WASM: WasmAroonOscillator, WasmVortex, WasmMassIndex.
- Wiki: Indicator-AroonOscillator/Vortex/MassIndex.md plus rows in
  Indicators-Overview.md and entries in Home.md.

cargo fmt + clippy (core/wickra/data/wasm/node) clean; 320 core tests,
25 data tests and 45 doctests green.
2026-05-22 18:17:38 +02:00

5.0 KiB
Raw Blame History

Vortex

Vortex Indicator — a pair of oscillators (VI+, VI) whose crossings identify the start of a new trend.

Quick reference

Field Value
Family Momentum (trend strength)
Sub-category Directional
Input type Candle (uses high, low, close)
Output type VortexOutput { plus, minus }
Output range each line >= 0, typically around 1.0
Default parameters period = 14 (Python)
Warmup period period + 1
Interpretation VI+ above VI = up-trend; the cross marks the turn.

Formula

VM+_t = |high_t  low_{t1}|        (positive vortex movement)
VM_t = |low_t   high_{t1}|       (negative vortex movement)
TR_t  = true range
VI+   = Σ VM+ over period / Σ TR over period
VI   = Σ VM over period / Σ TR over period

Each vortex movement measures how far this bar reached against the opposite extreme of the previous bar; dividing the running sums by the running true range normalises both lines to a comparable scale around 1.0. VI+ crossing above VI signals a new up-trend; the reverse, a down-trend.

Parameters

Name Type Default Valid range Description
period usize 14 (Python) >= 1 Summation window. 0 errors with Error::PeriodZero.

The Python binding defaults period to 14.

Inputs / Outputs

From crates/wickra-core/src/indicators/vortex.rs:

pub struct VortexOutput { pub plus: f64, pub minus: f64 }

impl Indicator for Vortex {
    type Input = Candle;
    type Output = VortexOutput;
}

Vortex is a candle-input indicator reading high, low and close. The streaming update returns VortexOutput (Rust), (plus, minus) (Python), or { plus, minus } (Node/WASM). The batch helper returns one row per input — a (n, 2) numpy array in Python, a flat [plus, minus, …] array of length 2·n in Node/WASM, with NaN during warmup.

Warmup

Vortex::new(period).warmup_period() == period + 1. The first VM/TR triple needs a previous bar, so it forms on bar 2; the summation window then needs period triples — the first output lands on input period + 1.

Edge cases

  • Flat market. A window with zero total true range cannot be normalised; both lines are reported as 0.0 (perfectly_flat_market_yields_zero pins this).
  • Non-negative. Both VI+ and VI are sums of absolute values over a non-negative range, so neither is ever negative (outputs_are_non_negative pins this).
  • Candle validation. Candle::new rejects invalid bars upstream.
  • Reset. vortex.reset() clears the previous bar, the window and the three running sums.

Examples

Rust

use wickra::{BatchExt, Candle, Indicator, Vortex};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let candles = [
        Candle::new(9.0, 10.0, 8.0, 9.0, 1.0, 0)?,
        Candle::new(10.0, 12.0, 9.0, 11.0, 1.0, 1)?,
        Candle::new(12.0, 13.0, 11.0, 12.0, 1.0, 2)?,
    ];
    let mut v = Vortex::new(2)?;
    let out = v.batch(&candles);
    println!("{:?}", out[2]);
    Ok(())
}

Output:

Some(VortexOutput { plus: 1.6, minus: 0.4 })

Over the two formed bars Σ VM+ = 8, Σ VM = 2, Σ TR = 5, giving VI+ = 1.6 and VI = 0.4. This matches the reference_values test in crates/wickra-core/src/indicators/vortex.rs.

Python

import numpy as np
import wickra as ta

v = ta.Vortex(14)
high = np.array([10.0, 12.0, 13.0])
low = np.array([8.0, 9.0, 11.0])
close = np.array([9.0, 11.0, 12.0])
# v.batch(high, low, close) -> (3, 2) array of [plus, minus], NaN during warmup
print(v.update((9.0, 10.0, 8.0, 9.0, 1.0, 0)))

Node

const ta = require('wickra');
const v = new ta.Vortex(14);
console.log(v.update(12, 9, 11)); // { plus, minus } or null during warmup

Interpretation

Vortex is a trend-onset detector. The signal is the crossing: when VI+ rises above VI, a new up-trend is starting; when VI rises above VI+, a down-trend. The gap between the lines measures conviction — a wide, widening gap is a strong trend, converging lines warn of a stall. Unlike a lagging moving-average cross, the vortex movements react to the reach of each bar, so the cross tends to fire early.

Common pitfalls

  • Reading the lines in isolation. A VI+ of 1.1 means nothing on its own — what matters is its position relative to VI.
  • Feeding it scalar prices. It needs high/low/close.

References

Etienne Botes and Douglas Siepman, "The Vortex Indicator", Technical Analysis of Stocks & Commodities (2010). The VM± / true-range definition here follows their original.

See also