use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
use crate::traits::Next;
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct VortexIndicator {
period: usize,
vm_plus: VecDeque<f64>,
vm_minus: VecDeque<f64>,
tr: VecDeque<f64>,
sum_vm_plus: f64,
sum_vm_minus: f64,
sum_tr: f64,
prev_high: Option<f64>,
prev_low: Option<f64>,
prev_close: Option<f64>,
}
impl VortexIndicator {
pub fn new(period: usize) -> Self {
Self {
period,
vm_plus: VecDeque::with_capacity(period),
vm_minus: VecDeque::with_capacity(period),
tr: VecDeque::with_capacity(period),
sum_vm_plus: 0.0,
sum_vm_minus: 0.0,
sum_tr: 0.0,
prev_high: None,
prev_low: None,
prev_close: None,
}
}
}
impl Next<(f64, f64, f64)> for VortexIndicator {
type Output = (f64, f64);
fn next(&mut self, (high, low, close): (f64, f64, f64)) -> Self::Output {
let (vmp, vmm, tr) = match (self.prev_high, self.prev_low, self.prev_close) {
(Some(ph), Some(pl), Some(pc)) => {
let vmp = (high - pl).abs();
let vmm = (low - ph).abs();
let tr = (high - low).max((high - pc).abs()).max((low - pc).abs());
(vmp, vmm, tr)
}
_ => (0.0, 0.0, 0.0), };
self.vm_plus.push_back(vmp);
self.vm_minus.push_back(vmm);
self.tr.push_back(tr);
self.sum_vm_plus += vmp;
self.sum_vm_minus += vmm;
self.sum_tr += tr;
if self.vm_plus.len() > self.period {
if let Some(old_vmp) = self.vm_plus.pop_front() {
self.sum_vm_plus -= old_vmp;
}
if let Some(old_vmm) = self.vm_minus.pop_front() {
self.sum_vm_minus -= old_vmm;
}
if let Some(old_tr) = self.tr.pop_front() {
self.sum_tr -= old_tr;
}
}
self.prev_high = Some(high);
self.prev_low = Some(low);
self.prev_close = Some(close);
if self.sum_tr == 0.0 {
(1.0, 1.0)
} else {
(
self.sum_vm_plus / self.sum_tr,
self.sum_vm_minus / self.sum_tr,
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serde::Deserialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Deserialize)]
struct VortexCase {
high: Vec<f64>,
low: Vec<f64>,
close: Vec<f64>,
expected_plus: Vec<f64>,
expected_minus: Vec<f64>,
}
#[test]
fn test_vortex_gold_standard() {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap();
let manifest_path = Path::new(&manifest_dir);
let path = manifest_path.join("tests/gold_standard/vortex_14.json");
let path = if path.exists() {
path
} else {
manifest_path
.parent()
.unwrap()
.join("tests/gold_standard/vortex_14.json")
};
let content = fs::read_to_string(path).unwrap();
let case: VortexCase = serde_json::from_str(&content).unwrap();
let mut vi = VortexIndicator::new(14);
for i in 0..case.high.len() {
let (plus, minus) = vi.next((case.high[i], case.low[i], case.close[i]));
approx::assert_relative_eq!(plus, case.expected_plus[i], epsilon = 1e-6);
approx::assert_relative_eq!(minus, case.expected_minus[i], epsilon = 1e-6);
}
}
fn vortex_batch(data: Vec<(f64, f64, f64)>, period: usize) -> Vec<(f64, f64)> {
let mut vi = VortexIndicator::new(period);
data.into_iter().map(|x| vi.next(x)).collect()
}
proptest! {
#[test]
fn test_vortex_parity(input in prop::collection::vec((0.0..100.0, 0.0..100.0, 0.0..100.0), 1..100)) {
let mut adj_input = Vec::with_capacity(input.len());
for (h, l, c) in input {
let h_f: f64 = h;
let l_f: f64 = l;
let c_f: f64 = c;
let high = h_f.max(l_f).max(c_f);
let low = l_f.min(h_f).min(c_f);
adj_input.push((high, low, c_f));
}
let period = 14;
let mut vi = VortexIndicator::new(period);
let mut streaming_results = Vec::with_capacity(adj_input.len());
for &val in &adj_input {
streaming_results.push(vi.next(val));
}
let batch_results = vortex_batch(adj_input, period);
for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
approx::assert_relative_eq!(s.0, b.0, epsilon = 1e-6);
approx::assert_relative_eq!(s.1, b.1, epsilon = 1e-6);
}
}
}
#[test]
fn test_vortex_basic() {
let mut vi = VortexIndicator::new(3);
let (p0, m0) = vi.next((10.0, 8.0, 9.0));
assert_eq!(p0, 1.0);
assert_eq!(m0, 1.0);
let (p1, m1) = vi.next((12.0, 10.0, 11.0));
approx::assert_relative_eq!(p1, 1.3333333333, epsilon = 1e-6);
assert_eq!(m1, 0.0);
}
}
pub const VORTEX_METADATA: IndicatorMetadata = IndicatorMetadata {
name: "Vortex Indicator",
description: "The Vortex Indicator helps identify the start of a new trend or the continuation of an existing one.",
usage: "Use to detect the start of new trends. A Vortex Indicator crossover (VI+ crossing above VI-) signals the beginning of an uptrend; the reverse signals a downtrend.",
keywords: &["trend", "momentum", "classic", "breakout"],
ehlers_summary: "The Vortex Indicator, developed by Etienne Botes and Douglas Siepman (2010), is inspired by the vortex flow of water discovered by Viktor Schauberger. VI+ measures upward movement relative to the prior bar low; VI- measures downward movement relative to the prior bar high. Normalized by ATR, they produce two oscillating lines whose crossovers signal trend changes. — Technical Analysis of Stocks and Commodities, 2010",
params: &[ParamDef {
name: "period",
default: "14",
description: "Period",
}],
formula_source: "https://www.investopedia.com/terms/v/vortex-indicator-vi.asp",
formula_latex: r#"
\[
VI+ = \frac{\sum VM+}{\sum TR} \\ VI- = \frac{\sum VM-}{\sum TR}
\]
"#,
gold_standard_file: "vortex.json",
category: "Classic",
};